@thehammer/danx-dashboard-mcp 0.1.243 → 0.1.245
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 +4 -0
- package/dist/activity.js +1 -1
- package/dist/index.js +11 -1
- package/dist/subagents-live.js +124 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -67,6 +67,10 @@ CLAUDE_CODE_SESSION_ID=<session> npx -y @thehammer/danx-dashboard-mcp@<version>
|
|
|
67
67
|
|
|
68
68
|
Run by the danxbot plugin's SessionStart hook for a session that is **not** plan-connected. A restarted Claude Code session gets a new session id and no plan binding, so nothing told it that the plan it had been working was unattended. `plan_connect` writes the session's working directory into its connection record (`projectKey`, record schema v2); this subcommand finds the newest OTHER session's record for the same project within `STALE_RECORD_MS`, resolves that record's own credential, and asks `GET /api/plan-sessions/<predecessor>/restart-notice` which plan it was on and how many operator events wait there (the connect digest's own query, read-only). Exit 0 with text on stdout = say it (the registry's `plan_restart.waiting_events` text — this package carries no wording); exit 0 with nothing = nothing to say (never connected from this project, plan archived or deleted, record stale); exit 1 = the lookup failed. A record from an older server (schema v1) is skipped here and refused, with the fix, everywhere a session reads its OWN record.
|
|
69
69
|
|
|
70
|
+
## `subagents-live` — the plan pane's local live sub-agent feed (DX-4508)
|
|
71
|
+
|
|
72
|
+
`danx-dashboard-mcp subagents-live <absolute path to the session's main transcript .jsonl>`. Spawned by the danxbot plugin's plan pane, which cannot tail files itself; the dashboard's sub-agent read lags the transcript by about 80s, this by about one second. It tails the session's transcript tree (the main file and `<session-id>/subagents/agent-*.jsonl` beside it) with the same `SessionLogWatcher` the bridge runs, from byte 0 once and then only appended bytes, polling every second, and computes each sub-agent through `@thehammer/danx-session-telemetry`'s `createSubagentLiveView` (the same observer, ledger counter and pricing rule the dashboard uses). stdout carries only `{"subagents":[...]}` lines: the first, after the catch-up read, holds every sub-agent (an empty array when there are none); each later one holds only the sub-agents a poll changed. No network, no credential. A missing, relative or non-`.jsonl` path exits 2 with one stderr line; SIGTERM, SIGINT or a closed stdout exits 0.
|
|
73
|
+
|
|
70
74
|
## Build + test
|
|
71
75
|
|
|
72
76
|
```bash
|
package/dist/activity.js
CHANGED
|
@@ -88,7 +88,7 @@ export function subagentActivityId(agentId) {
|
|
|
88
88
|
}
|
|
89
89
|
/**
|
|
90
90
|
* DX-4498 — the facts only a transcript knows (the spawning sub-agent, effort, current tool call, how a background task
|
|
91
|
-
* ended, the
|
|
91
|
+
* ended, the card its spawning `Agent` call names). A hook envelope carries none of them, so it reports each as unknown; the server's merge
|
|
92
92
|
* never lets an unknown erase a value the transcript producer stored. A `SubagentStop` names no status either: its
|
|
93
93
|
* finish reads as `done` unless a task notification says otherwise.
|
|
94
94
|
*/
|
package/dist/index.js
CHANGED
|
@@ -87,6 +87,7 @@ import { BACKGROUND_WORK_SUBCOMMAND, runBackgroundWorkCommand } from "./backgrou
|
|
|
87
87
|
import { ACTIVITY_SUBCOMMAND, runActivityCommand } from "./activity.js";
|
|
88
88
|
import { EVENT_TEXT_SUBCOMMAND, runEventTextCommand } from "./event-text.js";
|
|
89
89
|
import { RESTART_NOTICE_SUBCOMMAND, runRestartNoticeCommand } from "./restart-notice.js";
|
|
90
|
+
import { SUBAGENTS_LIVE_SUBCOMMAND, runSubagentsLiveCommand } from "./subagents-live.js";
|
|
90
91
|
import { resolveBootCredential } from "./credential.js";
|
|
91
92
|
import { SessionKeyUnusableError, deleteSessionKey } from "./session-key.js";
|
|
92
93
|
import { assertMcpTeamRef } from "./access-request.js";
|
|
@@ -686,9 +687,18 @@ if (isEntrypointModule(import.meta.url, process.argv[1])) {
|
|
|
686
687
|
process.exit(1);
|
|
687
688
|
});
|
|
688
689
|
}
|
|
690
|
+
else if (subcommand === SUBAGENTS_LIVE_SUBCOMMAND) {
|
|
691
|
+
// DX-4508 — the plugin pane's local live sub-agent feed: stdout is its JSON Lines channel, so a fatal goes to
|
|
692
|
+
// stderr only and exits 1; a refused path exits 2 from the command itself.
|
|
693
|
+
runSubagentsLiveCommand(rest).then((code) => process.exit(code), (err) => {
|
|
694
|
+
console.error(`[danx-dashboard-mcp] ${SUBAGENTS_LIVE_SUBCOMMAND} fatal: ${err.message}`);
|
|
695
|
+
process.exit(1);
|
|
696
|
+
});
|
|
697
|
+
}
|
|
689
698
|
else if (subcommand !== undefined) {
|
|
690
699
|
console.error(`[danx-dashboard-mcp] unknown subcommand "${subcommand}" (the only ones are ` +
|
|
691
|
-
`"${BRIDGE_SUBCOMMAND}", "${BACKGROUND_WORK_SUBCOMMAND}", "${ACTIVITY_SUBCOMMAND}", "${EVENT_TEXT_SUBCOMMAND}",
|
|
700
|
+
`"${BRIDGE_SUBCOMMAND}", "${BACKGROUND_WORK_SUBCOMMAND}", "${ACTIVITY_SUBCOMMAND}", "${EVENT_TEXT_SUBCOMMAND}", ` +
|
|
701
|
+
`"${RESTART_NOTICE_SUBCOMMAND}", and "${SUBAGENTS_LIVE_SUBCOMMAND}")`);
|
|
692
702
|
process.exit(2);
|
|
693
703
|
}
|
|
694
704
|
else {
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `danx-dashboard-mcp subagents-live <absolute main transcript .jsonl path>` — the LOCAL live feed of a session's
|
|
3
|
+
* sub-agents for the danxbot plugin's plan pane (DX-4508).
|
|
4
|
+
*
|
|
5
|
+
* WHY IT EXISTS. The pane's dashboard read (`GET /api/plan-sessions/:id/subagents`) lags the transcript by about 80s
|
|
6
|
+
* (the bridge's 5s transcript poll, its 15s heartbeat flush, the pane's 60s read). A hooks module cannot tail the files
|
|
7
|
+
* itself (`$.fs.read` has no offset and refuses files over 4 MiB), but it can spawn a long-lived child and read its
|
|
8
|
+
* stdout. This is that child: it tails the session's transcript tree on this machine and prints each change within one
|
|
9
|
+
* 1s poll of Claude Code writing it.
|
|
10
|
+
*
|
|
11
|
+
* ONE PLACE COMPUTES EACH NUMBER. It runs the same `SessionLogWatcher` the bridge runs (`session-stats.ts`), over the
|
|
12
|
+
* same session layout (the main transcript, and `<sessionDir>/<sessionId>/subagents/agent-<id>.jsonl` beside it), into
|
|
13
|
+
* the shared `createSubagentLiveView`, which feeds the same activity observer, usage ledger counter and pricing rule
|
|
14
|
+
* the dashboard's numbers come from. Nothing is counted or priced here.
|
|
15
|
+
*
|
|
16
|
+
* THE CONTRACT.
|
|
17
|
+
* - stdout carries ONLY lines of `{"subagents":[<snapshot>, ...]}` (`SubagentLiveSnapshot`), one per poll in which
|
|
18
|
+
* any snapshot changed, holding just the changed ones. The first line, printed once the whole tree has been read
|
|
19
|
+
* from byte 0, carries every sub-agent discovered (an empty array for a session with none, so a reader knows the
|
|
20
|
+
* catch-up is done). Every later line reads only bytes appended since.
|
|
21
|
+
* - diagnostics go to stderr.
|
|
22
|
+
* - a missing, relative or non-`.jsonl` path is refused: one stderr line, exit 2.
|
|
23
|
+
* - it runs until killed: SIGTERM, SIGINT or a closed stdout stops it cleanly with exit 0.
|
|
24
|
+
* - no network call, no credential, no dashboard client: everything it reads is a local file.
|
|
25
|
+
*/
|
|
26
|
+
import { statSync } from "node:fs";
|
|
27
|
+
import { basename, dirname, isAbsolute } from "node:path";
|
|
28
|
+
import { SessionLogWatcher, createSubagentLiveView, } from "@thehammer/danx-session-telemetry";
|
|
29
|
+
export const SUBAGENTS_LIVE_SUBCOMMAND = "subagents-live";
|
|
30
|
+
/** How often the transcript tree is polled: the pane's ~5s freshness target with room to spare. */
|
|
31
|
+
export const SUBAGENTS_LIVE_POLL_MS = 1_000;
|
|
32
|
+
const USAGE = `usage: danx-dashboard-mcp ${SUBAGENTS_LIVE_SUBCOMMAND} <absolute path to the session's main transcript .jsonl>`;
|
|
33
|
+
/** The validated transcript path, or the reason it is refused. */
|
|
34
|
+
export function parseSubagentsLiveArgs(args) {
|
|
35
|
+
if (args.length !== 1)
|
|
36
|
+
return { ok: false, reason: `expected exactly one argument, got ${args.length}` };
|
|
37
|
+
const path = args[0];
|
|
38
|
+
if (!isAbsolute(path))
|
|
39
|
+
return { ok: false, reason: `"${path}" is not an absolute path` };
|
|
40
|
+
if (!path.endsWith(".jsonl"))
|
|
41
|
+
return { ok: false, reason: `"${path}" is not a .jsonl transcript` };
|
|
42
|
+
let isFile;
|
|
43
|
+
try {
|
|
44
|
+
isFile = statSync(path).isFile();
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return { ok: false, reason: `"${path}" does not exist` };
|
|
48
|
+
}
|
|
49
|
+
if (!isFile)
|
|
50
|
+
return { ok: false, reason: `"${path}" is not a file` };
|
|
51
|
+
return { ok: true, path };
|
|
52
|
+
}
|
|
53
|
+
/** The one stdout line a poll's changes are printed as. */
|
|
54
|
+
export function formatSubagentsLine(subagents) {
|
|
55
|
+
return `${JSON.stringify({ subagents })}\n`;
|
|
56
|
+
}
|
|
57
|
+
export async function runSubagentsLiveCommand(args, deps = realDeps()) {
|
|
58
|
+
const parsed = parseSubagentsLiveArgs(args);
|
|
59
|
+
if (!parsed.ok) {
|
|
60
|
+
deps.writeError(`[danx-dashboard-mcp] ${SUBAGENTS_LIVE_SUBCOMMAND}: ${parsed.reason}. ${USAGE}`);
|
|
61
|
+
return 2;
|
|
62
|
+
}
|
|
63
|
+
const path = parsed.path;
|
|
64
|
+
const view = createSubagentLiveView();
|
|
65
|
+
let stopped = false;
|
|
66
|
+
let caughtUp = false;
|
|
67
|
+
let watcher = null;
|
|
68
|
+
let resolveStopped = () => { };
|
|
69
|
+
const stoppedHere = new Promise((resolve) => {
|
|
70
|
+
resolveStopped = resolve;
|
|
71
|
+
});
|
|
72
|
+
const emit = () => {
|
|
73
|
+
if (stopped)
|
|
74
|
+
return;
|
|
75
|
+
const changed = view.takeChanged();
|
|
76
|
+
// DX-4508 — the first poll's line is printed even when empty: it tells the reader the catch-up read is complete.
|
|
77
|
+
if (changed.length === 0 && caughtUp)
|
|
78
|
+
return;
|
|
79
|
+
caughtUp = true;
|
|
80
|
+
if (!deps.writeLine(formatSubagentsLine(changed)))
|
|
81
|
+
stop();
|
|
82
|
+
};
|
|
83
|
+
const stop = () => {
|
|
84
|
+
stopped = true;
|
|
85
|
+
watcher?.stop();
|
|
86
|
+
resolveStopped();
|
|
87
|
+
};
|
|
88
|
+
watcher = deps.makeWatcher({
|
|
89
|
+
cwd: dirname(path),
|
|
90
|
+
sessionDir: dirname(path),
|
|
91
|
+
sessionId: basename(path, ".jsonl"),
|
|
92
|
+
// DX-4508 — no stored cursors: every file is read from byte 0 once, then only its appended bytes.
|
|
93
|
+
startCursors: {},
|
|
94
|
+
pollIntervalMs: SUBAGENTS_LIVE_POLL_MS,
|
|
95
|
+
onSourceDiscovered: view.onSourceDiscovered,
|
|
96
|
+
onPollComplete: emit,
|
|
97
|
+
});
|
|
98
|
+
watcher.onUsageLine(view.onLine);
|
|
99
|
+
// Either the process is told to stop, or stdout refused a line (`emit`).
|
|
100
|
+
const stopRequested = Promise.race([deps.untilStopped(), stoppedHere]);
|
|
101
|
+
await watcher.start();
|
|
102
|
+
await stopRequested;
|
|
103
|
+
stop();
|
|
104
|
+
return 0;
|
|
105
|
+
}
|
|
106
|
+
function realDeps() {
|
|
107
|
+
return {
|
|
108
|
+
makeWatcher: (options) => new SessionLogWatcher(options),
|
|
109
|
+
writeLine: (line) => {
|
|
110
|
+
if (process.stdout.destroyed || !process.stdout.writable)
|
|
111
|
+
return false;
|
|
112
|
+
process.stdout.write(line);
|
|
113
|
+
return true;
|
|
114
|
+
},
|
|
115
|
+
writeError: (line) => process.stderr.write(`${line}\n`),
|
|
116
|
+
untilStopped: () => new Promise((resolve) => {
|
|
117
|
+
process.once("SIGTERM", () => resolve());
|
|
118
|
+
process.once("SIGINT", () => resolve());
|
|
119
|
+
// A reader that closes the pipe ends the command: there is no one left to print to.
|
|
120
|
+
process.stdout.once("error", () => resolve());
|
|
121
|
+
process.stdout.once("close", () => resolve());
|
|
122
|
+
}),
|
|
123
|
+
};
|
|
124
|
+
}
|
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.245",
|
|
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.29",
|
|
35
35
|
"zod": "^3.25.76"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|