@thehammer/danx-dashboard-mcp 0.1.150 → 0.1.154
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/handlers.js +16 -10
- package/dist/index.js +8 -25
- package/package.json +1 -1
- package/dist/entrypoint.test.js +0 -43
package/README.md
CHANGED
|
@@ -21,7 +21,7 @@ All exposed as `mcp__danx-dashboard__<name>` once wired through the workspace `m
|
|
|
21
21
|
|
|
22
22
|
| Tool | HTTP | Notes |
|
|
23
23
|
|---|---|---|
|
|
24
|
-
| `issue_list` | `GET /api/issues` | filters: `type`, `parent_id` (null → root-only), `dispatchable_derived`, `assigned_agent`, `include_closed`, `limit`, `offset` |
|
|
24
|
+
| `issue_list` | `GET /api/issues` | filters: `type`, `parent_id` (null → root-only), `dispatchable_derived`, `assigned_agent`, `include_closed`, `limit`, `offset`. `fields` (DX-3427) is the SAME recursive field tree `issue_get` takes, against the same issue resource — list-row fields (`ac_total`, `comments_count`, `waiting_on`, `quality_gates`, `next_pre_gate`, …) sit alongside every detail field. See `resource_fields` |
|
|
25
25
|
| `issue_get` | `GET /api/issues/:id` or `GET /api/issues/batch` | Pass `id` for one card, or `ids[]` (DX-2727, at most 100) to resolve many across boards in ONE call — global, so `ids` with `board` throws; per-id `not_found` rather than a whole-call 404. Minimal scalars by default; `fields` (DX-3426) opts in via a recursive field TREE — `{"description": true, "problems": {"solutions": true}, "comments": {"limit": 10}}` — not a flat group list; a cursor-paged relation (today only `comments`) takes its page args (`limit`/`before`) inside its own nested object, so paging now works on the batch form too. See `resource_fields` |
|
|
26
26
|
| `resource_fields` | `GET /api/resources/:resource/fields` | DX-3426 — what an `issue_get`-style field tree may name for one resource: `{resource, description, always, hashes, fields, relations}`. Install-global, no `board`. `resource: "issue"` is the root; a relation's own `resource` in the response is what to call this again with, one level deeper |
|
|
27
27
|
| `issue_create` | `POST /api/issues` | Epic REQUIRES non-empty `phase_children[]` (atomic insert). `title` = short domain-naming label; `summary` = 1–3 plain-language sentences, always shown; `description` = the collapsed "Context" body. Root and every phase child take their own `summary` |
|
package/dist/handlers.js
CHANGED
|
@@ -25,19 +25,21 @@ import { readFile } from "node:fs/promises";
|
|
|
25
25
|
import { basename, extname, isAbsolute } from "node:path";
|
|
26
26
|
import { resolvePriority } from "./priority.js";
|
|
27
27
|
/**
|
|
28
|
-
* `filter`/`fields`/`sort` are JSON
|
|
29
|
-
*
|
|
30
|
-
* `src/issues/read/reader.ts#parseEnvelope`); each is
|
|
31
|
-
* empty so the wire carries no `{}`/`[]` noise. `board`
|
|
32
|
-
* top-level param, resolved by the HTTP client exactly as
|
|
28
|
+
* `filter`/`fields`/`sort` are JSON-encoded onto the query string (the route
|
|
29
|
+
* parses `?filter=<JSON>`, `?fields=<JSON>`, `?sort=<JSON>` — see
|
|
30
|
+
* `src/issues/read/reader.ts#parseEnvelope` + `handleGetIssues`); each is
|
|
31
|
+
* omitted entirely when empty so the wire carries no `{}`/`[]` noise. `board`
|
|
32
|
+
* stays a flat top-level param, resolved by the HTTP client exactly as
|
|
33
|
+
* before.
|
|
33
34
|
*/
|
|
34
35
|
export async function issueList(client, args) {
|
|
35
36
|
const query = {};
|
|
36
37
|
if (args.filter !== undefined && Object.keys(args.filter).length > 0) {
|
|
37
38
|
query.filter = JSON.stringify(args.filter);
|
|
38
39
|
}
|
|
39
|
-
|
|
40
|
-
|
|
40
|
+
// DX-3427 — a JSON-encoded field tree, mirroring `issue_get` below.
|
|
41
|
+
if (args.fields !== undefined && Object.keys(args.fields).length > 0) {
|
|
42
|
+
query.fields = JSON.stringify(args.fields);
|
|
41
43
|
}
|
|
42
44
|
if (args.sort !== undefined && args.sort.length > 0) {
|
|
43
45
|
query.sort = JSON.stringify(args.sort);
|
|
@@ -1116,6 +1118,10 @@ export const PLAN_EVENT_KINDS = [
|
|
|
1116
1118
|
"session_connected",
|
|
1117
1119
|
"session_switched_away",
|
|
1118
1120
|
"idle_nudge_sent",
|
|
1121
|
+
// DX-3519 — plan sign-off.
|
|
1122
|
+
"plan_signed_off",
|
|
1123
|
+
"plan_sign_off_cleared",
|
|
1124
|
+
"plan_auto_sign_off_changed",
|
|
1119
1125
|
];
|
|
1120
1126
|
/**
|
|
1121
1127
|
* DX-3027 — every origin `plan_get`'s `events_origin` filter accepts.
|
|
@@ -1125,14 +1131,14 @@ export const PLAN_EVENT_KINDS = [
|
|
|
1125
1131
|
*/
|
|
1126
1132
|
export const PLAN_EVENT_ORIGINS = ["operator", "agent", "machine"];
|
|
1127
1133
|
/**
|
|
1128
|
-
* DX-2834 — the plan-status taxonomy, mirroring `PLAN_FIELD_GROUPS`
|
|
1129
|
-
* above: the ONE copy in this package (the `plan_list` zod enum in
|
|
1134
|
+
* DX-2834 / DX-3519 — the plan-status taxonomy, mirroring `PLAN_FIELD_GROUPS`
|
|
1135
|
+
* just above: the ONE copy in this package (the `plan_list` zod enum in
|
|
1130
1136
|
* `index.ts` reads this const), duplicated from the server's
|
|
1131
1137
|
* `src/issues/db/plans.ts#PLAN_STATUS_IDS` because the published package
|
|
1132
1138
|
* cannot import server source at runtime. `__tests__/handlers.test.ts`
|
|
1133
1139
|
* asserts the two are equal, so drift fails a test rather than a live call.
|
|
1134
1140
|
*/
|
|
1135
|
-
export const PLAN_STATUSES = ["awaiting-session", "planning", "building", "complete"];
|
|
1141
|
+
export const PLAN_STATUSES = ["awaiting-session", "planning", "building", "awaiting-sign-off", "complete"];
|
|
1136
1142
|
/** Every plan, plus which one THIS session is connected to. */
|
|
1137
1143
|
export async function planList(client, args = {}) {
|
|
1138
1144
|
return client.request({
|
package/dist/index.js
CHANGED
|
@@ -277,25 +277,6 @@ const EFFORT_VALUES = [
|
|
|
277
277
|
// still be CREATED through this MCP.
|
|
278
278
|
const ISSUE_TYPES = ["Epic", "Bug", "Feature", "Story", "Chore", "Task"];
|
|
279
279
|
const NON_EPIC_TYPES = ["Bug", "Feature", "Story", "Chore", "Task"];
|
|
280
|
-
// DX-935 / DX-937 — field-group taxonomy for the LIST read envelope,
|
|
281
|
-
// hand-copied from `src/issues/read/field-groups.ts` (`LIST_GROUPS` — this
|
|
282
|
-
// package cannot import server source). Drift surfaces at runtime as a server
|
|
283
|
-
// 400, not silently. DX-3426 moved `issue_get` to the field tree
|
|
284
|
-
// (`field-tree.ts`); DX-3427 moves the list the same way and deletes this.
|
|
285
|
-
const LIST_FIELD_GROUPS = [
|
|
286
|
-
"description",
|
|
287
|
-
// DX-2735: replaced the flat "solutions" group (hard cut, no alias).
|
|
288
|
-
"problems",
|
|
289
|
-
"ac",
|
|
290
|
-
"comments",
|
|
291
|
-
"retro",
|
|
292
|
-
"dependencies",
|
|
293
|
-
"triage",
|
|
294
|
-
"assignment",
|
|
295
|
-
"quality_gates",
|
|
296
|
-
"children",
|
|
297
|
-
"effort",
|
|
298
|
-
];
|
|
299
280
|
const SORT_ORDERS = ["asc", "desc"];
|
|
300
281
|
const sortField = z
|
|
301
282
|
.array(z.object({
|
|
@@ -360,7 +341,7 @@ const MARKDOWN_STYLE_DESCRIBE = "Renders as markdown here. Use `##`/`###` header
|
|
|
360
341
|
strictTool("issue_list",
|
|
361
342
|
// DX-2735: trimmed to pay for the problem tools inside the work-profile
|
|
362
343
|
// injected-surface budget — same facts, no repeated prose.
|
|
363
|
-
"List cards. Board-scoped; see `board`. `filter`: type, parent_id, dispatchable_derived, status_derived[], self_dispatchable_derived, assigned_agent, include_closed, include_deleted, q (free text over id+title+description). THE DEFAULT ROW IS MINIMAL — scalars only (id, type, title, status, parent_id, priority, created_at, updated_at, assigned_agent, content_hash), no joins;
|
|
344
|
+
"List cards. Board-scoped; see `board`. `filter`: type, parent_id, dispatchable_derived, status_derived[], self_dispatchable_derived, assigned_agent, include_closed, include_deleted, q (free text over id+title+description). THE DEFAULT ROW IS MINIMAL — scalars only (id, type, title, status, parent_id, priority, created_at, updated_at, assigned_agent, content_hash), no joins; `fields` opts into more — the SAME recursive field-tree JSON `issue_get` takes, against the SAME issue resource (list-row fields like `ac_total`/`comments_count`/`waiting_on`/`quality_gates`/`next_pre_gate` sit alongside every detail field `issue_get` can name). Call `resource_fields({resource:\"issue\"})` for the full, current list of what `fields` may name — never guess a name; unknown → 400 `unknown_field`. `sort`: [{column, order}] over id|priority|repo_name|title|type|status_derived|triage_ice_total|created_at|updated_at (default order: see `sort`'s own field description). `limit`/`offset` page (uncapped by default). `include_closed` defaults to FALSE: a bare call silently excludes every Done/Cancelled card (leaf AND container alike). The response always carries `total` (the full count matching every filter except limit/offset — compare against `issues.length` to tell an exhausted list from a truncated one) and, whenever `include_closed` was not explicitly `true`, `closed_excluded` (how many additional terminal cards the default withheld — re-call with `include_closed: true` to see them). issue_get reads one card in full.", {
|
|
364
345
|
filter: z
|
|
365
346
|
.object({
|
|
366
347
|
q: z.string().optional(),
|
|
@@ -381,10 +362,12 @@ strictTool("issue_list",
|
|
|
381
362
|
// its own `.strict()`.
|
|
382
363
|
.strict()
|
|
383
364
|
.optional(),
|
|
384
|
-
fields
|
|
385
|
-
|
|
365
|
+
// DX-3427 — a field TREE, exactly like `issue_get`'s `fields` below
|
|
366
|
+
// (against the same shared issue resource) — replaces the retired
|
|
367
|
+
// CSV field-GROUP list.
|
|
368
|
+
fields: fieldTreeSchema
|
|
386
369
|
.optional()
|
|
387
|
-
.describe("
|
|
370
|
+
.describe("A field tree (see tool description); absent/empty = minimal scalars. `resource_fields({resource:\"issue\"})` names every valid key."),
|
|
388
371
|
sort: sortField,
|
|
389
372
|
limit: z.number().int().positive().max(LIST_PAGE_MAX_LIMIT).optional(),
|
|
390
373
|
offset: z.number().int().nonnegative().optional(),
|
|
@@ -941,11 +924,11 @@ strictTool("brief_set_page", 'Write one Brief page. Board-scoped; see `board`. B
|
|
|
941
924
|
// plan id — and it can only ever bind the caller's own session. `plan_create`
|
|
942
925
|
// also takes no plan id, but for a different reason: it MAKES a plan rather
|
|
943
926
|
// than acting on one, so there is no existing plan for an id to name yet.
|
|
944
|
-
strictTool("plan_list", "List every plan, and learn which plan THIS session is connected to. Plans are GLOBAL, not board-scoped — a plan is a named, dated set of cards an operator assembled by hand, from any repo. Returns `{plans: [{id, ref, name, createdAt, cardCount, boards, status}], session, sessionListenerAttached}` — `ref` is the plan's short reference (`PLN-<id>`), cite it rather than the bare id. `status` is computed fresh on every read, never stored: `awaiting-session` (no live session — a session row isn't released just because a session ended), `planning` (no cards, or only Review/Backlog/terminal ones with ≥1 not Done/Cancelled), `building` (a live session AND ≥1 card ToDo/In Progress or stuck-but-active — Blocked/Needs Help), `complete` (≥1 card, all Done/Cancelled — wins even with no session). Pass `status` to filter. `session` is your own registration (`{sessionId, title, planId, planName, firstSeenAt, lastActiveAt}`) or null outside a Claude Code session; `planId: null` means connected to no plan — `plan_get` to browse, `plan_connect` to bind. `sessionListenerAttached` says whether your event stream is attached (the plugin's plan bridge); false for a few seconds right after connect is normal, false after that means events aren't reaching you — tell the operator. Distinct from the board Brief (`brief_list`).", {
|
|
927
|
+
strictTool("plan_list", "List every plan, and learn which plan THIS session is connected to. Plans are GLOBAL, not board-scoped — a plan is a named, dated set of cards an operator assembled by hand, from any repo. Returns `{plans: [{id, ref, name, createdAt, cardCount, boards, status, signedOffAt, signedOffBy, autoSignOff}], session, sessionListenerAttached}` — `ref` is the plan's short reference (`PLN-<id>`), cite it rather than the bare id. `status` is computed fresh on every read, never stored — except the sign-off pair itself: `awaiting-session` (no live session — a session row isn't released just because a session ended), `planning` (no cards, or only Review/Backlog/terminal ones with ≥1 not Done/Cancelled), `building` (a live session AND ≥1 card ToDo/In Progress or stuck-but-active — Blocked/Needs Help), `awaiting-sign-off` (≥1 card, all Done/Cancelled, but nobody has signed the plan off yet — DX-3519), `complete` (≥1 card, all Done/Cancelled, AND signed off — wins even with no session). `signedOffAt`/`signedOffBy` are `null` until a human (`signedOffBy` = their identity) or the `autoSignOff` toggle (`signedOffBy: 'auto'`) signs the plan off; sign-off itself is a human-only write, not exposed through this MCP surface — direct the operator to the dashboard's plan page. Pass `status` to filter. `session` is your own registration (`{sessionId, title, planId, planName, firstSeenAt, lastActiveAt}`) or null outside a Claude Code session; `planId: null` means connected to no plan — `plan_get` to browse, `plan_connect` to bind. `sessionListenerAttached` says whether your event stream is attached (the plugin's plan bridge); false for a few seconds right after connect is normal, false after that means events aren't reaching you — tell the operator. Distinct from the board Brief (`brief_list`).", {
|
|
945
928
|
status: z
|
|
946
929
|
.enum(PLAN_STATUSES)
|
|
947
930
|
.optional()
|
|
948
|
-
.describe("Filter to one computed status: awaiting-session, planning, building, complete. Omit for every plan."),
|
|
931
|
+
.describe("Filter to one computed status: awaiting-session, planning, building, awaiting-sign-off, complete. Omit for every plan."),
|
|
949
932
|
}, async (args) => jsonResult(await planList(client, args)));
|
|
950
933
|
strictTool("plan_get", "Read a plan. Pass `plan_id` to read ANY plan; OMIT to read the plan this session is connected to — omitting while connected to none fails loud `{error: \"session_not_connected\"}`, connect first. A BARE call (no `fields`) returns ONLY the plan's cheap scalars: `{plan, boards, cardCount, bucketCounts, status, session, sessionListenerAttached, available_field_groups}` — no cards, records, or architecture body. `plan` carries `{id, ref, name, createdAt}`; `ref` is the plan's short reference (`PLN-<id>`) — cite that, not the bare id. `status` is computed fresh on every read, never stored — see `plan_list` for the four values and what each means. Pass `fields` to opt into: `cards` (member cards, paged via `cards_offset`/`cards_limit` — see their own field descriptions — in stable card-reference order: board prefix then card number, pages never repeat/skip unless membership changes between reads; response carries `cards_total`), `records` (every goal+rule+caveat, keyed by kind) or `records:goal` / `records:rule` / `records:caveat` (one kind, cheaper), `architecture` (`{sections: [{id, planId, contentHash, title, content, sortOrder, createdAt, updatedAt}]}`), `sessions` (every session connected to the plan), `events` (the plan's durable event ledger — every human action and bridge message, paged/filtered via `events_limit`/`events_before`/`events_kinds`/`events_origin`/`events_writer` — see their own field descriptions; response carries `events: {items: [{id, at, kind, writer, origin, originSessionId, targetSessionId, cardId, cardTitle, boardId, detail}], next_cursor}`, `next_cursor` null on the last page; an event on a card whose board you cannot read is left out, plan-level events are always visible). `session`/`sessionListenerAttached` and `available_field_groups` ride every response regardless. `sessionListenerAttached` is `false` for a few seconds right after `plan_connect` while the plugin's event bridge starts; still `false` after that while connected means the plan's card events are not reaching you — tell the operator. ALWAYS `plan_get`/`plan_get_architecture_section` immediately before `plan_update_architecture_section`/`plan_delete_architecture_section` and pass the returned `contentHash` back as `base_hash`.", {
|
|
951
934
|
plan_id: z
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thehammer/danx-dashboard-mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.154",
|
|
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",
|
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
|
-
});
|