@thehammer/danx-dashboard-mcp 0.1.204 → 0.1.206
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/index.js +1 -1
- package/dist/restart-notice.js +33 -32
- package/dist/session-connection.js +23 -13
- package/dist/session-stats.js +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -463,7 +463,7 @@ strictTool("attach_file", "Attach a LOCAL file to a card: `card_id` + `path` (an
|
|
|
463
463
|
// no existing plan for an id to name yet.
|
|
464
464
|
strictTool("plan_connect",
|
|
465
465
|
// DX-2735: trimmed with the problem tools to stay inside the work-profile budget.
|
|
466
|
-
"Connect THIS session to a plan. ONE CALL IS ENOUGH TO START: the reply carries `{session, movedFrom, browserInstruction, briefing, listenerHealth, missedEvents}` — `missedEvents` is `{events, omitted}`: the operator events on this plan that no session had been told about before you connected, oldest first,
|
|
466
|
+
"Connect THIS session to a plan. ONE CALL IS ENOUGH TO START: the reply carries `{session, movedFrom, browserInstruction, briefing, listenerHealth, missedEvents}` — `missedEvents` is `{events, omitted}`: the operator events on this plan that no session had been told about before you connected, oldest first, capped to the newest few (`omitted` counts the older ones left out) — read them before anything else. `browserInstruction` is a server-built action to take NOW: it names the plan's URL and tells you to open it (in-app browser if you have one, else default), keep that tab open for the whole session without navigating it away, and use a different tab for your own browsing — the operator's tab for following and talking to you. Returned on every connect, including a re-connect, so it doubles as the post-context-loss reminder. `briefing` is every goal/rule/caveat (ref+body), every architecture section (id+title), the plan's own ref/name/status, a first page of open cards (id/type/status/title/openProblemCount/assignedAgent) with a `morePagesHint` when more exist, and the closed-card count — usually replacing the plan read + card batch-read a fresh session used to need. A session is on AT MOST ONE plan: connecting elsewhere MOVES it, and `movedFrom: {id, name} | null` says which plan it left (null = no plan, or already this one). Binds only your OWN session, resolved from the session id this server forwards; afterwards a `danxbot_api` call to a `/api/plans/mine/...` route (there is no dedicated plan-write tool) acts on this plan and takes no plan id. `listenerHealth` reports the event bridge's state — `null` (no session), or `{attached, state: \"unattached\"|\"credential_mismatch\"|\"healthy\", nextStep}` naming a concrete fix per unhealthy state (same shape the plan list/get routes report). Once healthy, every comment, answer, problem added and block/unblock on this plan's cards reaches the session unpolled, relayed by the plugin's event bridge as a line like `[DX-8 \"Title\" repo:board] newms87 answered \"<statement>\": chose \"Pause E2E\"`. An action the operator DECLINES arrives as `... rejected the action \"<title>\" for \"<statement>\" — reason: \"<note>\". Do not perform it; re-plan from the reason` (a decision with `outcome: \"rejected\"` — never treat it as work done). Pass `title` (call `get_session({session_id:\"self\"})` first and forward its `title` verbatim) so the dashboard shows the same name Claude does — this server cannot read it itself. The response's `reminders[]` (when present) carries the mantra — read and follow it.", {
|
|
467
467
|
plan_id: z.number().int().positive().describe("The plan id, from the plans list (`danxbot_api` `GET /api/plans`)."),
|
|
468
468
|
title: z
|
|
469
469
|
.string()
|
package/dist/restart-notice.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* working was unattended: six operator answers sat unread for an hour on
|
|
10
10
|
* 2026-10-01. The session's own connection record cannot help (it is the new
|
|
11
11
|
* session's, and does not exist yet); the PREVIOUS session's record can, found by
|
|
12
|
-
* the project key `plan_connect` wrote into it (`
|
|
12
|
+
* the project key `plan_connect` wrote into it (`findPredecessorCandidates`).
|
|
13
13
|
*
|
|
14
14
|
* THE CONTRACT, which `event-hook.sh` reads by exit code:
|
|
15
15
|
* - exit 0, text on stdout: say this to the agent (the registry's
|
|
@@ -26,9 +26,15 @@
|
|
|
26
26
|
* connected it. READ-ONLY: it advances no cursor on the server.
|
|
27
27
|
*/
|
|
28
28
|
import { SESSION_ID_HEADER, resolveBridgeOptions } from "./bridge.js";
|
|
29
|
-
import {
|
|
29
|
+
import { findPredecessorCandidates, projectKeyOf } from "./session-connection.js";
|
|
30
30
|
export const RESTART_NOTICE_SUBCOMMAND = "restart-notice";
|
|
31
31
|
export const RESTART_NOTICE_REQUEST_TIMEOUT_MS = 5_000;
|
|
32
|
+
/**
|
|
33
|
+
* How many earlier sessions of the project one run asks about, newest first. Many
|
|
34
|
+
* sessions share one checkout and the dashboard answers "nothing" for a LIVE sibling
|
|
35
|
+
* (it is not a predecessor), so the newest candidate is often not the one.
|
|
36
|
+
*/
|
|
37
|
+
export const RESTART_NOTICE_MAX_CANDIDATES = 10;
|
|
32
38
|
/** The registry row whose text is the notice (`src/issues/reminders/seed.yaml`). */
|
|
33
39
|
export const RESTART_NOTICE_REMINDER_KEY = "plan_restart.waiting_events";
|
|
34
40
|
const USAGE = `usage: CLAUDE_CODE_SESSION_ID=<session-id> danx-dashboard-mcp ${RESTART_NOTICE_SUBCOMMAND} [--cwd <project directory>]`;
|
|
@@ -105,36 +111,31 @@ resolveFrom = {}) {
|
|
|
105
111
|
process.stderr.write(`no_session_id: CLAUDE_CODE_SESSION_ID is not set\n${USAGE}\n`);
|
|
106
112
|
return 1;
|
|
107
113
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
if (!output.ok) {
|
|
134
|
-
process.stderr.write(`${output.reason}: could not ask the dashboard what the previous session of this project was working\n`);
|
|
135
|
-
return 1;
|
|
114
|
+
const candidates = findPredecessorCandidates(projectKeyOf(cwd), sessionId, resolveFrom).slice(0, RESTART_NOTICE_MAX_CANDIDATES);
|
|
115
|
+
for (const predecessor of candidates) {
|
|
116
|
+
let options;
|
|
117
|
+
try {
|
|
118
|
+
options = resolveBridgeOptions({ sessionId: predecessor.sessionId, resumeIds: [] }, env, resolveFrom);
|
|
119
|
+
}
|
|
120
|
+
catch (err) {
|
|
121
|
+
const start = err;
|
|
122
|
+
process.stderr.write(`${start.reason}: ${start.message}. Fix: ${start.fix}.\n`);
|
|
123
|
+
return 1;
|
|
124
|
+
}
|
|
125
|
+
const output = await fetchRestartNotice({
|
|
126
|
+
dashboardUrl: options.dashboardUrl,
|
|
127
|
+
token: options.token,
|
|
128
|
+
predecessorId: predecessor.sessionId,
|
|
129
|
+
askingSessionId: sessionId,
|
|
130
|
+
}, { fetch: (input, init) => fetch(input, init), requestTimeoutMs: RESTART_NOTICE_REQUEST_TIMEOUT_MS });
|
|
131
|
+
if (!output.ok) {
|
|
132
|
+
process.stderr.write(`${output.reason}: could not ask the dashboard what the previous session of this project was working\n`);
|
|
133
|
+
return 1;
|
|
134
|
+
}
|
|
135
|
+
if (output.text !== null) {
|
|
136
|
+
process.stdout.write(output.text);
|
|
137
|
+
return 0;
|
|
138
|
+
}
|
|
136
139
|
}
|
|
137
|
-
if (output.text !== null)
|
|
138
|
-
process.stdout.write(output.text);
|
|
139
140
|
return 0;
|
|
140
141
|
}
|
|
@@ -31,16 +31,25 @@
|
|
|
31
31
|
*/
|
|
32
32
|
import { mkdirSync, readdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
33
33
|
import { homedir } from "node:os";
|
|
34
|
-
import { join, resolve } from "node:path";
|
|
34
|
+
import { join, resolve, win32 } from "node:path";
|
|
35
35
|
/** One canonical shape (CLAUDE.md Core Principle 1) — a record of any other version is refused, never adapted. */
|
|
36
36
|
// DX-3928 — v2 adds `projectKey`, the directory the connecting session ran in, so a
|
|
37
37
|
// restarted session (a NEW session id, no record of its own) can find which plan the
|
|
38
|
-
// previous session of the same project was on (`
|
|
38
|
+
// previous session of the same project was on (`findPredecessorCandidates`).
|
|
39
39
|
export const SESSION_CONNECTION_SCHEMA_VERSION = 2;
|
|
40
40
|
export const STALE_RECORD_MS = 7 * 24 * 60 * 60 * 1000;
|
|
41
|
-
/**
|
|
41
|
+
/**
|
|
42
|
+
* DX-3928 — the one definition of "the same project": the resolved working
|
|
43
|
+
* directory, normalised so the SAME directory reached two ways compares equal.
|
|
44
|
+
* A Windows path is written with forward slashes and a lower-case drive letter
|
|
45
|
+
* (`C:\Work\p` and `c:/Work/p` are one key), because the MCP server and the
|
|
46
|
+
* plugin's hook can each report the cwd in either spelling.
|
|
47
|
+
*/
|
|
42
48
|
export function projectKeyOf(cwd) {
|
|
43
|
-
|
|
49
|
+
if (!/^[A-Za-z]:[\\/]/.test(cwd))
|
|
50
|
+
return resolve(cwd);
|
|
51
|
+
const resolved = win32.resolve(cwd).replace(/\\/g, "/");
|
|
52
|
+
return resolved[0].toLowerCase() + resolved.slice(1);
|
|
44
53
|
}
|
|
45
54
|
export function sessionConnectionDir(home = homedir()) {
|
|
46
55
|
return join(home, ".config", "danxbot", "plan-sessions");
|
|
@@ -184,16 +193,18 @@ export function invalidRecordReason(value) {
|
|
|
184
193
|
return `source.kind is ${JSON.stringify(source.kind)}, which this version does not know`;
|
|
185
194
|
}
|
|
186
195
|
/**
|
|
187
|
-
* DX-3928 — the
|
|
188
|
-
* `currentSessionId
|
|
189
|
-
*
|
|
196
|
+
* DX-3928 — the records of the sessions that ran in `projectKey` before
|
|
197
|
+
* `currentSessionId`, NEWEST FIRST: other sessions' records for that project,
|
|
198
|
+
* within `STALE_RECORD_MS`. Which of them is a real predecessor (not a live
|
|
199
|
+
* sibling in the same checkout) is the dashboard's call, not this reader's, so
|
|
200
|
+
* all candidates are returned; the caller asks about them in order.
|
|
190
201
|
*
|
|
191
202
|
* A record that cannot be understood (another schema version, not JSON) is
|
|
192
203
|
* SKIPPED here and nowhere else: this answers "was a plan being worked in this
|
|
193
204
|
* project", and a record from an older server cannot say. Every consumer of a
|
|
194
205
|
* session's OWN record (`readSessionConnection`) still fails loud.
|
|
195
206
|
*/
|
|
196
|
-
export function
|
|
207
|
+
export function findPredecessorCandidates(projectKey, currentSessionId, options = {}) {
|
|
197
208
|
const dir = sessionConnectionDir(options.home ?? homedir());
|
|
198
209
|
const now = options.now ?? Date.now();
|
|
199
210
|
let names;
|
|
@@ -201,9 +212,9 @@ export function findPredecessorConnection(projectKey, currentSessionId, options
|
|
|
201
212
|
names = readdirSync(dir);
|
|
202
213
|
}
|
|
203
214
|
catch {
|
|
204
|
-
return
|
|
215
|
+
return []; // nothing written yet
|
|
205
216
|
}
|
|
206
|
-
|
|
217
|
+
const found = [];
|
|
207
218
|
for (const name of names) {
|
|
208
219
|
if (!name.endsWith(".json"))
|
|
209
220
|
continue;
|
|
@@ -222,8 +233,7 @@ export function findPredecessorConnection(projectKey, currentSessionId, options
|
|
|
222
233
|
const connectedAt = Date.parse(record.connectedAt);
|
|
223
234
|
if (!Number.isFinite(connectedAt) || now - connectedAt > STALE_RECORD_MS)
|
|
224
235
|
continue;
|
|
225
|
-
|
|
226
|
-
newest = record;
|
|
236
|
+
found.push({ record, connectedAt });
|
|
227
237
|
}
|
|
228
|
-
return
|
|
238
|
+
return found.sort((x, y) => y.connectedAt - x.connectedAt).map((f) => f.record);
|
|
229
239
|
}
|
package/dist/session-stats.js
CHANGED
|
@@ -143,7 +143,7 @@ export class OperatorSessionStats {
|
|
|
143
143
|
// Replays are credited (the ledger is idempotent) and marked so the
|
|
144
144
|
// collector's own estimate never counts them twice; the bridge has no
|
|
145
145
|
// side-effecting transcript subscriber to skip them for.
|
|
146
|
-
watcher.
|
|
146
|
+
watcher.onUsageLine((raw, meta) => this.usage.observeRawLine(raw, meta));
|
|
147
147
|
watcher.onEntry(activities.onEntry);
|
|
148
148
|
this.watcher = watcher;
|
|
149
149
|
void watcher
|
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.206",
|
|
4
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",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"@modelcontextprotocol/sdk": "1.29.0",
|
|
34
|
-
"@thehammer/danx-session-telemetry": "0.1.
|
|
34
|
+
"@thehammer/danx-session-telemetry": "0.1.23",
|
|
35
35
|
"zod": "^3.25.76"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|