@thehammer/danx-dashboard-mcp 0.1.164 → 0.1.166
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/handlers.js +5 -17
- package/dist/http-client.js +10 -1
- package/dist/index.js +42 -17
- package/dist/lifecycle.js +17 -5
- package/package.json +3 -3
- package/dist/entrypoint.test.js +0 -43
- package/dist/field-tree.js +0 -45
- package/dist/mantra.js +0 -128
- package/dist/priority.js +0 -70
package/dist/handlers.js
CHANGED
|
@@ -62,23 +62,11 @@ export async function planConnect(client, args) {
|
|
|
62
62
|
},
|
|
63
63
|
});
|
|
64
64
|
}
|
|
65
|
-
//
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
* may name for ONE resource: `{resource, description, always, hashes,
|
|
71
|
-
* fields, relations}`. Install-global (no `board` — the failure-category
|
|
72
|
-
* list route is board-less the same way: nothing server-side would read one).
|
|
73
|
-
* Unknown resource → 404 `unknown_resource`.
|
|
74
|
-
*/
|
|
75
|
-
export async function resourceFields(client, args) {
|
|
76
|
-
return client.request({
|
|
77
|
-
method: "GET",
|
|
78
|
-
path: `/${encodeURIComponent(args.resource)}/fields`,
|
|
79
|
-
basePath: RESOURCES_BASE_PATH,
|
|
80
|
-
});
|
|
81
|
-
}
|
|
65
|
+
// DX-3465 (code-architecture gate finding, at 275f82c0d) — the dedicated
|
|
66
|
+
// `resource_fields` handler (DX-3426) was deleted from here: it was a thin
|
|
67
|
+
// wrapper around `GET /api/resources/:resource/fields`, which `danxbot_api`
|
|
68
|
+
// already reaches with nothing agent-specific in it. See `index.ts`'s own
|
|
69
|
+
// note at the former tool-registration site.
|
|
82
70
|
// ---------------- danxbot_api (DX-3465, PLN-11 R-25) ----------------
|
|
83
71
|
export const DANXBOT_API_METHODS = ["GET", "POST", "PATCH", "PUT", "DELETE"];
|
|
84
72
|
export function isDanxbotApiMethod(value) {
|
package/dist/http-client.js
CHANGED
|
@@ -102,7 +102,16 @@ export class DashboardHttpClient {
|
|
|
102
102
|
for (const [k, v] of Object.entries(extraQuery)) {
|
|
103
103
|
if (v === undefined || v === null)
|
|
104
104
|
continue;
|
|
105
|
-
|
|
105
|
+
// DX-3465 (code-quality gate finding, at 275f82c0d) — a field TREE
|
|
106
|
+
// (`fields`, and any other tree-shaped route param) arrives here as a
|
|
107
|
+
// real object/array, not a string; `String(v)` on either produces
|
|
108
|
+
// the useless literal "[object Object]" (objects) or a comma-joined
|
|
109
|
+
// list that drops all nesting (arrays). JSON-encode both so the
|
|
110
|
+
// server's own `fields=` query-string parser (which expects exactly
|
|
111
|
+
// this shape — see `src/dashboard/...` field-tree route handling)
|
|
112
|
+
// receives what it can actually parse; a plain scalar still
|
|
113
|
+
// stringifies the same way it always did.
|
|
114
|
+
params.set(k, typeof v === "object" ? JSON.stringify(v) : String(v));
|
|
106
115
|
}
|
|
107
116
|
}
|
|
108
117
|
return `${base}${basePath}${cleanPath}?${params.toString()}`;
|
package/dist/index.js
CHANGED
|
@@ -23,19 +23,23 @@
|
|
|
23
23
|
* first to find the route for something you need to do.
|
|
24
24
|
* - danxbot_api_spec discovers the exact request/response shape of any route
|
|
25
25
|
* `danxbot_api` can call, before calling it.
|
|
26
|
-
* - resource_fields GET /api/resources/:resource/fields (DX-3426 — what `fields` may
|
|
27
|
-
* name for a resource; install-global, no `board`)
|
|
28
26
|
* - plan_connect POST /api/plan-sessions/me/plan — SESSION-BOUND (kept as a
|
|
29
|
-
* dedicated tool; every
|
|
30
|
-
* danxbot_api now
|
|
27
|
+
* dedicated tool; every plan READ or WRITE operation goes
|
|
28
|
+
* through danxbot_api now, e.g. GET /api/plans/mine or
|
|
29
|
+
* POST /api/plans/mine/records)
|
|
31
30
|
*
|
|
32
|
-
* DX-2683 —
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
31
|
+
* DX-2683, updated DX-3465 — PLAN WRITES ARE SESSION-BOUND, and
|
|
32
|
+
* asymmetrically so relative to reads. There is no dedicated plan-write
|
|
33
|
+
* tool any more (DX-3465 deleted every `plan_add_*`/`plan_update_*`/
|
|
34
|
+
* `plan_delete_*`/`plan_remove_card`/`plan_rename`/`plan_reorder_*` tool);
|
|
35
|
+
* a plan write is an ordinary `danxbot_api` call to a `/api/plans/mine/...`
|
|
36
|
+
* route. Reads may name any plan by id (`/api/plans/:id/...`); a WRITE
|
|
37
|
+
* route instead takes NO plan id at all and acts on the plan this session
|
|
38
|
+
* is connected to (`plan_connect`), resolved server-side from the session
|
|
39
|
+
* id this package forwards on every request. A session therefore cannot
|
|
40
|
+
* modify a plan it is not connected to, whatever id it believes. A write
|
|
41
|
+
* from an unconnected session is refused loudly (`session_not_connected`),
|
|
42
|
+
* never silently dropped and never guessed at.
|
|
39
43
|
*
|
|
40
44
|
* BOARD-ONLY (DX-1171): board is the first-level concept; repo is
|
|
41
45
|
* DERIVED from board server-side, never passed. The package composes the
|
|
@@ -79,7 +83,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
79
83
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
80
84
|
import { z } from "zod";
|
|
81
85
|
import { DashboardHttpClient } from "./http-client.js";
|
|
82
|
-
import { planConnect,
|
|
86
|
+
import { planConnect, danxbotApi, danxbotApiList, danxbotApiSpec, attachFile, DANXBOT_API_METHODS, } from "./handlers.js";
|
|
83
87
|
import { registerLifecycleTools, applyLifecycleVisibility, readLifecycleUrlsFromEnv, } from "./lifecycle.js";
|
|
84
88
|
function readEnvOrDie(name) {
|
|
85
89
|
const v = process.env[name];
|
|
@@ -311,10 +315,14 @@ const boardField = {
|
|
|
311
315
|
.optional()
|
|
312
316
|
.describe("Board id `<repo>:<slug>`; omit for this dispatch's board. Unknown → 404."),
|
|
313
317
|
};
|
|
314
|
-
//
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
+
// DX-3465 (code-architecture gate finding, at 275f82c0d) — `resource_fields`
|
|
319
|
+
// (DX-3426) was deleted from here: it was a dedicated wrapper around a plain
|
|
320
|
+
// `GET /api/resources/:resource/fields`, which `danxbot_api` already reaches
|
|
321
|
+
// with no agent-specific behavior earning it its own tool (R-25 keeps a
|
|
322
|
+
// dedicated tool only for what's genuinely agent-specific — session binding,
|
|
323
|
+
// local-file reads, dispatch lifecycle). Call
|
|
324
|
+
// `danxbot_api({method:"GET", path:"/api/resources/<resource>/fields"})`
|
|
325
|
+
// instead; the route and its response shape are unchanged.
|
|
318
326
|
// ---------------- danxbot_api (DX-3465, PLN-11 R-25) ----------------
|
|
319
327
|
strictTool("danxbot_api", "Call any dashboard API route your credential may reach. `method` + `path` (a FULL path, e.g. " +
|
|
320
328
|
'"/api/issues/DX-1/transition" — substitute real ids yourself, never a route template with `:id` in it) + ' +
|
|
@@ -329,7 +337,24 @@ strictTool("danxbot_api", "Call any dashboard API route your credential may reac
|
|
|
329
337
|
.min(1)
|
|
330
338
|
.startsWith("/api/")
|
|
331
339
|
.describe('Full path starting with "/api/" — never a route template.'),
|
|
332
|
-
|
|
340
|
+
// DX-3465 (code-quality gate finding, at 275f82c0d) — a query value may
|
|
341
|
+
// be a field TREE (a plain object or array, e.g.
|
|
342
|
+
// `{"description":true,"comments":{"limit":20}}`), not just a scalar —
|
|
343
|
+
// SERVER_INSTRUCTIONS below says "ALWAYS pass `fields` … a field tree"
|
|
344
|
+
// and the OpenAPI spec agrees (`fields`'s schema is `{type:"object"}`),
|
|
345
|
+
// so the schema here must accept what those two tell the caller to
|
|
346
|
+
// send. `DashboardHttpClient#buildUrl` JSON-encodes an object/array
|
|
347
|
+
// value into the query string; a scalar serializes as before.
|
|
348
|
+
query: z
|
|
349
|
+
.record(z.union([
|
|
350
|
+
z.string(),
|
|
351
|
+
z.number(),
|
|
352
|
+
z.boolean(),
|
|
353
|
+
z.null(),
|
|
354
|
+
z.record(z.unknown()),
|
|
355
|
+
z.array(z.unknown()),
|
|
356
|
+
]))
|
|
357
|
+
.optional(),
|
|
333
358
|
body: z.unknown().optional(),
|
|
334
359
|
...boardField,
|
|
335
360
|
}, async (args) => jsonResult(await danxbotApi(client, args)));
|
package/dist/lifecycle.js
CHANGED
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
*/
|
|
39
39
|
import { z } from "zod";
|
|
40
40
|
/**
|
|
41
|
-
* The
|
|
41
|
+
* The five canonical agent-visible `danxbot_complete` statuses. DX-3394
|
|
42
42
|
* problem 1682 / Core Principle 1 — the pre-DX-770 deprecated aliases
|
|
43
43
|
* (`completed`, `agent_blocked`) were already gone; this merge additionally
|
|
44
44
|
* drops the `ready` / `archive` / `review` backward-compat aliases that
|
|
@@ -132,8 +132,15 @@ function parseOptionalAbsolutePathArray(field, value) {
|
|
|
132
132
|
*/
|
|
133
133
|
export function registerLifecycleTools(strictTool) {
|
|
134
134
|
// ---------------- danxbot_complete ----------------
|
|
135
|
-
//
|
|
136
|
-
|
|
135
|
+
// DX-3465 (code-quality gate finding, at 275f82c0d) — this tool used to be
|
|
136
|
+
// unconditionally visible on the theory that "every dispatch ends with
|
|
137
|
+
// it", but an OPERATOR session is not a dispatch: it has no
|
|
138
|
+
// DANXBOT_STOP_URL, so `danxbot_complete` was always advertised there and
|
|
139
|
+
// every call threw. It is now gated on the stop URL exactly like every
|
|
140
|
+
// other lifecycle tool below (`applyLifecycleVisibility`) — a real
|
|
141
|
+
// dispatch always has the URL, so nothing is lost there; an operator
|
|
142
|
+
// session simply never sees a tool it could never successfully call.
|
|
143
|
+
const danxbotComplete = strictTool("danxbot_complete", "Finalize the current dispatch. Releases the worker's per-dispatch scope and ends the dispatch row. " +
|
|
137
144
|
"Does NOT move any card — if you're working an issue-bound dispatch and want the card to move " +
|
|
138
145
|
"(Done / Blocked / Cancelled / etc.), transition the card FIRST through danxbot_api " +
|
|
139
146
|
"({method:'POST', path:'/api/issues/:id/transition', body:{action:...}}), then call this tool. " +
|
|
@@ -409,6 +416,7 @@ export function registerLifecycleTools(strictTool) {
|
|
|
409
416
|
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
410
417
|
});
|
|
411
418
|
return {
|
|
419
|
+
danxbotComplete,
|
|
412
420
|
slackReply,
|
|
413
421
|
slackPostUpdate,
|
|
414
422
|
linkThreadToIssue,
|
|
@@ -424,8 +432,11 @@ export function registerLifecycleTools(strictTool) {
|
|
|
424
432
|
/**
|
|
425
433
|
* Disable every conditionally-visible lifecycle tool whose URL is absent
|
|
426
434
|
* from `urls` — the same advertise-filter contract `buildActiveTools` used
|
|
427
|
-
* to enforce in the old standalone server. `danxbot_complete`
|
|
428
|
-
*
|
|
435
|
+
* to enforce in the old standalone server. `danxbot_complete` (DX-3465,
|
|
436
|
+
* code-quality gate finding, at 275f82c0d) is gated on `urls.stop` the same
|
|
437
|
+
* way — a real dispatch always carries `DANXBOT_STOP_URL`, so it stays
|
|
438
|
+
* visible there; an operator session (no stop URL) never advertises a tool
|
|
439
|
+
* it could never successfully call.
|
|
429
440
|
*/
|
|
430
441
|
export function applyLifecycleVisibility(tools, urls) {
|
|
431
442
|
const setVisible = (tool, visible) => {
|
|
@@ -434,6 +445,7 @@ export function applyLifecycleVisibility(tools, urls) {
|
|
|
434
445
|
else
|
|
435
446
|
tool.disable();
|
|
436
447
|
};
|
|
448
|
+
setVisible(tools.danxbotComplete, !!urls.stop);
|
|
437
449
|
setVisible(tools.slackReply, !!urls.slackReply);
|
|
438
450
|
setVisible(tools.slackPostUpdate, !!urls.slackUpdate);
|
|
439
451
|
setVisible(tools.linkThreadToIssue, !!urls.linkThreadToIssue);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thehammer/danx-dashboard-mcp",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Stdio MCP server
|
|
3
|
+
"version": "0.1.166",
|
|
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",
|
|
7
7
|
"main": "dist/index.js",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"node": ">=20"
|
|
22
22
|
},
|
|
23
23
|
"scripts": {
|
|
24
|
-
"build": "tsc -p tsconfig.json && node -e \"require('fs').chmodSync('dist/index.js', 0o755)\"",
|
|
24
|
+
"build": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.json && node -e \"require('fs').chmodSync('dist/index.js', 0o755)\"",
|
|
25
25
|
"verify:boot": "bash scripts/verify-boot.sh",
|
|
26
26
|
"start": "node dist/index.js",
|
|
27
27
|
"dev": "tsx src/index.ts",
|
package/dist/entrypoint.test.js
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
|
2
|
-
import { mkdtempSync, writeFileSync, symlinkSync, rmSync, realpathSync, } from "node:fs";
|
|
3
|
-
import { tmpdir } from "node:os";
|
|
4
|
-
import { join } from "node:path";
|
|
5
|
-
import { pathToFileURL } from "node:url";
|
|
6
|
-
import { isEntrypointModule } from "./entrypoint.js";
|
|
7
|
-
describe("isEntrypointModule (DX-1647)", () => {
|
|
8
|
-
let dir;
|
|
9
|
-
let real;
|
|
10
|
-
let link;
|
|
11
|
-
beforeAll(() => {
|
|
12
|
-
dir = mkdtempSync(join(tmpdir(), "entrypoint-test-"));
|
|
13
|
-
real = join(dir, "index.js");
|
|
14
|
-
writeFileSync(real, "// stub entry\n");
|
|
15
|
-
link = join(dir, "danx-dashboard-mcp"); // mimics node_modules/.bin symlink
|
|
16
|
-
symlinkSync(real, link);
|
|
17
|
-
});
|
|
18
|
-
afterAll(() => {
|
|
19
|
-
rmSync(dir, { recursive: true, force: true });
|
|
20
|
-
});
|
|
21
|
-
// import.meta.url always reports the module REALPATH — model that here.
|
|
22
|
-
const moduleUrl = () => pathToFileURL(realpathSync(real)).href;
|
|
23
|
-
it("true when argv[1] is the real file (direct `node index.js`)", () => {
|
|
24
|
-
expect(isEntrypointModule(moduleUrl(), real)).toBe(true);
|
|
25
|
-
});
|
|
26
|
-
it("true when argv[1] is a SYMLINK to the file (npx / global bin) — the fleet regression", () => {
|
|
27
|
-
// The symlink path !== the realpath, but the entrypoint MUST still be
|
|
28
|
-
// detected, or npx imports the module and exits without booting the server.
|
|
29
|
-
expect(link).not.toBe(realpathSync(link));
|
|
30
|
-
expect(isEntrypointModule(moduleUrl(), link)).toBe(true);
|
|
31
|
-
});
|
|
32
|
-
it("false when argv[1] is undefined (module imported, not run as bin)", () => {
|
|
33
|
-
expect(isEntrypointModule(moduleUrl(), undefined)).toBe(false);
|
|
34
|
-
});
|
|
35
|
-
it("false when argv[1] is an empty string", () => {
|
|
36
|
-
expect(isEntrypointModule(moduleUrl(), "")).toBe(false);
|
|
37
|
-
});
|
|
38
|
-
it("false when argv[1] points at a different real file", () => {
|
|
39
|
-
const other = join(dir, "other.js");
|
|
40
|
-
writeFileSync(other, "// other\n");
|
|
41
|
-
expect(isEntrypointModule(moduleUrl(), other)).toBe(false);
|
|
42
|
-
});
|
|
43
|
-
});
|
package/dist/field-tree.js
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* DX-3426 — the shared recursive FIELD-TREE shape.
|
|
3
|
-
*
|
|
4
|
-
* The dashboard's single-card, batch card and plan reads (`danxbot_api`
|
|
5
|
-
* against `GET /api/issues/:id`, `GET /api/issues`, `GET /api/plans/:planId`
|
|
6
|
-
* — DX-3428) take a nested JSON tree instead of a
|
|
7
|
-
* flat field-GROUP list: each key names a field or relation of the resource
|
|
8
|
-
* at that level, and each value is either `true` (the field, or a relation's
|
|
9
|
-
* child with its default fields), a non-negative integer (a cursor-paged
|
|
10
|
-
* relation's own `limit` argument), an opaque cursor STRING (a paged
|
|
11
|
-
* relation's `before` — DX-3428: not every relation's cursor is a plain
|
|
12
|
-
* integer, e.g. a plan event's visibility-scanned row id), a non-empty
|
|
13
|
-
* STRING or ARRAY OF STRINGS (a relation's own declared, non-paging argument
|
|
14
|
-
* — e.g. `records`' `kind`, `events`' `kinds`/`origin`/`writer`), or another
|
|
15
|
-
* field tree (a relation's child with a NAMED field selection). Example:
|
|
16
|
-
*
|
|
17
|
-
* {"description": true, "ac": true, "problems": {"solutions": {"steps": true}, "decisions": true}, "comments": {"limit": 10}, "records": {"kind": "goal"}}
|
|
18
|
-
*
|
|
19
|
-
* One schema, one module — every tool that accepts a field tree imports THIS
|
|
20
|
-
* export rather than redeclaring the shape, so the recursion is defined once
|
|
21
|
-
* and every caller's JSON Schema serializes the same way. `z.lazy` is
|
|
22
|
-
* required because the schema refers to itself; `zod-to-json-schema` (the
|
|
23
|
-
* MCP SDK's own zod v3 → JSON Schema converter, see
|
|
24
|
-
* `@modelcontextprotocol/sdk`'s `zod-json-schema-compat.ts`) resolves that
|
|
25
|
-
* self-reference to a local `$ref` rather than unrolling it infinitely —
|
|
26
|
-
* verified directly against the listed tool schema in
|
|
27
|
-
* `__tests__/tool-defs.test.ts`, not assumed from reading the library's
|
|
28
|
-
* source. The problem-solutions route's pre-existing `steps` field (DX-3310)
|
|
29
|
-
* is the same `z.lazy` self-reference pattern already shipping on this server.
|
|
30
|
-
*/
|
|
31
|
-
import { z } from "zod";
|
|
32
|
-
/**
|
|
33
|
-
* The shared schema. A record whose values are `true`, a non-negative
|
|
34
|
-
* integer, a non-empty string, a non-empty array of non-empty strings, or
|
|
35
|
-
* the same schema recursively — generic on purpose (no issue/plan-specific
|
|
36
|
-
* field names baked in here) so every tool that reads a resource tree
|
|
37
|
-
* reuses it unchanged.
|
|
38
|
-
*/
|
|
39
|
-
export const fieldTreeSchema = z.lazy(() => z.record(z.union([
|
|
40
|
-
z.literal(true),
|
|
41
|
-
z.number().int().nonnegative(),
|
|
42
|
-
z.string().min(1),
|
|
43
|
-
z.array(z.string().min(1)).min(1),
|
|
44
|
-
fieldTreeSchema,
|
|
45
|
-
])));
|
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
|
-
}
|
package/dist/priority.js
DELETED
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tier-word → numeric priority resolution for `danxbot_api`'s
|
|
3
|
-
* `PATCH /api/issues/:id/edit` route (DX-1532).
|
|
4
|
-
*
|
|
5
|
-
* Priority is a STRUCTURED numeric column `issues.priority` (float in `[0,6)`).
|
|
6
|
-
* Both the Trello "priority" label and the dashboard priority badge read it via
|
|
7
|
-
* `priorityTier(issue.priority)` — description PROSE ("Priority: Low") is read
|
|
8
|
-
* by neither. So an agent that "sets priority" by editing description text
|
|
9
|
-
* produces a silent false-positive: nothing downstream changes. The fix is to
|
|
10
|
-
* let the agent write the numeric column through the issue-edit route.
|
|
11
|
-
*
|
|
12
|
-
* Agents (and operators) speak in tier WORDS ("low" / "high"), so the tool
|
|
13
|
-
* accepts a tier word and resolves it to the tier's MIDPOINT here — the inverse
|
|
14
|
-
* of `src/issue-tracker/priority-tier.ts#priorityTier`. A raw numeric is passed
|
|
15
|
-
* through unchanged for the server route to range-validate (it 400s on
|
|
16
|
-
* out-of-`[0,6)`), so the MCP boundary never has to duplicate that bound.
|
|
17
|
-
*
|
|
18
|
-
* This package is a standalone published artifact and cannot import the
|
|
19
|
-
* backend's `priority-tier` module, so the midpoint table is duplicated here.
|
|
20
|
-
* Each value IS the `[min,max)` bucket midpoint by construction and equals the
|
|
21
|
-
* backend's `PRIORITY_TIERS.defaultValue`; `src/issue-tracker/priority-tier.test.ts`
|
|
22
|
-
* asserts `priorityTier(defaultValue) === key` on the canonical side, pinning
|
|
23
|
-
* the round-trip these midpoints rely on.
|
|
24
|
-
*/
|
|
25
|
-
/**
|
|
26
|
-
* The six priority tier words, low → high. A const tuple so it doubles as the
|
|
27
|
-
* `z.enum` source in `index.ts`.
|
|
28
|
-
*/
|
|
29
|
-
export const PRIORITY_TIER_WORDS = [
|
|
30
|
-
"lowest",
|
|
31
|
-
"low",
|
|
32
|
-
"medium",
|
|
33
|
-
"high",
|
|
34
|
-
"very_high",
|
|
35
|
-
"critical",
|
|
36
|
-
];
|
|
37
|
-
/**
|
|
38
|
-
* Tier word → bucket midpoint (inverse of `priorityTier`). Lands the numeric
|
|
39
|
-
* column squarely inside the requested tier so a later `priorityTier` read
|
|
40
|
-
* returns the same word.
|
|
41
|
-
*/
|
|
42
|
-
export const PRIORITY_TIER_MIDPOINTS = {
|
|
43
|
-
lowest: 0.5,
|
|
44
|
-
low: 1.5,
|
|
45
|
-
medium: 2.5,
|
|
46
|
-
high: 3.5,
|
|
47
|
-
very_high: 4.5,
|
|
48
|
-
critical: 5.5,
|
|
49
|
-
};
|
|
50
|
-
/**
|
|
51
|
-
* Resolve a priority input — a tier WORD or a raw numeric — to the numeric
|
|
52
|
-
* value the `PATCH /api/issues/:id/edit` route stores in `issues.priority`.
|
|
53
|
-
*
|
|
54
|
-
* - tier word → its bucket midpoint (`PRIORITY_TIER_MIDPOINTS`).
|
|
55
|
-
* - number → passed through unchanged; the server route range-validates it to
|
|
56
|
-
* `[0,6)` and 400s otherwise (the MCP boundary deliberately does not
|
|
57
|
-
* re-validate the bound — single source of truth on the route).
|
|
58
|
-
* - unknown string → throws here at the MCP boundary (no round-trip). The Zod
|
|
59
|
-
* enum on the tool schema already rejects this for tool callers; the explicit
|
|
60
|
-
* throw keeps the helper safe for any direct caller.
|
|
61
|
-
*/
|
|
62
|
-
export function resolvePriority(input) {
|
|
63
|
-
if (typeof input === "number")
|
|
64
|
-
return input;
|
|
65
|
-
const midpoint = PRIORITY_TIER_MIDPOINTS[input];
|
|
66
|
-
if (midpoint === undefined) {
|
|
67
|
-
throw new Error(`issue-edit: unknown priority tier word "${input}" (expected one of ${PRIORITY_TIER_WORDS.join(", ")} or a number in [0,6))`);
|
|
68
|
-
}
|
|
69
|
-
return midpoint;
|
|
70
|
-
}
|