@thehammer/danx-dashboard-mcp 0.1.87 → 0.1.88

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
@@ -392,11 +392,7 @@ export async function issueComment(client, args) {
392
392
  return client.request({
393
393
  method: "POST",
394
394
  path: `/${idEnc}/comments`,
395
- body: {
396
- text,
397
- ...(args.metadata !== undefined ? { metadata: args.metadata } : {}),
398
- ...(args.problem_id !== undefined ? { problem_id: args.problem_id } : {}),
399
- },
395
+ body: { text, ...(args.metadata !== undefined ? { metadata: args.metadata } : {}) },
400
396
  board,
401
397
  });
402
398
  }
@@ -848,7 +844,6 @@ export const PLAN_FIELD_GROUPS = [
848
844
  "records:caveat",
849
845
  "architecture",
850
846
  "sessions",
851
- "notes",
852
847
  ];
853
848
  /**
854
849
  * DX-2834 — the plan-status taxonomy, mirroring `PLAN_FIELD_GROUPS` just
@@ -1152,85 +1147,6 @@ export async function planDeleteRecord(client, args) {
1152
1147
  body: { content_hash: args.content_hash },
1153
1148
  });
1154
1149
  }
1155
- /**
1156
- * Write a milestone note to a plan, via `POST /api/plans/:plan_id/notes`.
1157
- * TAKES AN EXPLICIT `plan_id`, unlike `plan_add_record`/`plan_add_architecture_section`
1158
- * — mirrors `plan_add_card`'s sibling `plan_remove_card`/`plan_rename`: a
1159
- * dispatched worker has no plan connection when it finishes a card, and a
1160
- * card can sit on several plans, so the writer names which one the note
1161
- * belongs to. A note is a MILESTONE, not a log — write one when a card (or a
1162
- * related group of cards) finishes, an important decision lands, or a
1163
- * goal/rule/caveat/architecture section changes meaningfully; routine step
1164
- * progress stays a card comment. Links resolve on read into what they point
1165
- * at (a card's title, a record's ref + body, a section's title) — an unknown
1166
- * card id, an unparseable or foreign record ref, or an unknown or foreign
1167
- * section id is refused 400 naming exactly which one. A card link does NOT
1168
- * require the card to be a member of this plan; a record/section link MUST
1169
- * belong to THIS plan. `author` is stamped server-side from your identity,
1170
- * never sent by you. Unknown plan → 404. Returns the new note plus the
1171
- * plan's latest notes page.
1172
- */
1173
- export async function planAddNote(client, args) {
1174
- return client.request({
1175
- method: "POST",
1176
- path: `/${args.plan_id}/notes`,
1177
- basePath: PLANS_BASE_PATH,
1178
- body: {
1179
- title: args.title,
1180
- body: args.body,
1181
- ...(args.card_ids === undefined ? {} : { card_ids: args.card_ids }),
1182
- ...(args.record_refs === undefined ? {} : { record_refs: args.record_refs }),
1183
- ...(args.section_ids === undefined ? {} : { section_ids: args.section_ids }),
1184
- },
1185
- });
1186
- }
1187
- /**
1188
- * Edit a plan note, via `PATCH /api/plans/:plan_id/notes/:note_id`.
1189
- * `content_hash` MUST be the note's `contentHash` from your last read; on a
1190
- * mismatch nothing is written and you get `{error: "stale_plan_note",
1191
- * currentHash, currentTitle, currentBody, currentLinks}` — merge into those
1192
- * and retry with `content_hash: currentHash`, never blindly. `title`/`body`
1193
- * are each optional and keep their stored value when omitted. The LINK
1194
- * fields are all-or-nothing as a GROUP: omit all three to keep the stored
1195
- * link set untouched; send ANY one of them to REPLACE THE WHOLE SET (never a
1196
- * per-link add/remove — the set is the unit of change). The hash covers
1197
- * title, body AND the link set, so a stale read of any of the three is
1198
- * refused. TAKES AN EXPLICIT `plan_id`, same reason as `plan_add_note`.
1199
- * Unknown plan or note id → 404. Returns the edited note plus the plan's
1200
- * latest notes page.
1201
- */
1202
- export async function planUpdateNote(client, args) {
1203
- return client.request({
1204
- method: "PATCH",
1205
- path: `/${args.plan_id}/notes/${args.note_id}`,
1206
- basePath: PLANS_BASE_PATH,
1207
- body: {
1208
- content_hash: args.content_hash,
1209
- ...(args.title === undefined ? {} : { title: args.title }),
1210
- ...(args.body === undefined ? {} : { body: args.body }),
1211
- ...(args.card_ids === undefined ? {} : { card_ids: args.card_ids }),
1212
- ...(args.record_refs === undefined ? {} : { record_refs: args.record_refs }),
1213
- ...(args.section_ids === undefined ? {} : { section_ids: args.section_ids }),
1214
- },
1215
- });
1216
- }
1217
- /**
1218
- * Soft-delete a plan note, via `DELETE /api/plans/:plan_id/notes/:note_id`.
1219
- * `content_hash` MUST be the note's `contentHash` from your last read; a
1220
- * stale hash deletes nothing and returns the same `{error:
1221
- * "stale_plan_note", currentHash, currentTitle, currentBody, currentLinks}`
1222
- * shape `plan_update_note` uses. TAKES AN EXPLICIT `plan_id`, same reason as
1223
- * `plan_add_note`. Unknown plan, unknown note, or an already-deleted note →
1224
- * 404. Returns the plan's remaining latest notes page.
1225
- */
1226
- export async function planDeleteNote(client, args) {
1227
- return client.request({
1228
- method: "DELETE",
1229
- path: `/${args.plan_id}/notes/${args.note_id}`,
1230
- basePath: PLANS_BASE_PATH,
1231
- body: { content_hash: args.content_hash },
1232
- });
1233
- }
1234
1150
  // ---------------- failure_category_list / _create / _update (DX-2792) ----------------
