@thehammer/danx-dashboard-mcp 0.1.304 → 0.1.305
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/compaction-control.js +145 -0
- package/dist/index.js +2 -0
- package/dist/listen.js +20 -0
- package/dist/plan-events-relay.js +51 -1
- package/dist/session-connection.js +5 -2
- package/dist/urgent-records.js +5 -2
- package/package.json +1 -1
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DX-4993 (PLAN-42 rule R-4) - the compaction control of an OPERATOR session, as this package relays it.
|
|
3
|
+
*
|
|
4
|
+
* 50 Arms decides when a session is asked to prepare for a compaction and when its agent has said ready (the dashboard's `compaction-handshake/`). The
|
|
5
|
+
* session's stream carries the decision as a `compaction` frame (`event: compaction`, data `{"control": <control> | null}`, level-triggered: the current
|
|
6
|
+
* control on connect and whenever it changes). The relay does two things with each frame:
|
|
7
|
+
*
|
|
8
|
+
* - the REQUEST (`control.ask`) reaches the agent as an urgent record, once per request (`ask.at` names it), so it arrives inside a running turn;
|
|
9
|
+
* - the rest is written to ONE local file the danxbot plugin's `PreCompact` gate reads: `~/.config/danxbot/compaction/<sessionId>.json`. The gate never
|
|
10
|
+
* asks the network: Claude Code proposes a compaction before every model request.
|
|
11
|
+
*
|
|
12
|
+
* The file is `{v: 1, sessionId, control: {floorTokens, passAtTokens, windowTokens, releaseUntilMs, releaseId} | null, askedAt: string | null}`. `sessionId` is the
|
|
13
|
+
* session the file was written for (the plugin refuses a file that names another); `windowTokens` is the context window the dashboard assumed for the session's
|
|
14
|
+
* model line (the plugin does not arm a session whose real window is smaller). `control: null` says the session is not under the handshake (the plugin disarms). `askedAt` is the relay's own memory of the last request it showed the agent, kept in the file so a restart
|
|
15
|
+
* of this server does not show it again; the plugin ignores it.
|
|
16
|
+
*
|
|
17
|
+
* The shapes below mirror the dashboard's `OperatorCompactionControl` (`src/dashboard/compaction-handshake/operator-session.ts`) and the plugin's gate
|
|
18
|
+
* (`danxbot/hooks/compaction/control.ts` in the plugin repo): this published package cannot import either. Each side tolerates the other's previous
|
|
19
|
+
* version by ignoring a frame or a file key it does not know and refusing nothing else (see the release order on DX-4993).
|
|
20
|
+
*/
|
|
21
|
+
import { mkdirSync, readFileSync, renameSync, utimesSync, writeFileSync } from "node:fs";
|
|
22
|
+
import { homedir } from "node:os";
|
|
23
|
+
import { dirname, join } from "node:path";
|
|
24
|
+
import { prefixedLine } from "./one-line.js";
|
|
25
|
+
import { assertSessionIdSegment, pruneStaleSessionFiles } from "./session-connection.js";
|
|
26
|
+
/** The longest request line relayed whole (the pacing line's bound): the dashboard's request is a few hundred characters. */
|
|
27
|
+
export const COMPACTION_ASK_MAX_CODE_POINTS = 1000;
|
|
28
|
+
const isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
29
|
+
const isCount = (v) => typeof v === "number" && Number.isInteger(v) && v >= 0;
|
|
30
|
+
/** Why a `compaction` frame's data cannot be read, or `null` when it can. */
|
|
31
|
+
export function invalidCompactionReason(value) {
|
|
32
|
+
if (!isRecord(value) || !("control" in value))
|
|
33
|
+
return "the frame has no control";
|
|
34
|
+
const c = value.control;
|
|
35
|
+
if (c === null)
|
|
36
|
+
return null;
|
|
37
|
+
if (!isRecord(c))
|
|
38
|
+
return "control is not an object or null";
|
|
39
|
+
if (c.v !== 1)
|
|
40
|
+
return `control version ${JSON.stringify(c.v)} is not 1`;
|
|
41
|
+
if (!isCount(c.floorTokens) || !isCount(c.passAtTokens) || !isCount(c.windowTokens))
|
|
42
|
+
return "floorTokens, passAtTokens and windowTokens must be whole numbers";
|
|
43
|
+
// DX-4993: the plugin refuses a control whose valve is not above its floor, and the dashboard never sends one; a frame that would become such a file is a fault line
|
|
44
|
+
if (c.passAtTokens <= c.floorTokens)
|
|
45
|
+
return "passAtTokens must be above floorTokens";
|
|
46
|
+
if (c.windowTokens <= c.passAtTokens)
|
|
47
|
+
return "windowTokens must be above passAtTokens";
|
|
48
|
+
if (c.ask !== null) {
|
|
49
|
+
if (!isRecord(c.ask) || !isCount(c.ask.seq) || typeof c.ask.at !== "string" || c.ask.at === "" || typeof c.ask.text !== "string" || c.ask.text.trim() === "") {
|
|
50
|
+
return "ask must be null or {seq, at, text}";
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if ((c.releaseUntilMs === null) !== (c.releaseId === null))
|
|
54
|
+
return "releaseUntilMs and releaseId are both set or both null";
|
|
55
|
+
if (c.releaseUntilMs !== null && !(typeof c.releaseUntilMs === "number" && Number.isFinite(c.releaseUntilMs)))
|
|
56
|
+
return "releaseUntilMs must be a number or null";
|
|
57
|
+
if (c.releaseId !== null && (typeof c.releaseId !== "string" || c.releaseId === ""))
|
|
58
|
+
return "releaseId must be a string or null";
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
/** The control of a valid frame's data (`invalidCompactionReason` is `null`). */
|
|
62
|
+
export function controlOfFrame(value) {
|
|
63
|
+
const reason = invalidCompactionReason(value);
|
|
64
|
+
if (reason !== null)
|
|
65
|
+
throw new Error(`malformed compaction frame: ${reason}`);
|
|
66
|
+
return value.control;
|
|
67
|
+
}
|
|
68
|
+
/** The line the agent reads for a request: the dashboard's text on one line, prefixed like every stream line. */
|
|
69
|
+
export function formatCompactionAskLine(ask) {
|
|
70
|
+
return prefixedLine(ask.text, COMPACTION_ASK_MAX_CODE_POINTS);
|
|
71
|
+
}
|
|
72
|
+
/** The file of session `sessionId` for `control` (`null`: not under the handshake) after the request `askedAt` was shown last. */
|
|
73
|
+
export function fileOf(control, askedAt, sessionId) {
|
|
74
|
+
return {
|
|
75
|
+
v: 1,
|
|
76
|
+
sessionId,
|
|
77
|
+
control: control === null
|
|
78
|
+
? null
|
|
79
|
+
: { floorTokens: control.floorTokens, passAtTokens: control.passAtTokens, windowTokens: control.windowTokens, releaseUntilMs: control.releaseUntilMs, releaseId: control.releaseId },
|
|
80
|
+
askedAt,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
export function compactionDir(home = homedir()) {
|
|
84
|
+
return join(home, ".config", "danxbot", "compaction");
|
|
85
|
+
}
|
|
86
|
+
/** `~/.config/danxbot/compaction/<sessionId>.json`, next to the plugin's other per-session records. */
|
|
87
|
+
export function compactionFilePath(sessionId, home = homedir()) {
|
|
88
|
+
assertSessionIdSegment(sessionId);
|
|
89
|
+
return join(compactionDir(home), `${sessionId}.json`);
|
|
90
|
+
}
|
|
91
|
+
export function parseCompactionFile(text, where) {
|
|
92
|
+
const parsed = JSON.parse(text);
|
|
93
|
+
if (!isRecord(parsed) || parsed.v !== 1 || typeof parsed.sessionId !== "string" || !("control" in parsed) || (parsed.askedAt !== null && typeof parsed.askedAt !== "string")) {
|
|
94
|
+
throw new Error(`${where} is not a compaction file`);
|
|
95
|
+
}
|
|
96
|
+
const reason = parsed.control === null ? null : invalidCompactionReason({ control: { ...parsed.control, v: 1, ask: null } });
|
|
97
|
+
if (reason !== null)
|
|
98
|
+
throw new Error(`${where}: ${reason}`);
|
|
99
|
+
return parsed;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* The real store, under `home`. Every write also removes the files nothing has touched for a week (`pruneStaleSessionFiles`, like the session's connection
|
|
103
|
+
* record and key file). A live session's file is never among them: its relay touches the file at every heartbeat (`touch`), so only the files of sessions whose
|
|
104
|
+
* relay has been gone for a week are removed.
|
|
105
|
+
*/
|
|
106
|
+
export function compactionFilesIn(home = homedir(), now = Date.now) {
|
|
107
|
+
return {
|
|
108
|
+
read(sessionId) {
|
|
109
|
+
const path = compactionFilePath(sessionId, home);
|
|
110
|
+
let text;
|
|
111
|
+
try {
|
|
112
|
+
text = readFileSync(path, "utf-8");
|
|
113
|
+
}
|
|
114
|
+
catch (err) {
|
|
115
|
+
if (err.code === "ENOENT")
|
|
116
|
+
return null;
|
|
117
|
+
throw err;
|
|
118
|
+
}
|
|
119
|
+
const file = parseCompactionFile(text, path);
|
|
120
|
+
if (file.sessionId !== sessionId)
|
|
121
|
+
throw new Error(`${path} was written for session ${JSON.stringify(file.sessionId)}`);
|
|
122
|
+
return file;
|
|
123
|
+
},
|
|
124
|
+
write(sessionId, file) {
|
|
125
|
+
const path = compactionFilePath(sessionId, home);
|
|
126
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
127
|
+
pruneStaleSessionFiles(dirname(path), now());
|
|
128
|
+
// A sibling then a rename: the plugin's gate reads this file on every compaction proposal and must never see half of one.
|
|
129
|
+
const temp = `${path}.${process.pid}.tmp`;
|
|
130
|
+
writeFileSync(temp, JSON.stringify(file), "utf-8");
|
|
131
|
+
renameSync(temp, path);
|
|
132
|
+
},
|
|
133
|
+
touch(sessionId) {
|
|
134
|
+
const at = new Date(now());
|
|
135
|
+
try {
|
|
136
|
+
utimesSync(compactionFilePath(sessionId, home), at, at);
|
|
137
|
+
}
|
|
138
|
+
catch (err) {
|
|
139
|
+
// a session with no file has nothing to keep; any other failure is the caller's to report
|
|
140
|
+
if (err.code !== "ENOENT")
|
|
141
|
+
throw err;
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -82,6 +82,7 @@
|
|
|
82
82
|
* 5xx and network failures throw — never silently swallowed.
|
|
83
83
|
*/
|
|
84
84
|
import { isEntrypointModule } from "./entrypoint.js";
|
|
85
|
+
import { compactionFilesIn } from "./compaction-control.js";
|
|
85
86
|
import { PlanEventRelay, WAIT_MAX_MS, WAIT_MIN_MS } from "./plan-events-relay.js";
|
|
86
87
|
import { READ_IDLE_TIMEOUT_MS } from "./listen.js";
|
|
87
88
|
import { REQUEST_TIMEOUT_MS, realScheduleInterval, realSleep } from "./session-stream.js";
|
|
@@ -256,6 +257,7 @@ function boot() {
|
|
|
256
257
|
readIdleTimeoutMs: READ_IDLE_TIMEOUT_MS,
|
|
257
258
|
requestTimeoutMs: REQUEST_TIMEOUT_MS,
|
|
258
259
|
scheduleInterval: realScheduleInterval,
|
|
260
|
+
compactionFiles: compactionFilesIn(),
|
|
259
261
|
});
|
|
260
262
|
permissionWatch = new PermissionWatch({
|
|
261
263
|
dashboardUrl,
|
package/dist/listen.js
CHANGED
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
* (`src/issues/db/issue-activity.ts`); this published package cannot import
|
|
42
42
|
* danxbot source.
|
|
43
43
|
*/
|
|
44
|
+
import { controlOfFrame, invalidCompactionReason } from "./compaction-control.js";
|
|
44
45
|
import { LINE_PREFIX, oneLine } from "./one-line.js";
|
|
45
46
|
import { pacingRelayLine } from "./pacing-relay.js";
|
|
46
47
|
import { isUrgent } from "./urgent-records.js";
|
|
@@ -524,6 +525,22 @@ export async function runListener(options, deps) {
|
|
|
524
525
|
remember(id);
|
|
525
526
|
return null;
|
|
526
527
|
};
|
|
528
|
+
const emitCompaction = (message) => {
|
|
529
|
+
let parsed;
|
|
530
|
+
let invalid;
|
|
531
|
+
try {
|
|
532
|
+
parsed = JSON.parse(message.data);
|
|
533
|
+
invalid = invalidCompactionReason(parsed);
|
|
534
|
+
}
|
|
535
|
+
catch (err) {
|
|
536
|
+
invalid = `not JSON: ${err.message}`;
|
|
537
|
+
}
|
|
538
|
+
if (invalid === null)
|
|
539
|
+
deps.write({ type: "compaction", control: controlOfFrame(parsed) });
|
|
540
|
+
else
|
|
541
|
+
deps.write({ type: "event", id: null, text: `${LINE_PREFIX} could not read compaction control (${invalid}): ${oneLine(message.data, 300)}`, origin: null, urgent: isUrgent({ source: "fault" }), scope: "session" });
|
|
542
|
+
return null;
|
|
543
|
+
};
|
|
527
544
|
const handle = (message) => {
|
|
528
545
|
if (message.event === "end") {
|
|
529
546
|
// DX-2920 (round 3) — a JSON.parse failure here falls through to the
|
|
@@ -551,6 +568,9 @@ export async function runListener(options, deps) {
|
|
|
551
568
|
// DX-4342 - the auto-resume's message once the limit that paused or stopped this session has reset: advisory like a pacing message.
|
|
552
569
|
if (message.event === "resume")
|
|
553
570
|
return emitAdvisory(message, "resume message", invalidResumeReason, (p) => formatResumeLine(p), () => ({ source: "resume" }), "session");
|
|
571
|
+
// DX-4993 - the compaction control: level-triggered, no id, no replay. A frame that cannot be read is a fault line (never silence), and the next frame still lands.
|
|
572
|
+
if (message.event === "compaction")
|
|
573
|
+
return emitCompaction(message);
|
|
554
574
|
if (message.event !== "activity")
|
|
555
575
|
return null;
|
|
556
576
|
// `Number(null)` is 0, so a frame with no id must never be read as event 0: that id
|
|
@@ -53,11 +53,13 @@
|
|
|
53
53
|
* mint / inventory requests are aborted by `close()`, and `close()` also clears a pending call's wait timer (the one ref'd timer,
|
|
54
54
|
* held only until the call answers). The server calls `close()` when its transport closes and when its stdin ends (`shutdown.ts`).
|
|
55
55
|
*/
|
|
56
|
+
import { fileOf, formatCompactionAskLine } from "./compaction-control.js";
|
|
56
57
|
import { DigestHold } from "./digest-hold.js";
|
|
57
58
|
import { HeartbeatReporter } from "./heartbeat-reporter.js";
|
|
58
59
|
import { capResumeIds, DELIVERED_ID_MEMORY, HEALTHY_CONNECTION_MS, INITIAL_BACKOFF_MS, MAX_BACKOFF_MS, runListener, } from "./listen.js";
|
|
59
60
|
import { fetchPlanInventory, HEARTBEAT_INTERVAL_MS, heartbeatConnection, MAX_REFUSED_TICKETS_IN_A_ROW, mintTicket, } from "./session-stream.js";
|
|
60
61
|
import { fixForStop } from "./stop-fixes.js";
|
|
62
|
+
import { LINE_PREFIX } from "./one-line.js";
|
|
61
63
|
import { isUrgent } from "./urgent-records.js";
|
|
62
64
|
/** The shortest and longest wait one call may ask for; a short ceiling keeps an orphaned waiter's life short. */
|
|
63
65
|
export const WAIT_MIN_MS = 1_000;
|
|
@@ -295,10 +297,22 @@ export class PlanEventRelay {
|
|
|
295
297
|
this.suspend();
|
|
296
298
|
return;
|
|
297
299
|
}
|
|
298
|
-
|
|
300
|
+
const auth = this.deps.auth();
|
|
301
|
+
if (auth === null)
|
|
299
302
|
return;
|
|
303
|
+
this.touchCompactionFile(auth.sessionId);
|
|
300
304
|
this.reporter?.tick();
|
|
301
305
|
}
|
|
306
|
+
/** DX-4993: a live relay keeps its session's compaction file out of the weekly pruning (`CompactionFiles.touch`); a failure is reported to stderr, never ends the heartbeat. */
|
|
307
|
+
touchCompactionFile(sessionId) {
|
|
308
|
+
try {
|
|
309
|
+
this.deps.compactionFiles.touch(sessionId);
|
|
310
|
+
}
|
|
311
|
+
catch (err) {
|
|
312
|
+
process.stderr.write(`plan_events_wait: the compaction control file could not be kept alive: ${err instanceof Error ? err.message : String(err)}
|
|
313
|
+
`);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
302
316
|
/**
|
|
303
317
|
* Let go of everything the relay holds for a caller: the stream, the heartbeat, the digest, and the plan-scoped records.
|
|
304
318
|
* What was held for a digest was never marked delivered, so the dashboard's replay (after the caller's cursor) returns it to
|
|
@@ -338,6 +352,38 @@ export class PlanEventRelay {
|
|
|
338
352
|
this.terminal(run, "relay_failed", message);
|
|
339
353
|
});
|
|
340
354
|
}
|
|
355
|
+
/**
|
|
356
|
+
* DX-4993 - the session's compaction control arrived: show the agent a request it has not been shown, and write the file the plugin's gate reads.
|
|
357
|
+
* A control with a new request (`ask.at`) is shown once: the file remembers which, so a reconnect, a restart of this server or a repeat of the same
|
|
358
|
+
* frame shows nothing twice. A file that cannot be read or written is a line to the agent (a fault: the gate cannot work), never silence.
|
|
359
|
+
*/
|
|
360
|
+
applyCompaction(control) {
|
|
361
|
+
const auth = this.deps.auth();
|
|
362
|
+
if (auth === null)
|
|
363
|
+
return;
|
|
364
|
+
// DX-4993: a file that cannot be read is no memory of what was shown, never a reason not to write: the write below replaces it (otherwise the same fault repeats
|
|
365
|
+
// at every frame and the plugin's gate never gets a readable control). The agent is told once per frame that it happened.
|
|
366
|
+
let askedAt = null;
|
|
367
|
+
try {
|
|
368
|
+
askedAt = this.deps.compactionFiles.read(auth.sessionId)?.askedAt ?? null;
|
|
369
|
+
}
|
|
370
|
+
catch (err) {
|
|
371
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
372
|
+
this.digest.route({ type: "event", id: null, text: `${LINE_PREFIX} the compaction control file of this session could not be read (${detail}); it is replaced`, origin: null, urgent: isUrgent({ source: "fault" }), scope: "session" });
|
|
373
|
+
}
|
|
374
|
+
try {
|
|
375
|
+
const ask = control?.ask ?? null;
|
|
376
|
+
const show = ask !== null && ask.at !== askedAt;
|
|
377
|
+
this.deps.compactionFiles.write(auth.sessionId, fileOf(control, ask === null ? null : show ? ask.at : askedAt, auth.sessionId));
|
|
378
|
+
if (show) {
|
|
379
|
+
this.digest.route({ type: "event", id: null, text: formatCompactionAskLine(ask), origin: null, urgent: isUrgent({ source: "compaction_ask" }), scope: "session" });
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
catch (err) {
|
|
383
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
384
|
+
this.digest.route({ type: "event", id: null, text: `${LINE_PREFIX} the compaction control could not be kept for this session (${detail}), so its compaction gate cannot work`, origin: null, urgent: isUrgent({ source: "fault" }), scope: "session" });
|
|
385
|
+
}
|
|
386
|
+
}
|
|
341
387
|
current(run) {
|
|
342
388
|
return this.run === run && !run.abort.signal.aborted;
|
|
343
389
|
}
|
|
@@ -415,6 +461,10 @@ export class PlanEventRelay {
|
|
|
415
461
|
return;
|
|
416
462
|
}
|
|
417
463
|
attempt.emitted = true;
|
|
464
|
+
if (output.type === "compaction") {
|
|
465
|
+
this.applyCompaction(output.control);
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
418
468
|
this.digest.route(output);
|
|
419
469
|
},
|
|
420
470
|
onState: (state) => {
|
|
@@ -67,7 +67,10 @@ export function sessionConnectionPath(sessionId, home = homedir()) {
|
|
|
67
67
|
assertSessionIdSegment(sessionId);
|
|
68
68
|
return join(sessionConnectionDir(home), `${sessionId}.json`);
|
|
69
69
|
}
|
|
70
|
-
/**
|
|
70
|
+
/**
|
|
71
|
+
* Remove the per-session `.json` files in `dir` that nothing has touched for `STALE_RECORD_MS`, and the `.tmp` files a failed
|
|
72
|
+
* write-then-rename left beside them (DX-4993).
|
|
73
|
+
*/
|
|
71
74
|
export function pruneStaleSessionFiles(dir, now) {
|
|
72
75
|
let names;
|
|
73
76
|
try {
|
|
@@ -77,7 +80,7 @@ export function pruneStaleSessionFiles(dir, now) {
|
|
|
77
80
|
return; // nothing written yet
|
|
78
81
|
}
|
|
79
82
|
for (const name of names) {
|
|
80
|
-
if (!name.endsWith(".json"))
|
|
83
|
+
if (!name.endsWith(".json") && !name.endsWith(".tmp"))
|
|
81
84
|
continue;
|
|
82
85
|
const file = join(dir, name);
|
|
83
86
|
try {
|
package/dist/urgent-records.js
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
* creates a record names its source and calls `isUrgent`; none writes an `urgent` literal of its own. The flag is a required
|
|
8
8
|
* boolean on `ListenEvent`, so a site that forgets it fails `make typecheck`, and no reader defaults a missing one.
|
|
9
9
|
*
|
|
10
|
-
* Urgent: a critical usage-pacing stop (the account is about to hit its limit and the session must wind down now),
|
|
11
|
-
* notice that this session's key was revoked (it can no longer call the dashboard). Everything else is not: activity events
|
|
10
|
+
* Urgent: a critical usage-pacing stop (the account is about to hit its limit and the session must wind down now), the
|
|
11
|
+
* notice that this session's key was revoked (it can no longer call the dashboard), and 50 Arms' request to prepare for a compaction. Everything else is not: activity events
|
|
12
12
|
* (comments, answers, problems, blocks, design and pull-request outcomes), the idle nudge, the auto-resume message, a digest,
|
|
13
13
|
* the relay's own failure notice, and any line that reports an unreadable event or frame (a fault line).
|
|
14
14
|
*
|
|
@@ -23,6 +23,9 @@ export function isUrgent(record) {
|
|
|
23
23
|
return record.level === "critical";
|
|
24
24
|
case "key_revoked":
|
|
25
25
|
return true;
|
|
26
|
+
// DX-4993: 50 Arms' request to prepare for a compaction must reach the agent inside a running turn, not after it: the compaction waits for the agent's ready.
|
|
27
|
+
case "compaction_ask":
|
|
28
|
+
return true;
|
|
26
29
|
case "activity":
|
|
27
30
|
case "nudge":
|
|
28
31
|
case "resume":
|
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.305",
|
|
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, request_permission, attach_file, and danxbot_slack_reply) (DX-3465, PLN-11 R-25).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|