@thehammer/danx-dashboard-mcp 0.1.142 → 0.1.144

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
@@ -336,6 +336,39 @@ function argCheckers(tool, mode) {
336
336
  },
337
337
  };
338
338
  }
339
+ /**
340
+ * Every action-dispatched tool's args carry these three regardless of
341
+ * `action` — never action-specific, so every per-action allow-list passed to
342
+ * `refuseInapplicableFields` is unioned with this rather than repeating it.
343
+ */
344
+ const STRUCTURAL_ACTION_FIELDS = ["id", "action", "board"];
345
+ /**
346
+ * DX-3310 (code-review nit) — a field the caller sent that means nothing for
347
+ * the chosen `action` used to be silently ignored: `issue_solution`'s `add`
348
+ * built its `content` object from `title`/`body`/`pro`/`con`/`recommended`
349
+ * regardless of action, then simply never read it on `remove`/`add_step`/etc,
350
+ * and `issue_problem`'s `remove` never looked at `statement`/`context`/
351
+ * `solutions` at all. Either way the caller got a 200 with the field quietly
352
+ * discarded — no different from a typo. Refused loud instead, mirroring
353
+ * `argCheckers`' own wording: every ACTUALLY-SUPPLIED (`!== undefined`) key
354
+ * outside the action's own allow-list (plus `STRUCTURAL_ACTION_FIELDS`,
355
+ * always allowed) is named in one error, so a caller fixes every offending
356
+ * field in one round trip rather than one at a time.
357
+ */
358
+ // A9 (code-review fix) — takes `object` rather than `Record<string, unknown>`
359
+ // so every caller (each with its own specific `*Args` interface, no index
360
+ // signature) passes `args` directly, with no `as unknown as Record<...>`
361
+ // bridge cast at the call site. `Object.entries` reads the key/value pairs
362
+ // without needing an index signature on the input type either.
363
+ function refuseInapplicableFields(tool, mode, args, allowed) {
364
+ const offending = Object.entries(args)
365
+ .filter(([key, value]) => value !== undefined && !STRUCTURAL_ACTION_FIELDS.includes(key) && !allowed.has(key))
366
+ .map(([key]) => key)
367
+ .sort();
368
+ if (offending.length > 0) {
369
+ throw new Error(`${tool} ${mode} refuses fields that do not apply to this action: ${offending.join(", ")}`);
370
+ }
371
+ }
339
372
  export async function issueComment(client, args) {
340
373
  const idEnc = encodeURIComponent(args.id);
341
374
  const board = args.board;
@@ -537,10 +570,25 @@ export async function issueChecklist(client, args) {
537
570
  *
538
571
  * No answer action, for the reason `issueSolution` gives.
539
572
  */
573
+ /**
574
+ * DX-3310 (nit) — per-action allow-list for `refuseInapplicableFields`.
575
+ * `id`/`action`/`board` are structural and never listed (every action takes
576
+ * them). `type` is listed under `edit` even though it can never actually be
577
+ * SENT there (see the dedicated throw below) — that keeps the generic
578
+ * refusal from firing first with a less specific message than the one that
579
+ * already names the real reason.
580
+ */
581
+ const PROBLEM_ACTION_FIELDS = {
582
+ list: [],
583
+ add: ["statement", "context", "type", "summary", "solutions"],
584
+ edit: ["problem_id", "base_hash", "statement", "context", "summary", "type"],
585
+ remove: ["problem_id", "base_hash"],
586
+ };
540
587
  export async function issueProblem(client, args) {
541
588
  const idEnc = encodeURIComponent(args.id);
542
589
  const board = args.board;
543
590
  const need = argCheckers("issue_problem", `action=${args.action}`);
591
+ refuseInapplicableFields("issue_problem", `action=${args.action}`, args, new Set(PROBLEM_ACTION_FIELDS[args.action]));
544
592
  switch (args.action) {
545
593
  case "list":
546
594
  return client.request({ method: "GET", path: `/${idEnc}/problems`, board });
@@ -621,11 +669,24 @@ export async function issueProblem(client, args) {
621
669
  * question could release the very stop it set to wait for a human. The operator
622
670
  * answers in the dashboard.
623
671
  */
672
+ const SOLUTION_ACTION_FIELDS = {
673
+ add: ["title", "body", "pro", "con", "recommended", "steps"],
674
+ edit: ["solution_id", "base_hash", "title", "body", "pro", "con", "recommended", "steps", "steps_base_hash"],
675
+ remove: ["solution_id", "base_hash"],
676
+ add_step: ["solution_id", "title", "description", "parent_step_id", "position"],
677
+ edit_step: ["solution_id", "step_id", "base_hash", "title", "description"],
678
+ remove_step: ["solution_id", "step_id", "base_hash"],
679
+ };
624
680
  export async function issueSolution(client, args) {
625
681
  const need = argCheckers("issue_solution", `action=${args.action}`);
626
682
  // DX-2735: checked at runtime too, not only by the schema — a caller that skips
627
683
  // the MCP boundary must never build `/problems/undefined/solutions`.
628
684
  const problemId = need.id(args.problem_id, "problem_id");
685
+ // DX-3310 (nit) — `problem_id` is unioned in here rather than listed in
686
+ // every `SOLUTION_ACTION_FIELDS` entry: unlike `issue_problem` (where it
687
+ // only applies to edit/remove), `issue_solution` requires it for every
688
+ // action (checked above), so it is structural FOR THIS TOOL specifically.
689
+ refuseInapplicableFields("issue_solution", `action=${args.action}`, args, new Set(["problem_id", ...SOLUTION_ACTION_FIELDS[args.action]]));
629
690
  const base = `/${encodeURIComponent(args.id)}/problems/${problemId}/solutions`;
630
691
  const board = args.board;
631
692
  const content = {};
package/dist/index.js CHANGED
@@ -192,9 +192,6 @@ function resolveCredentialOrDie() {
192
192
  * `CLAUDE_CODE_SESSION_ID` is set on every process an interactive Claude Code
193
193
  * session spawns — this server among them — so no launcher, no dispatch
194
194
  * overlay and no agent cooperation is needed for a session to be identifiable.
195
- * The same variable is what `src/agent/nested-claude-preflight.ts` already
196
- * relies on to detect a nested session, so this is an established fact about
197
- * the runtime rather than a new assumption.
198
195
  *
199
196
  * OPTIONAL, unlike every other env this package reads. A process running
200
197
  * outside a Claude Code session has no session to report; it sends no session
@@ -342,14 +339,14 @@ const boardField = {
342
339
  .string()
343
340
  .min(1)
344
341
  .optional()
345
- .describe("Target another board by its qualified id `<repo>:<slug>`; omit for this dispatch's board. Unknown → 404."),
342
+ .describe("Board id `<repo>:<slug>`; omit for this dispatch's board. Unknown → 404."),
346
343
  };
347
344
  // The three prose fields of a card, each with ONE job. Shared by issue_create
348
345
  // (root + phase children) and issue_edit so the guidance an agent reads is
349
346
  // identical wherever it writes the field.
350
- const TITLE_DESCRIBE = 'Short, specific label naming the domain, so a reader recognises the card unopened (e.g. "Guest checkout rejects carts holding a gift card"). Never generic ("2 real decisions needed", "Fix bug", "Follow-up").';
351
- const SUMMARY_DESCRIBE = "1–3 plain-language sentences, no markdown/jargon, for someone new to this codebase: what the card is and why it matters. Always shown, never collapsed — not a second title, not a teaser for the description.";
352
- const DESCRIPTION_DESCRIBE = 'The full body ("Context"): evidence, examples, technical detail; markdown, collapsed by default. A question for the operator and its options go in issue_problem, not here. Style rules: see issue_comment\'s `text` param.';
347
+ const TITLE_DESCRIBE = 'Short, specific label naming the domain (e.g. "Guest checkout rejects gift-card carts"). Never generic ("Fix bug", "Follow-up").';
348
+ const SUMMARY_DESCRIBE = "1–3 plain-language sentences, no markdown/jargon: what the card is and why it matters. Always shown, never collapsed — not a second title, not a teaser.";
349
+ const DESCRIPTION_DESCRIBE = 'The full body ("Context"): evidence, examples, technical detail; markdown, collapsed by default. Operator questions go in issue_problem. Style: see issue_comment\'s `text` param.';
353
350
  // DX-3335 — condensed from the now-deleted danxbot:comment-style skill (PLN-11
354
351
  // R-16: fold a skill's rule into the tool description it governs rather than
355
352
  // require a separate load). Shared by every prose-writing field below so an
@@ -412,7 +409,7 @@ strictTool("issue_get",
412
409
  .optional()
413
410
  .describe("Single-id form only. How many comments the page holds (server default 20), the window anchored at the " +
414
411
  "NEWEST comment. An out-of-range value (server max 200) is refused by the server with its own 400, " +
415
- "never silently clamped here. Requires `fields` to include `comments`."),
412
+ "never silently clamped here. Requires `fields:[\"comments\"]`."),
416
413
  comments_offset: z
417
414
  .number()
418
415
  .int()
@@ -488,7 +485,7 @@ strictTool("issue_create", '`plan` is REQUIRED on every create — no default, n
488
485
  priority: z
489
486
  .union([z.enum(PRIORITY_TIER_WORDS), z.number()])
490
487
  .optional()
491
- .describe('A tier word ("lowest"=0–1…"critical"=5–6; higher number = more urgent, prefer tier word) or number in [0,6); higher numbers are more urgent. Omit for the route\'s own default.'),
488
+ .describe('Tier word ("lowest"…"critical", prefer this) or number in [0,6); higher = more urgent. Omit for the route\'s own default.'),
492
489
  // DX-3238 — `assigned_agent` is DELIBERATELY left undeclared here, unlike
493
490
  // `priority` above. The route accepts it (`ALLOWED_CREATE_KEYS`,
494
491
  // create.ts:39-56) but it only matters for the create→`list_id`-lands-
@@ -525,7 +522,7 @@ strictTool("issue_edit", 'Patch a card. ALLOWED keys: title, summary, descriptio
525
522
  status: z
526
523
  .enum(CHECKLIST_ITEM_STATUSES)
527
524
  .optional()
528
- .describe("Optional full status; omitted → from `checked`. `deferred` REQUIRES a non-empty `detail`; a 📡 item can never be `passing`."),
525
+ .describe("Optional full status; omitted → from `checked`. See tool description for the deferred/📡 rules."),
529
526
  detail: z
530
527
  .string()
531
528
  .optional()
@@ -552,7 +549,7 @@ strictTool("issue_edit", 'Patch a card. ALLOWED keys: title, summary, descriptio
552
549
  priority: z
553
550
  .union([z.enum(PRIORITY_TIER_WORDS), z.number()])
554
551
  .optional()
555
- .describe('A tier word ("lowest"=0–1…"critical"=5–6; higher number = more urgent, prefer tier word) or number in [0,6); higher numbers are more urgent. The only way to set priority.'),
552
+ .describe('Tier word ("lowest"…"critical", prefer this) or number in [0,6); higher = more urgent — see tool description for why this is the only way to set it.'),
556
553
  list_id: z.string().min(1).nullable().optional(),
557
554
  triage_enabled: z
558
555
  .boolean()
@@ -647,15 +644,15 @@ const STEP_INPUT = z.lazy(() => z
647
644
  .int()
648
645
  .positive()
649
646
  .optional()
650
- .describe("present -> this node IS an existing step (matched on this id; 400 if not a live child of this exact " +
651
- "parent scope). absent -> matched against unclaimed siblings by TITLE (first match), else created new. " +
652
- "An id-less MIDDLE insert recreates every later id-less sibling's row — send ids whenever an existing " +
653
- "step's identity matters (checked progress, a later edit)."),
647
+ .describe("present -> matches an existing step by this id (400 if not a live child of this exact parent scope, " +
648
+ "never claimed by the fallback below); absent -> matches the first unclaimed same-titled sibling " +
649
+ "under the same parent, else creates new (title-based, never positional — keeps later id-less " +
650
+ "siblings' ids intact on a middle insert). Send ids anyway when identity matters (checked progress, " +
651
+ "a later edit) or siblings share a title."),
654
652
  title: z
655
653
  .string()
656
654
  .min(1)
657
- .describe("one imperative line, the thing to do — labels (\"1.\", \"2a\") are DERIVED on read and must never appear " +
658
- "in the title itself."),
655
+ .describe("one imperative line — labels (\"1.\", \"2a\") are DERIVED on read, never put one in the title."),
659
656
  description: z.string().nullable().optional().describe("markdown detail, or null/omitted for none"),
660
657
  steps: z.array(STEP_INPUT).optional().describe("this step's own children, nested — refused past 3 levels total"),
661
658
  })
@@ -669,9 +666,7 @@ strictTool("issue_problem", "A card's PROBLEMS: one statement the operator must
669
666
  .string()
670
667
  .min(1)
671
668
  .optional()
672
- .describe("add/edit; ONE plain sentence, at most 200 characters. For a question: the question itself. For an action: " +
673
- "WHAT IS TO BE DONE — the deed itself, never phrased as a question (\"Rotate the staging DB credential\", " +
674
- "not \"Should we rotate it?\")."),
669
+ .describe("add/edit; ONE plain sentence, at most 200 characters — see `type` below for question vs action phrasing."),
675
670
  context: z
676
671
  .string()
677
672
  .nullable()
@@ -697,16 +692,14 @@ strictTool("issue_problem", "A card's PROBLEMS: one statement the operator must
697
692
  .string()
698
693
  .nullable()
699
694
  .optional()
700
- .describe("add/edit. Plain text, 1-3 sentences — never markdown detail (that's `context`'s job). For a question: " +
701
- "optional, why it matters. For an action: REQUIRED and non-empty — why the action is needed AND why you " +
702
- "(the agent raising it) cannot do it yourself; omitting it on an action add is refused 400 naming this " +
703
- "field, and an edit that would clear it on a live action problem is refused the same way. add: sent only " +
704
- "when given (omit/null on a question means none). edit: omit to keep the stored summary, null to clear it " +
705
- "(refused if the problem is a live action)."),
695
+ .describe("add/edit. Plain text, 1-3 sentences — never markdown (that's `context`'s job). Required+non-empty for an " +
696
+ "action problem (why needed + why you can't do it yourself — see `type`; refused 400 if missing on add or " +
697
+ "cleared on a live action edit); optional for a question. add: sent only when given (omit/null on a " +
698
+ "question means none). edit: omit keeps the stored summary, null clears it."),
706
699
  solutions: z
707
700
  .array(z.object({ title: z.string().min(1), ...SOLUTION_FIELDS, steps: z.array(STEP_INPUT).optional() }).strict())
708
701
  .optional()
709
- .describe("add only; fields as issue_solution add, INCLUDING steps (S6/AC 35332) — an ACTION problem's inline " +
702
+ .describe("add only; fields as issue_solution add, INCLUDING steps — an ACTION problem's inline " +
710
703
  "solutions must each carry at least one step (refused 400 otherwise); a QUESTION's may carry any number, " +
711
704
  "including none."),
712
705
  ...boardField,
@@ -715,27 +708,29 @@ strictTool("issue_problem", "A card's PROBLEMS: one statement the operator must
715
708
  strictTool("issue_solution", "One problem's options, AND one solution's individual " +
716
709
  "procedure steps; `problem_id` is REQUIRED (from issue_problem list/add; " +
717
710
  "another problem's solution id → 404). add {title, body?, pro?, con?, recommended?, steps?}: title names the " +
718
- "option/route, body its markdown detail, pro/con the case for and against; `steps` authors its WHOLE procedure " +
719
- "in one call — an ordered array of {title, description?, steps?}, nestable to 3 levels (a 4th refused 400 " +
720
- "naming the step and the limit). AC 35332 — under an ACTION problem `steps` must be non-empty (400 otherwise); " +
721
- "under a QUESTION it may be empty/omitted. Labels (\"1\", \"2a\", \"2a.i\") are ALWAYS DERIVED on read from " +
722
- "position — never put one in a title. edit :sid {base_hash, ...changed fields, steps?, steps_base_hash?}: " +
723
- "`steps` omitted leaves the procedure untouched; an explicit array (including [], refused if it would leave " +
724
- "an action's solution with zero steps) DIFFS against the stored tree the same way STEP_INPUT's own `id` field " +
725
- "describes. `steps_base_hash` is REQUIRED whenever `steps` is sent — the solution's current `steps_hash`; " +
726
- "stale → 409 `stale_steps` with the current tree. Prefer add_step/edit_step/remove_step below over resending " +
727
- "the whole tree for a one-word fix. remove :sid {base_hash}. A stale base_hash → 409 `stale_solution` with " +
728
- "currentHash + currentSolution: merge, then retry. At most ONE live recommended per problem (second → 409 " +
729
- "naming `recommended_solution_id`). A CHOSEN option's words AND whole procedure are frozen — every route below " +
730
- "also refuses once a decision has chosen this solution (add a new solution instead).\n\n" +
731
- "Granular single-step edits: add_step {solution_id, title, " +
732
- "description?, parent_step_id?, position?} — title is ONE imperative line, never a label (derived on read); " +
733
- "parent_step_id omitted/null = top-level; position 1-indexed among current live children, omitted = append, " +
734
- "beyond sibling count refused 400; nesting past depth 3 → 400. edit_step {solution_id, step_id, base_hash, " +
735
- "title?, description?} — never moves a step. remove_step {solution_id, step_id, base_hash} — soft-deletes it " +
736
- "and its own live children; refused 409 if this solution belongs to an ACTION problem and removing it would " +
737
- "leave zero live steps. A stale base_hash on a step → 409 `stale_step` with currentHash + currentStep (its " +
738
- "derived `label`).", {
711
+ "option, body/pro/con its markdown detail and case for/against; `steps` authors the WHOLE procedure in one " +
712
+ "call — {title, description?, steps?}[], nestable to 3 levels (4th refused 400, naming the step + limit). " +
713
+ "ACTION problem: `steps` must be non-empty (400 otherwise); QUESTION: may be omitted/empty. Labels " +
714
+ "(\"1\", \"2a\", \"2a.i\") are DERIVED on read from position — never put one in a title. edit :sid {base_hash, " +
715
+ "...changed fields, steps?, steps_base_hash?}: `steps` omitted leaves the procedure untouched; an explicit " +
716
+ "array (incl. [], refused if it leaves an action's solution with zero steps) DIFFS against it — send an " +
717
+ "existing step's `id` to keep it (even while retitling/reordering), omit `id` for a new step; any existing " +
718
+ "step left out is removed. An id-less node matches the first unclaimed same-titled sibling under the same " +
719
+ "parent (never positional), so an id-less MIDDLE insert keeps later id-less siblings' ids — send ids anyway " +
720
+ "when identity matters. `steps_base_hash` REQUIRED whenever `steps` is sent — the solution's current " +
721
+ "`steps_hash`; stale → 409 `stale_steps` with the current tree. Never resend the whole tree to fix one word " +
722
+ "— use add_step/edit_step/remove_step below. remove :sid {base_hash}: stale → 409 `stale_solution` with " +
723
+ "currentHash + currentSolution, merge and retry. At most ONE live recommended per problem — a second → 409 " +
724
+ "naming `recommended_solution_id`. A CHOSEN option's words and WHOLE PROCEDURE are frozen (409 — add a new " +
725
+ "one instead); every steps route below refuses once a decision has chosen this solution.\n\n" +
726
+ "Granular single-step actions, no tree resend: add_step {solution_id, title, description?, parent_step_id?, " +
727
+ "position?} — title is one imperative line (labels derived, never typed); parent_step_id omitted/null = " +
728
+ "top-level; position 1-indexed among current live children, omitted = append, beyond sibling count → 400 " +
729
+ "(never clamped); nesting past depth 3 → 400. edit_step {solution_id, step_id, base_hash, title?, " +
730
+ "description?} — never moves a step. remove_step {solution_id, step_id, base_hash} — soft-deletes it and " +
731
+ "its live children; refused 409 if this solution belongs to an ACTION problem and removing it leaves zero " +
732
+ "live steps. Stale base_hash on a step → 409 `stale_step` with currentHash + currentStep (real derived " +
733
+ "`label`).", {
739
734
  id: z.string().min(1),
740
735
  action: z.enum(["add", "edit", "remove", "add_step", "edit_step", "remove_step"]),
741
736
  problem_id: z.number().int().positive(),
@@ -833,8 +828,8 @@ strictTool("quality_gate_instruction", "Fetch a named quality gate's current ins
833
828
  // ---------------- issue_retro ----------------
834
829
  strictTool("issue_retro", "Replace the retro block: {good, bad, correctable_danxbot_problem, correctable_danxbot_problem_description, action_item_ids[], commits[], tests[]}. Refuses 409 unless the card is terminal (completed_at or cancelled_at). good/bad upsert; the three list fields REPLACE (soft-delete prior live rows, fresh ordinals). action_item_ids[] entries match <PREFIX>-N. commits[] entries: {sha, subject?}. tests[] is required (empty array = ran none): one row per test GROUP that ran (name the group, not individual unit tests) or per e2e test (kind:'e2e', listed explicitly since few/expensive); each row {name, kind:'group'|'e2e', num_tests, num_passing_tests, duration_ms} required, num_assertions/num_passing_assertions nullable (omit when the runner has no assertion totals).", {
835
830
  id: z.string().min(1),
836
- good: z.string().describe("Free-form markdown (prose or a list — pick whichever reads fastest, they render identically). Style rules: see issue_comment's `text` param."),
837
- bad: z.string().describe("Free-form markdown (prose or a list — pick whichever reads fastest, they render identically). Style rules: see issue_comment's `text` param."),
831
+ good: z.string().describe("Free-form markdown (prose or list, they render identically). Style: see issue_comment's `text` param."),
832
+ bad: z.string().describe("Free-form markdown (prose or list, they render identically). Style: see issue_comment's `text` param."),
838
833
  correctable_danxbot_problem: z
839
834
  .boolean()
840
835
  .describe("Required. Did this dispatch hit a problem in danxbot's OWN code/configuration (not \"this card was hard\") that danxbot could change so it stops happening? A true+described answer starts exactly one automated repair (fix or a ready card) — the only reliable channel for a danxbot defect found mid-dispatch. Answer explicitly every time, never a default."),
@@ -947,14 +942,14 @@ strictTool("plan_get", "Read a plan. Pass `plan_id` to read ANY plan; OMIT to re
947
942
  .int()
948
943
  .nonnegative()
949
944
  .optional()
950
- .describe("Where the `cards` page starts (default 0). Requires `fields` to include `cards`. Page with cards_offset while cards_offset + cards.length < cards_total."),
945
+ .describe("Where the `cards` page starts (default 0). Requires `fields:[\"cards\"]`. Page with cards_offset while cards_offset + cards.length < cards_total."),
951
946
  cards_limit: z
952
947
  .number()
953
948
  .int()
954
949
  .positive()
955
950
  .max(LIST_PAGE_MAX_LIMIT)
956
951
  .optional()
957
- .describe("How many cards one page holds, 1.." + LIST_PAGE_MAX_LIMIT + " (default " + PLAN_GET_CARDS_DEFAULT_LIMIT + "). Requires `fields` to include `cards`."),
952
+ .describe("How many cards one page holds, 1.." + LIST_PAGE_MAX_LIMIT + " (default " + PLAN_GET_CARDS_DEFAULT_LIMIT + "). Requires `fields:[\"cards\"]`."),
958
953
  events_limit: z
959
954
  .number()
960
955
  .int()
@@ -1049,9 +1044,9 @@ strictTool("plan_update_note", 'Edit a plan note. `content_hash` must be the not
1049
1044
  content_hash: z.string().describe("The note's `contentHash` from your last read. Required."),
1050
1045
  title: z.string().min(1).optional().describe("New title, at most 60 characters. Omit to keep the stored title."),
1051
1046
  body: z.string().min(1).optional().describe("New wrap-up, at most 250 characters. Omit to keep the stored body."),
1052
- card_ids: z.array(z.string().min(1)).optional().describe("REPLACES the whole link set when sent (with record_refs/section_ids)."),
1053
- record_refs: z.array(z.string().min(1)).optional().describe("REPLACES the whole link set when sent (with card_ids/section_ids)."),
1054
- section_ids: z.array(z.number().int().positive()).optional().describe("REPLACES the whole link set when sent (with card_ids/record_refs)."),
1047
+ card_ids: z.array(z.string().min(1)).optional().describe("Part of the link-set group — see tool description."),
1048
+ record_refs: z.array(z.string().min(1)).optional().describe("Part of the link-set group — see tool description."),
1049
+ section_ids: z.array(z.number().int().positive()).optional().describe("Part of the link-set group — see tool description."),
1055
1050
  }, async (args) => jsonResult(await planUpdateNote(client, args)));
1056
1051
  strictTool("plan_delete_note", 'Soft-delete a plan note. `content_hash` must be the note\'s last-read `contentHash` — stale deletes nothing, returns `{error: "stale_plan_note", currentHash, currentTitle, currentBody, currentLinks}` (same shape as `plan_update_note`). Takes an explicit `plan_id` (see `plan_add_note`). Unknown plan, note, or already-deleted note → 404. Returns `{notes_count}`.', {
1057
1052
  plan_id: z.number().int().positive().describe("The plan id, from `plan_list`."),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thehammer/danx-dashboard-mcp",
3
- "version": "0.1.142",
3
+ "version": "0.1.144",
4
4
  "description": "Stdio MCP server wrapping danxbot's dashboard /api/issues/* normalized DB-backed HTTP routes for dispatched agents (DX-704 Phase 2).",
5
5
  "license": "MIT",
6
6
  "type": "module",