1235
1151
  /**
1236
1152
  * DX-2792 (Failure evaluation 3/4) — wraps `src/dashboard/failure-categories-routes.ts`,
package/dist/index.js CHANGED
@@ -94,14 +94,13 @@
94
94
  */
95
95
  import { isEntrypointModule } from "./entrypoint.js";
96
96
  import { BRIDGE_SUBCOMMAND, runBridgeCommand } from "./bridge.js";
97
- import { PLAN_STATE_SUBCOMMAND, runPlanStateCommand } from "./plan-state.js";
98
97
  import { resolveDeclaredCredential } from "./credential.js";
99
98
  import { recordSessionConnectionAfterConnect } from "./session-connection.js";
100
99
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
101
100
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
102
101
  import { z } from "zod";
103
102
  import { DashboardHttpClient } from "./http-client.js";
104
- import { issueAttach, issueChecklist, issueComment, issueCreate, issueDependency, issueEdit, issueGet, issueList, issueProblem, issueQualityGate, issueQualityGateVerdict, issueRetireBranch, issueRetro, issueSolution, issueTransition, issueTriage, briefGetPage, briefList, briefSetPage, failureCategoryCreate, failureCategoryList, failureCategoryUpdate, planAddArchitectureSection, planAddCard, planAddNote, planAddRecord, planConnect, planCreate, planDeleteArchitectureSection, planDeleteNote, planDeleteRecord, planGet, PLAN_FIELD_GROUPS, PLAN_STATUSES, ISSUE_BATCH_GET_MAX, LIST_PAGE_MAX_LIMIT, PLAN_GET_CARDS_DEFAULT_LIMIT, planGetArchitectureSection, planGetRecord, planList, planRemoveCard, planRename, planReorderArchitectureSection, planUpdateArchitectureSection, planUpdateNote, planUpdateRecord, repoKnowledgeGet, repoKnowledgeSet, } from "./handlers.js";
103
+ import { issueAttach, issueChecklist, issueComment, issueCreate, issueDependency, issueEdit, issueGet, issueList, issueProblem, issueQualityGate, issueQualityGateVerdict, issueRetireBranch, issueRetro, issueSolution, issueTransition, issueTriage, briefGetPage, briefList, briefSetPage, failureCategoryCreate, failureCategoryList, failureCategoryUpdate, planAddArchitectureSection, planAddCard, planAddRecord, planConnect, planCreate, planDeleteArchitectureSection, planDeleteRecord, planGet, PLAN_FIELD_GROUPS, PLAN_STATUSES, ISSUE_BATCH_GET_MAX, LIST_PAGE_MAX_LIMIT, PLAN_GET_CARDS_DEFAULT_LIMIT, planGetArchitectureSection, planGetRecord, planList, planRemoveCard, planRename, planReorderArchitectureSection, planUpdateArchitectureSection, planUpdateRecord, repoKnowledgeGet, repoKnowledgeSet, } from "./handlers.js";
105
104
  import { PRIORITY_TIER_WORDS } from "./priority.js";
