@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 CHANGED
@@ -62,23 +62,11 @@ export async function planConnect(client, args) {
62
62
  },
63
63
  });
64
64
  }
65
- // ---------------- resource_fields (DX-3426) ----------------
66
- const RESOURCES_BASE_PATH = "/api/resources";
67
- /**
68
- * `GET /api/resources/:resource/fields` — what a field TREE (the `fields`
69
- * arg on `GET /api/issues/:id`, and any later route built on the same shape)
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) {
@@ -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
- params.set(k, String(v));
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 other plan_* operation goes through
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 — THE PLAN TOOLS ARE SESSION-BOUND, and asymmetrically so. Reads
33
- * may name any plan; WRITES take no plan id at all and act on the plan this
34
- * session is connected to, resolved server-side from the session id this
35
- * package forwards on every request. A session therefore cannot modify a plan
36
- * it is not connected to, whatever id it believes. A write from an
37
- * unconnected session is refused loudly (`session_not_connected`), never
38
- * silently dropped and never guessed at.
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, resourceFields, danxbotApi, danxbotApiList, danxbotApiSpec, attachFile, DANXBOT_API_METHODS, } from "./handlers.js";
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
- // ---------------- resource_fields ----------------
315
- strictTool("resource_fields", "DX-3426: what a field TREE (the `fields` arg on `GET /api/issues/:id` via `danxbot_api`, and any later tool that takes one) may name for one resource — `{resource, description, always: string[], hashes: string[], fields: [{name, description}], relations: [{name, description, resource, paging?: {default_limit, max_limit, page_field}}]}`. `always`/`hashes` ride every response regardless of `fields` (never request them). A relation's `resource` names what to call this same tool with next, to go one level deeper. Install-global, not board-scoped. `resource: \"issue\"` is the root; unknown resource → 404 `unknown_resource`.", {
316
- resource: z.string().min(1).describe('Resource name, e.g. "issue", or a relation\'s own `resource` from a prior call.'),
317
- }, async (args) => jsonResult(await resourceFields(client, args)));
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
- query: z.record(z.union([z.string(), z.number(), z.boolean(), z.null()])).optional(),
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 six canonical agent-visible `danxbot_complete` statuses. DX-3394
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
- // Always visible — every dispatch, of every kind, ends with this call.
136
- strictTool("danxbot_complete", "Finalize the current dispatch. Releases the worker's per-dispatch scope and ends the dispatch row. " +
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` is never in
428
- * this map — it is unconditionally visible (every dispatch ends with it).
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.164",
4
- "description": "Stdio MCP server wrapping danxbot's dashboard /api/issues/* normalized DB-backed HTTP routes for dispatched agents (DX-704 Phase 2).",
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",
@@ -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
- });
@@ -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
- }