106
105
  function readEnvOrDie(name) {
107
106
  const v = process.env[name];
@@ -504,13 +503,12 @@ server.tool("issue_triage", "Record a triage confidence score via POST /api/issu
504
503
  ...boardField,
505
504
  }, async (args) => jsonResult(await issueTriage(client, args)));
506
505
  // ---------------- issue_comment ----------------
507
- server.tool("issue_comment", "Comment CRUD via /api/issues/:id/comments[/:cid]. action=add → POST {text, metadata?, problem_id?} (server stamps author from bearer + auto-incrementing ordinal); action=edit → PATCH /:cid {text}; action=delete → DELETE /:cid (soft-delete, audit trail preserved — comments are NEVER hard-deleted). Client-supplied author is IGNORED (server-stamped to prevent impersonation). `metadata` (DX-2157, action=add only) is an OPTIONAL opaque JSON object a calling app attaches to the comment — e.g. a generated `{sql, explanation}` packet its own UI renders specially. danxbot stores + returns it verbatim and enforces NO shape on its contents; omit for a plain markdown-only comment (unaffected either way). `problem_id` (DX-2906, action=add only) OPTIONALLY threads the comment as a follow-up question under one of this SAME card's problems (from `issue_problem` list/add) WITHOUT answering it — commenting never changes open_problem_count, blocked, or records a decision; use issue_problem's answer route for that. An unknown id, another card's id, or a removed problem's id all 404 naming the problem id — an ANSWERED (but not removed) problem still accepts a follow-up comment, since the thread continues after a decision.", {
506
+ server.tool("issue_comment", "Comment CRUD via /api/issues/:id/comments[/:cid]. action=add → POST {text, metadata?} (server stamps author from bearer + auto-incrementing ordinal); action=edit → PATCH /:cid {text}; action=delete → DELETE /:cid (soft-delete, audit trail preserved — comments are NEVER hard-deleted). Client-supplied author is IGNORED (server-stamped to prevent impersonation). `metadata` (DX-2157, action=add only) is an OPTIONAL opaque JSON object a calling app attaches to the comment — e.g. a generated `{sql, explanation}` packet its own UI renders specially. danxbot stores + returns it verbatim and enforces NO shape on its contents; omit for a plain markdown-only comment (unaffected either way).", {
508
507
  id: z.string().min(1),
509
508
  action: z.enum(["add", "edit", "delete"]),
510
509
  comment_id: z.number().int().positive().optional(),
511
510
  text: z.string().min(1).optional(),
512
511
  metadata: z.record(z.unknown()).optional(),
513
- problem_id: z.number().int().positive().optional().describe("action=add only — thread this comment as a follow-up under a LIVE problem of this same card, without answering it"),
514
512
  ...boardField,
515
513
  }, async (args) => jsonResult(await issueComment(client, args)));
516
514
  // ---------------- issue_checklist ----------------
@@ -794,32 +792,6 @@ server.tool("plan_delete_record", 'Soft-delete a goal/rule/caveat of your connec
794
792
  record_id: z.number().int().positive().describe("The record id to delete."),
795
793
  content_hash: z.string().describe("The record's `contentHash` from your last read. Required."),
796
794
  }, async (args) => jsonResult(await planDeleteRecord(client, args)));
797
- server.tool("plan_add_note", "Write a milestone note to a plan's timeline, via POST /api/plans/:plan_id/notes (DX-2915). A note is a MILESTONE, not a log — write one for a card (or related group of cards) finishing, an important decision, or a meaningful goal/rule/caveat/architecture-section change; routine step progress stays a card comment, never a note. Terse tone: `title` at most 60 characters, `body` (the wrap-up) at most 250 (both 400 if too long, naming the limit and actual length). Links resolve on read into what they point at (a card's title, a record's ref+body, a section's title): an unknown card, an unparseable or foreign record ref, or an unknown or foreign section id is refused 400 naming exactly which one. A card link does NOT require the card to be a member of this plan; a record/section link MUST belong to THIS plan. `author` is stamped from your identity server-side — there is no field for it. TAKES AN EXPLICIT `plan_id` (like `plan_remove_card`/`plan_rename`), so a dispatched worker with no plan connection can still write. Unknown plan → 404. Returns the new note plus the plan's latest notes page.", {
798
- plan_id: z.number().int().positive().describe("The plan id, from `plan_list`."),
799
- title: z.string().min(1).describe("At most 60 characters."),
800
- body: z.string().min(1).describe("The wrap-up, at most 250 characters."),
801
- card_ids: z.array(z.string().min(1)).optional().describe("Card ids this note concerns, e.g. `[\"DX-2894\"]`."),
802
- record_refs: z
803
- .array(z.string().min(1))
804
- .optional()
805
- .describe("Goal/rule/caveat references this note announces, e.g. `[\"G-1\", \"R-3\", \"CAV-2\"]`."),
806
- section_ids: z.array(z.number().int().positive()).optional().describe("Architecture section ids this note announces."),
807
- }, async (args) => jsonResult(await planAddNote(client, args)));
808
- server.tool("plan_update_note", 'Edit a plan note, via PATCH /api/plans/:plan_id/notes/:note_id (DX-2915). `content_hash` MUST be the note\'s `contentHash` from your last read; a mismatch writes NOTHING and returns `{error: "stale_plan_note", currentHash, currentTitle, currentBody, currentLinks}` — merge into those and retry with `content_hash: currentHash`, never blindly. `title`/`body` are each optional and keep their stored value when omitted. The link fields (`card_ids`/`record_refs`/`section_ids`) are all-or-nothing AS A GROUP: omit all three to leave the stored link set untouched; send ANY one of them to REPLACE THE WHOLE SET — there is no per-link add/remove. The hash covers title, body AND the link set. TAKES AN EXPLICIT `plan_id`, same reason as `plan_add_note`. Unknown plan or note id → 404. Returns the edited note plus the plan\'s latest notes page.', {
809
- plan_id: z.number().int().positive().describe("The plan id, from `plan_list`."),
810
- note_id: z.number().int().positive().describe("The note id to edit."),
811
- content_hash: z.string().describe("The note's `contentHash` from your last read. Required."),
812
- title: z.string().min(1).optional().describe("New title, at most 60 characters. Omit to keep the stored title."),
813
- body: z.string().min(1).optional().describe("New wrap-up, at most 250 characters. Omit to keep the stored body."),
814
- card_ids: z.array(z.string().min(1)).optional().describe("REPLACES the whole link set when sent (with record_refs/section_ids)."),
815
- record_refs: z.array(z.string().min(1)).optional().describe("REPLACES the whole link set when sent (with card_ids/section_ids)."),
816
- section_ids: z.array(z.number().int().positive()).optional().describe("REPLACES the whole link set when sent (with card_ids/record_refs)."),
817
- }, async (args) => jsonResult(await planUpdateNote(client, args)));
818
- server.tool("plan_delete_note", 'Soft-delete a plan note, via DELETE /api/plans/:plan_id/notes/:note_id (DX-2915). `content_hash` must be the note\'s `contentHash` from your last read; a stale hash deletes nothing and returns `{error: "stale_plan_note", currentHash, currentTitle, currentBody, currentLinks}` — the same shape `plan_update_note` uses. TAKES AN EXPLICIT `plan_id`, same reason as `plan_add_note`. Unknown plan, unknown note, or an already-deleted note → 404. Returns the plan\'s remaining latest notes page.', {
819
- plan_id: z.number().int().positive().describe("The plan id, from `plan_list`."),
820
- note_id: z.number().int().positive().describe("The note id to delete."),
821
- content_hash: z.string().describe("The note's `contentHash` from your last read. Required."),
822
- }, async (args) => jsonResult(await planDeleteNote(client, args)));
823
795
  server.tool("plan_add_card", "Add an existing card to the plan this session is connected to, via POST /api/plans/mine/cards (DX-2683). The card may live on ANY board — that is what a plan is for. Idempotent: re-adding a card already on the plan is a no-op, not an error, and a card may sit in several plans at once. This adds MEMBERSHIP only; it never edits the card. TAKES NO PLAN ID: the plan is resolved from your connected session. Not connected → `{error: \"session_not_connected\"}`. Unknown card → 404. Returns the plan's full member list.", {
824
796
  card_id: z.string().min(1).describe("An existing card id, e.g. `DX-2683`."),
825
797
  }, async (args) => jsonResult(await planAddCard(client, args)));
@@ -909,25 +881,16 @@ async function main() {
909
881
  // symlink-aware (DX-1647) so it holds under the symlinked `npx` bin the worker
910
882
  // spawns, not just a direct `node dist/index.js`.
911
883
  //
912
- // `bridge` and `plan-state` are the two subcommands. `bridge` is the session
913
- // event stream client the danxbot plugin's plan event bridge runs. It never
914
- // boots the MCP server; its only input is `CLAUDE_CODE_SESSION_ID` (read from
915
- // its environment) plus `--resume-ids` — the dashboard URL, credential source,
916
- // and credential itself come from the session-connection record this server's
917
- // own `plan_connect` wrote (`session-connection.ts`), never from the bridge's
918
- // own environment (DX-2862: a bridge trusting its own ambient
919
- // `DANXBOT_DISPATCH_TOKEN` is the exact bug this repo fixed).
920
- //
921
- // `plan-state` (DX-2921) is the ONE-SHOT client of
922
- // `GET /api/plan-sessions/me/turn-state`, run by the DX-2888 plugin's
923
- // turn-start / turn-end hooks — see `plan-state.ts`'s own doc comment for the
924
- // full contract. It ALWAYS exits 0 (success or failure alike: the hook must
925
- // stay silent), so even an unexpected rejection here is caught and reported
926
- // the same `{ok:false, reason}` way rather than propagating a non-zero exit
927
- // or an uncaught stack trace into the hook.
928
- //
929
- // Any OTHER argument is refused rather than ignored, so a typo cannot
930
- // silently start an MCP server on stdio where a subcommand was meant to run.
884
+ // `bridge` is the one subcommand: the session event stream client the danxbot
885
+ // plugin's plan event bridge runs. It never boots the MCP server; its only
886
+ // input is `CLAUDE_CODE_SESSION_ID` (read from its environment) plus
887
+ // `--resume-ids` — the dashboard URL, credential source, and credential itself
888
+ // come from the session-connection record this server's own `plan_connect`
889
+ // wrote (`session-connection.ts`), never from the bridge's own environment
890
+ // (DX-2862: a bridge trusting its own ambient `DANXBOT_DISPATCH_TOKEN` is the
891
+ // exact bug this repo fixed). Any OTHER argument is refused rather than
892
+ // ignored, so a typo cannot silently start an MCP server on stdio where the
893
+ // bridge was meant to run.
931
894
  if (isEntrypointModule(import.meta.url, process.argv[1])) {
932
895
  const [subcommand, ...rest] = process.argv.slice(2);
933
896
  if (subcommand === BRIDGE_SUBCOMMAND) {
@@ -936,16 +899,8 @@ if (isEntrypointModule(import.meta.url, process.argv[1])) {
936
899
  process.exit(1);
937
900
  });
938
901
  }
939
- else if (subcommand === PLAN_STATE_SUBCOMMAND) {
940
- runPlanStateCommand(rest).then((code) => process.exit(code), (err) => {
941
- console.error(`[danx-dashboard-mcp] plan-state fatal: ${err.message}`);
942
- process.stdout.write(`${JSON.stringify({ ok: false, reason: "fatal" })}\n`);
943
- process.exit(0);
944
- });
945
- }
946
902
  else if (subcommand !== undefined) {
947
- console.error(`[danx-dashboard-mcp] unknown subcommand "${subcommand}" (the only ones are ` +
948
- `"${BRIDGE_SUBCOMMAND}" and "${PLAN_STATE_SUBCOMMAND}")`);
903
+ console.error(`[danx-dashboard-mcp] unknown subcommand "${subcommand}" (the only one is "${BRIDGE_SUBCOMMAND}")`);
949
904
  process.exit(2);
950
905
  }
951
906
  else {