@thehammer/danx-dashboard-mcp 0.1.285 → 0.1.287
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 +3 -9
- package/dist/credential.js +5 -5
- package/dist/digest-hold.js +2 -1
- package/dist/heartbeat-reporter.js +2 -2
- package/dist/index.js +16 -44
- package/dist/listen.js +11 -6
- package/dist/plan-events-relay.js +4 -3
- package/dist/request-bound.js +1 -2
- package/dist/restart-notice.js +50 -4
- package/dist/session-connection.js +9 -10
- package/dist/session-options.js +6 -6
- package/dist/stop-fixes.js +4 -4
- package/dist/urgent-records.js +34 -0
- package/package.json +1 -1
- package/dist/activity.js +0 -260
- package/dist/background-work.js +0 -162
- package/dist/one-shot-session-request.js +0 -105
- package/dist/ready-cards.js +0 -176
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@ Stdio MCP server fronting danxbot's dashboard HTTP API. Since DX-3465 it exposes
|
|
|
9
9
|
| Var | Purpose |
|
|
10
10
|
|---|---|
|
|
11
11
|
| `DANXBOT_DASHBOARD_URL` | Dashboard base, e.g. `http://danxbot-dashboard:5555` (inside compose) or `http://localhost:5555` (host). An operator session gets it from the danxbot plugin: its `dashboard_url` option (default the hosted dashboard), overridden by `DANXBOT_DASHBOARD_URL` in your shell or a repo's committed `.claude/settings.json` `env` block |
|
|
12
|
-
| `DANX_DASHBOARD_CREDENTIAL` | A DISPATCH's credential: the bearer verbatim, substituted into the materialized `.mcp.json` from its overlay (DX-2862, `src/credential.ts`). Authoritative when set; it never signs out. An operator session sets nothing: with no credential the server boots SIGNED OUT, every tool except `plan_connect` returns one "not signed in" halt, and `plan_connect` requests access (below).
|
|
12
|
+
| `DANX_DASHBOARD_CREDENTIAL` | A DISPATCH's credential: the bearer verbatim, substituted into the materialized `.mcp.json` from its overlay (DX-2862, `src/credential.ts`). Authoritative when set; it never signs out. An operator session sets nothing: with no credential the server boots SIGNED OUT, every tool except `plan_connect` returns one "not signed in" halt, and `plan_connect` requests access (below). `restart_notice` uses the same resolver, so a session's tools and its predecessor lookups can never sign in as two different identities; the relay in this server (`plan_events_wait`) signs in with the server's own credential |
|
|
13
13
|
| `DANXBOT_TEAM` | OPTIONAL, non-secret (DX-4446): the team slug (or id) whose approvers see this session's access request in their danxbot site. Set it in your shell or a repo's committed `.claude/settings.json` `env` block. Unset, the request is answered by link only. A value that is not a team slug fails at boot; a team the dashboard does not know makes `plan_connect` answer `unknown_team` (fix `DANXBOT_TEAM`), never a silent link fallback |
|
|
14
14
|
| `DANX_REPO_NAME` | OPTIONAL (DX-4555): repo half of the qualified board id; sent as the access request's repo label |
|
|
15
15
|
| `DANXBOT_BOARD_NAME` | OPTIONAL (DX-4555; needs `DANX_REPO_NAME`): board-slug half of the qualified board id (the dispatch injects `board.slug` here). With neither variable set (the danxbot plugin's own server) the server has no default board: a call that needs a board must carry its `board` argument or the dashboard answers 400 (plan and plan-session routes take their boards from the plan and need none) |
|
|
@@ -20,7 +20,7 @@ BOARD-ONLY (DX-1171): board is the first-level concept; repo is DERIVED from boa
|
|
|
20
20
|
|
|
21
21
|
An operator session has no stored credential. Signed out, `plan_connect` (optionally with `title`, the session's own Claude title) posts `POST /api/access-requests` and answers at once `{state: "approval_required", approvalUrl, confirmCode, expiresAt, instruction}`: show the user the code and open the URL; they approve in the dashboard. The server polls `POST /api/access-requests/claim` in the background, so the key is stored when the approval lands; the next `plan_connect` waits up to about 45 seconds for it, then connects, or answers `approval_pending`. One tool call never blocks for minutes. `denied` is reported once; a request that lapsed (the dashboard deletes it within about a minute of expiry, so a claim answers 404) starts a fresh one on the next call.
|
|
22
22
|
|
|
23
|
-
- **Storage.** `~/.config/danxbot/session-keys/<sha12(dashboard url)>/<session id>.json`, mode 0600, written write-then-rename, pruned after 7 days. One file per dashboard and per session; this server reads it at boot, and
|
|
23
|
+
- **Storage.** `~/.config/danxbot/session-keys/<sha12(dashboard url)>/<session id>.json`, mode 0600, written write-then-rename, pruned after 7 days. One file per dashboard and per session; this server reads it at boot, and `restart_notice` reads the same file through the `session-key` credential source and proves the fingerprint. The key lapses on the dashboard after 24 hours without use (every authenticated call, the relay's heartbeat included, renews it), and is revocable from the account page.
|
|
24
24
|
- **A 401 on the stored key** deletes the file and returns the sign-in halt (in `plan_connect`: a new request in the same call). That is a key that lapsed. A key a PERSON revoked is answered `401 key_revoked` (DX-4418): the key file is deleted and EVERY dashboard tool, `plan_connect` included, returns one stop halt (who revoked it and when; commit work in progress and end; a dispatched agent makes no further call), for the rest of the process. No access request is made. The worker finds the revocation itself and ends the dispatch as revoked (every dashboard route, the dispatch-complete one included, refuses a revoked key). `plan_events_wait` answers `stopped` with the reason `key_revoked` the same way. **A 404 on create** means the dashboard predates request-and-approve and says so.
|
|
25
25
|
- A dispatch's `DANX_DASHBOARD_CREDENTIAL` skips all of this. There is no token-file credential source.
|
|
26
26
|
|
|
@@ -65,7 +65,7 @@ A program calls this, not a person: the danxbot Claude Code plugin's relay loop,
|
|
|
65
65
|
|
|
66
66
|
Input `{plan_id: number, cursor: string | null, timeout_ms: 1000..30000, transcript_path?: string}`. Answer, one of:
|
|
67
67
|
|
|
68
|
-
- `{events: [{cursor: string, text: string}]}`: every record the caller has not delivered, in arrival order (empty at the timeout). Each record carries the cursor to store once THAT record is delivered. There is no `id`: the caller compares nothing.
|
|
68
|
+
- `{events: [{cursor: string, text: string, urgent: boolean}]}`: every record the caller has not delivered, in arrival order (empty at the timeout). Each record carries the cursor to store once THAT record is delivered. There is no `id`: the caller compares nothing. `urgent` (DX-4721) is decided in one place by the record's kind (`src/urgent-records.ts`): true for a critical usage-pacing stop and the revoked-key notice, false for everything else (activity events, nudge, resume message, digest, the relay's failure notice, an unreadable line). The caller adds an urgent record to a running turn at once and delivers any other after the turn ends; `urgent` never changes order, seq or acknowledgement. The dashboard writes no operator-stop record yet; when it does, it becomes one more kind in that function.
|
|
69
69
|
- `{stopped: {reason, detail, fix}}`: a terminal outcome (`not_connected`, `plan_archived`, `unauthorized`, `key_revoked`, `mint_refused`, `mint_bad_response`, `session_is_worker`, `superseded`, `replaced`, `revoked`, `refused`, `credential_unavailable`, `relay_failed`). `fix` is the remedy text (`STOP_FIXES`, `src/stop-fixes.ts`). The next call starts from scratch.
|
|
70
70
|
|
|
71
71
|
The contract the plugin builds against:
|
|
@@ -79,8 +79,6 @@ The contract the plugin builds against:
|
|
|
79
79
|
- **Nothing outlives the session.** The heartbeat interval, the backoff sleep and the digest hold are unref'd; `close()` (run when the transport closes and when stdin ends) aborts the stream and the mint and inventory requests and clears a pending call's wait timer, the one ref'd timer, which otherwise holds the process only until the call answers.
|
|
80
80
|
- **Credential and audience.** The server's own, from its boot-time `CLAUDE_CODE_SESSION_ID`: no connection record, fingerprint check or environment variable is involved. The tool is advertised only to a person's session holding `plans.view`, never to a dispatch. The stream's advisory frames (the idle nudge, DX-4340's pacing message, DX-4342's auto-resume message) arrive as records like any event, at once and never held for a digest. The digest (agent and machine events held up to 10 minutes) is one record, `[plan events] digest`, and a held id is never acknowledged before it is returned.
|
|
81
81
|
|
|
82
|
-
The one-shot subcommands below (`activity`, `background-work`, `ready-cards`) still read the connection record this server wrote on `plan_connect` (`src/session-connection.ts`) and refuse to run unless the credential it names fingerprints identically (`credential_mismatch`, DX-2862).
|
|
83
|
-
|
|
84
82
|
## `restart_notice` — what a restarted session was working on (DX-3928, DX-4632)
|
|
85
83
|
|
|
86
84
|
A tool for the danxbot plugin's session-start hook, not for a model, advertised only on a person's session (never a dispatch) and **usable before sign-in**: a restarted Claude Code session gets a new session id, no plan binding and, until a person approves its first call, no dashboard key of its own, so nothing else could tell 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); `restart_notice` finds the newest OTHER sessions' records for the same project (at most ten, each within `STALE_RECORD_MS`), resolves each 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). No arguments. Answers one of:
|
|
@@ -91,10 +89,6 @@ A tool for the danxbot plugin's session-start hook, not for a model, advertised
|
|
|
91
89
|
|
|
92
90
|
ONE deadline of 8 s bounds the whole lookup, every candidate included (each request also keeps its own 5 s bound), so a hung dashboard answers `lookup_timeout` and never wedges a session start. A record from an older server (schema v1) is skipped here and refused, with the fix, everywhere a session reads its OWN record.
|
|
93
91
|
|
|
94
|
-
## `ready-cards` — the plan's cards anyone could pick up now (DX-4534)
|
|
95
|
-
|
|
96
|
-
Run by the danxbot plugin's Stop hook for a plan-connected session. `GET /api/plans/mine` answers the session's plan and the boards its cards live on (a session on no plan is a 409 `session_not_connected`, read as an empty answer); then, for each board, `GET /api/issues?board=` with `filter.plan_id` + `filter.status_derived` returns the plan's ToDo cards, and the ready ones are kept: a Story, Bug or Chore with no assigned agent, no live dispatch, no block, no open problem, no unmet `depends_on` and no active `conflict_on`. No dashboard route was added. Exit 0 prints one JSON line `{"plan_id": <n|null>, "cards": [{"id", "title"}]}`; exit 1 prints one stderr line that starts with its reason (`credential_unavailable`, `no_connection_record`, `http_error`, ...) and nothing on stdout.
|
|
97
|
-
|
|
98
92
|
## `subagents-live` — the plan pane's local live sub-agent feed (DX-4508)
|
|
99
93
|
|
|
100
94
|
`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 relay 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.
|
package/dist/credential.js
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
* DX-2862 / DX-4390 - the ONE resolver for this package's dashboard credential.
|
|
3
3
|
*
|
|
4
4
|
* WHY IT IS ONE FUNCTION AND NOT TWO. Two kinds of caller speak to the dashboard for the same
|
|
5
|
-
* Claude Code session: the MCP SERVER (the session's tools and its event relay) and the
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* both go through it, and
|
|
5
|
+
* Claude Code session: the MCP SERVER (the session's tools and its event relay) and the `restart_notice` predecessor lookup.
|
|
6
|
+
* When they resolved their credential independently they could differ (DX-2862): the session's tools worked, a
|
|
7
|
+
* lookup authenticated as somebody else, and its result was dropped with no error anywhere. So resolution lives HERE,
|
|
8
|
+
* both go through it, and the lookup additionally PROVES it holds the server's credential by comparing fingerprints
|
|
9
9
|
* (`session-connection.ts`).
|
|
10
10
|
*
|
|
11
11
|
* A credential comes from exactly one of two places:
|
|
@@ -27,7 +27,7 @@ export const CREDENTIAL_TOKEN_ENV = "DANX_DASHBOARD_CREDENTIAL";
|
|
|
27
27
|
/**
|
|
28
28
|
* A credential that cannot be resolved. Carries the remedy beside the reason, because both of this
|
|
29
29
|
* module's callers report failures to a human who is not reading a log: the server prints it to
|
|
30
|
-
* stderr, and
|
|
30
|
+
* stderr, and `restart_notice` answers it to the session.
|
|
31
31
|
*/
|
|
32
32
|
export class CredentialError extends Error {
|
|
33
33
|
fix;
|
package/dist/digest-hold.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isUrgent } from "./urgent-records.js";
|
|
1
2
|
export const DIGEST_INTERVAL_MS = 10 * 60_000;
|
|
2
3
|
export const DIGEST_PREFIX = "[plan events]";
|
|
3
4
|
/** ONE combined record for every event held since the last flush. */
|
|
@@ -39,7 +40,7 @@ export class DigestHold {
|
|
|
39
40
|
const entries = this.held;
|
|
40
41
|
this.held = [];
|
|
41
42
|
const ids = entries.map((e) => e.id).filter((id) => id !== null);
|
|
42
|
-
this.emit({ type: "event", id: ids.length === 0 ? null : Math.max(...ids), ids, text: formatDigest(entries), origin: null });
|
|
43
|
+
this.emit({ type: "event", id: ids.length === 0 ? null : Math.max(...ids), ids, text: formatDigest(entries), origin: null, urgent: isUrgent({ source: "digest" }) });
|
|
43
44
|
}
|
|
44
45
|
/**
|
|
45
46
|
* Stop the timer WITHOUT flushing. For a stop where a newer listener already streams this session and will receive (and
|
|
@@ -85,7 +85,7 @@ export class HeartbeatReporter {
|
|
|
85
85
|
// dashboard call says the same.
|
|
86
86
|
if (answer.revocation !== null && !this.revocationAnnounced) {
|
|
87
87
|
this.revocationAnnounced = true;
|
|
88
|
-
this.deps.announce(keyRevokedHalt(answer.revocation, false));
|
|
88
|
+
this.deps.announce(keyRevokedHalt(answer.revocation, false), "key_revoked");
|
|
89
89
|
}
|
|
90
90
|
return answer;
|
|
91
91
|
})
|
|
@@ -104,7 +104,7 @@ export class HeartbeatReporter {
|
|
|
104
104
|
// The session finds out, rather than continuing to believe it is connected.
|
|
105
105
|
this.deps.announce(oneLine(`${DIGEST_PREFIX} this session's listener stopped reaching the dashboard for ` +
|
|
106
106
|
`${Math.round((HEARTBEAT_SILENT_FAILURE_THRESHOLD * HEARTBEAT_INTERVAL_MS) / 1000)}s while it still ` +
|
|
107
|
-
`believed it was streaming — forcing a fresh connection now.`));
|
|
107
|
+
`believed it was streaming — forcing a fresh connection now.`), "relay_notice");
|
|
108
108
|
// End the CURRENT connection now, rather than wait on the stream reader's own read-idle timeout (which this exact
|
|
109
109
|
// failure mode can outlive indefinitely: the connection reads as established and keeps producing keep-alive bytes,
|
|
110
110
|
// only the outbound heartbeat silently never lands). The new attempt reconnects with the SAME ticket; the
|
package/dist/index.js
CHANGED
|
@@ -88,9 +88,6 @@ import { REQUEST_TIMEOUT_MS, realScheduleInterval, realSleep } from "./session-s
|
|
|
88
88
|
import { servePlanEventsWait } from "./plan-events-tool.js";
|
|
89
89
|
import { lookupRestartNotice, RESTART_NOTICE_ARGS } from "./restart-notice.js";
|
|
90
90
|
import { stopWhenSessionEnds } from "./shutdown.js";
|
|
91
|
-
import { BACKGROUND_WORK_SUBCOMMAND, runBackgroundWorkCommand } from "./background-work.js";
|
|
92
|
-
import { ACTIVITY_SUBCOMMAND, runActivityCommand } from "./activity.js";
|
|
93
|
-
import { READY_CARDS_SUBCOMMAND, runReadyCardsCommand } from "./ready-cards.js";
|
|
94
91
|
import { SUBAGENTS_LIVE_SUBCOMMAND, runSubagentsLiveCommand } from "./subagents-live.js";
|
|
95
92
|
import { resolveBootCredential } from "./credential.js";
|
|
96
93
|
import { SessionKeyUnusableError, deleteSessionKey } from "./session-key.js";
|
|
@@ -161,10 +158,10 @@ let client;
|
|
|
161
158
|
let listenerReminder;
|
|
162
159
|
/**
|
|
163
160
|
* DX-4390 — this server's credential: a dispatch's env bearer, this session's approved key, or
|
|
164
|
-
* none (signed out). The plan event relay in this server signs in with it directly (`access.currentToken()`).
|
|
165
|
-
*
|
|
161
|
+
* none (signed out). The plan event relay in this server signs in with it directly (`access.currentToken()`).
|
|
162
|
+
* `restart_notice` runs against other sessions' credentials and cannot see it, so `access.connection()` (the URL, where the credential is, its
|
|
166
163
|
* fingerprint; never the credential itself) is written to the session's connection record on a successful `plan_connect`
|
|
167
|
-
* for
|
|
164
|
+
* for it.
|
|
168
165
|
*/
|
|
169
166
|
let access;
|
|
170
167
|
// DX-4434 — late-bound with `access` and `client`; see `permission-request.ts`.
|
|
@@ -329,8 +326,8 @@ async function syncToolsAfterSignIn() {
|
|
|
329
326
|
*/
|
|
330
327
|
const sleepHoldingProcess = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
331
328
|
/**
|
|
332
|
-
* DX-2862 / DX-4390 — the credential this server boots with, through the ONE resolver
|
|
333
|
-
*
|
|
329
|
+
* DX-2862 / DX-4390 — the credential this server boots with, through the ONE resolver
|
|
330
|
+
* `restart_notice` also uses (`session-options.ts`), so a session's tools and its lookups can never sign in as
|
|
334
331
|
* two different identities. None is not an error: the server boots signed out. A stored key that
|
|
335
332
|
* is there but unusable is reported loudly and deleted - it is a cache of a revocable key, and the
|
|
336
333
|
* session asks for a new one.
|
|
@@ -737,7 +734,7 @@ strictTool("plan_connect",
|
|
|
737
734
|
'The `naming` block\'s exact `{Name}: PLAN-NNN {short title}`, after renaming this thread. Omit on the first connect.'),
|
|
738
735
|
disconnect: z.boolean().optional().describe("true = leave `plan_id` (your current plan); no `title`."),
|
|
739
736
|
},
|
|
740
|
-
// DX-2862 — the connect is also the moment the session's connection record is written for the
|
|
737
|
+
// DX-2862 — the connect is also the moment the session's connection record is written for `restart_notice` and the listener reminder: of the
|
|
741
738
|
// dashboard MCP servers a session may have configured, the one that connected the plan is the one whose credential they
|
|
742
739
|
// must resolve (the relay needs none of this: it lives in each server, with that server's own credential).
|
|
743
740
|
async (args) => {
|
|
@@ -794,14 +791,16 @@ async (args) => {
|
|
|
794
791
|
strictTool("plan_events_wait",
|
|
795
792
|
// DX-4233 - for the danxbot plugin's relay loop. DX-4667 - and for a background waiter sub-agent of a plan orchestrator, which
|
|
796
793
|
// loops on it until events exist and returns them (and the cursor) to the orchestrator it wakes. It answers `events` (each
|
|
797
|
-
// `{cursor, text}`; empty after `timeout_ms`), the cursor to send being the last delivered event's, or `stopped` with the remedy
|
|
794
|
+
// `{cursor, text, urgent}`; empty after `timeout_ms`), the cursor to send being the last delivered event's, or `stopped` with the remedy
|
|
798
795
|
// to tell the session. General-audience text (DX-4550): nothing here names our own setup.
|
|
799
796
|
"Waits for events on the plan this session is connected to. For a relay or a background waiter that calls it in a loop " +
|
|
800
797
|
"and hands every event to the session; do not call it for any other reason. `plan_id` is the plan the session is on (a " +
|
|
801
798
|
"different plan than the last call's starts clean). `cursor` is the `cursor` of the last event delivered, as answered (null " +
|
|
802
|
-
"when none). Answers `{events: [{cursor, text}]}`, with `events` empty once `timeout_ms` passes, or `{stopped: " +
|
|
799
|
+
"when none). Answers `{events: [{cursor, text, urgent}]}`, with `events` empty once `timeout_ms` passes, or `{stopped: " +
|
|
803
800
|
"{reason, detail, fix}}` when the stream ended: tell the session the `fix`. An event stays until a later call's " +
|
|
804
|
-
"`cursor` passes it.
|
|
801
|
+
"`cursor` passes it. `urgent` is true for an event the session must hear at once (a critical usage limit, a revoked " +
|
|
802
|
+
"key) and false for every other: deliver an urgent one to the session even while it is working, and any other once its " +
|
|
803
|
+
"current turn ends. Only one wait is pending per session: a newer call ends the older. " +
|
|
805
804
|
"`transcript_path` is the session's transcript file, for its usage and title.", {
|
|
806
805
|
plan_id: z.number().int().positive().describe("Id of the plan this session is connected to."),
|
|
807
806
|
cursor: z
|
|
@@ -859,40 +858,14 @@ async function main() {
|
|
|
859
858
|
// symlink-aware (DX-1647) so it holds under the symlinked `npx` bin the worker
|
|
860
859
|
// spawns, not just a direct `node dist/index.js`.
|
|
861
860
|
//
|
|
862
|
-
// The
|
|
863
|
-
//
|
|
864
|
-
// session-connection record this server's own `plan_connect` wrote (`session-connection.ts`), never from the subcommand's own
|
|
865
|
-
// environment (DX-2862: trusting an ambient env credential is the exact bug this repo fixed). There is no event-stream
|
|
866
|
-
// subcommand: a session's plan events are served in this process by `plan_events_wait` (DX-4233).
|
|
861
|
+
// The only subcommand is `subagents-live`, which the danxbot plugin's plan pane runs (DX-4508); it does not boot the MCP
|
|
862
|
+
// server and uses no network or credential. A session's plan events are served in this process by `plan_events_wait` (DX-4233).
|
|
867
863
|
//
|
|
868
864
|
// Any OTHER argument is refused rather than ignored, so a typo cannot
|
|
869
865
|
// silently start an MCP server on stdio where a subcommand was meant to run.
|
|
870
866
|
if (isEntrypointModule(import.meta.url, process.argv[1])) {
|
|
871
867
|
const [subcommand, ...rest] = process.argv.slice(2);
|
|
872
|
-
if (subcommand ===
|
|
873
|
-
runBackgroundWorkCommand(rest).then((code) => process.exit(code), (err) => {
|
|
874
|
-
console.error(`[danx-dashboard-mcp] background-work fatal: ${err.message}`);
|
|
875
|
-
process.stdout.write(`${JSON.stringify({ ok: false, reason: "fatal" })}\n`);
|
|
876
|
-
process.exit(0);
|
|
877
|
-
});
|
|
878
|
-
}
|
|
879
|
-
else if (subcommand === ACTIVITY_SUBCOMMAND) {
|
|
880
|
-
// DX-3284 — the same hook-only contract as background-work: a rejection prints one
|
|
881
|
-
// failure line and exits 0, so an async hook never surfaces a stack trace.
|
|
882
|
-
runActivityCommand(rest).then((code) => process.exit(code), (err) => {
|
|
883
|
-
console.error(`[danx-dashboard-mcp] activity fatal: ${err.message}`);
|
|
884
|
-
process.stdout.write(`${JSON.stringify({ ok: false, reason: "fatal" })}\n`);
|
|
885
|
-
process.exit(0);
|
|
886
|
-
});
|
|
887
|
-
}
|
|
888
|
-
else if (subcommand === READY_CARDS_SUBCOMMAND) {
|
|
889
|
-
// DX-4534 — the plugin Stop hook's read; exit 0 (one JSON line) / 1 (read failed).
|
|
890
|
-
runReadyCardsCommand(rest).then((code) => process.exit(code), (err) => {
|
|
891
|
-
console.error(`[danx-dashboard-mcp] ${READY_CARDS_SUBCOMMAND} fatal: ${err.message}`);
|
|
892
|
-
process.exit(1);
|
|
893
|
-
});
|
|
894
|
-
}
|
|
895
|
-
else if (subcommand === SUBAGENTS_LIVE_SUBCOMMAND) {
|
|
868
|
+
if (subcommand === SUBAGENTS_LIVE_SUBCOMMAND) {
|
|
896
869
|
// DX-4508 — the plugin pane's local live sub-agent feed: stdout is its JSON Lines channel, so a fatal goes to
|
|
897
870
|
// stderr only and exits 1; a refused path exits 2 from the command itself.
|
|
898
871
|
runSubagentsLiveCommand(rest).then((code) => process.exit(code), (err) => {
|
|
@@ -901,9 +874,8 @@ if (isEntrypointModule(import.meta.url, process.argv[1])) {
|
|
|
901
874
|
});
|
|
902
875
|
}
|
|
903
876
|
else if (subcommand !== undefined) {
|
|
904
|
-
console.error(`[danx-dashboard-mcp] unknown subcommand "${subcommand}" (the only
|
|
905
|
-
`"${
|
|
906
|
-
`"${READY_CARDS_SUBCOMMAND}", and "${SUBAGENTS_LIVE_SUBCOMMAND}")`);
|
|
877
|
+
console.error(`[danx-dashboard-mcp] unknown subcommand "${subcommand}" (the only one is ` +
|
|
878
|
+
`"${SUBAGENTS_LIVE_SUBCOMMAND}")`);
|
|
907
879
|
process.exit(2);
|
|
908
880
|
}
|
|
909
881
|
else {
|
package/dist/listen.js
CHANGED
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
import { errorCodeOf } from "./json-body.js";
|
|
52
52
|
import { LINE_PREFIX, oneLine } from "./one-line.js";
|
|
53
53
|
import { pacingRelayLine } from "./pacing-relay.js";
|
|
54
|
+
import { isUrgent } from "./urgent-records.js";
|
|
54
55
|
export const INITIAL_BACKOFF_MS = 1_000;
|
|
55
56
|
export const MAX_BACKOFF_MS = 30_000;
|
|
56
57
|
/** Three of the dashboard's 15 s keep-alives without a byte means the connection is dead. */
|
|
@@ -503,7 +504,7 @@ export async function runListener(options, deps) {
|
|
|
503
504
|
* session; a pacing change is an instruction to act now). A frame that is not JSON or not the right shape is reported from an explicit check
|
|
504
505
|
* naming the fault, and the next frame still emits.
|
|
505
506
|
*/
|
|
506
|
-
const emitAdvisory = (message, what, invalidReason, format, scope) => {
|
|
507
|
+
const emitAdvisory = (message, what, invalidReason, format, sourceOf, scope) => {
|
|
507
508
|
let parsed;
|
|
508
509
|
let invalid;
|
|
509
510
|
try {
|
|
@@ -514,7 +515,9 @@ export async function runListener(options, deps) {
|
|
|
514
515
|
invalid = `not JSON: ${err.message}`;
|
|
515
516
|
}
|
|
516
517
|
const text = invalid === null ? format(parsed) : `${LINE_PREFIX} could not read ${what} (${invalid}): ${oneLine(message.data, 300)}`;
|
|
517
|
-
|
|
518
|
+
// DX-4721: an unreadable frame is a fault line, never the frame it failed to be (a malformed `critical` pacing frame is not a stop).
|
|
519
|
+
const urgent = isUrgent(invalid === null ? sourceOf(parsed) : { source: "fault" });
|
|
520
|
+
deps.write({ type: "event", id: null, text, origin: null, urgent, ...(scope === undefined ? {} : { scope }) });
|
|
518
521
|
return null;
|
|
519
522
|
};
|
|
520
523
|
const handle = (message) => {
|
|
@@ -537,13 +540,13 @@ export async function runListener(options, deps) {
|
|
|
537
540
|
return { kind: "ended", reason: isKnownWireEndReason(reason) ? reason : "unknown" };
|
|
538
541
|
}
|
|
539
542
|
if (message.event === "nudge")
|
|
540
|
-
return emitAdvisory(message, "nudge", invalidNudgeReason, (p) => formatNudgeLine(p));
|
|
543
|
+
return emitAdvisory(message, "nudge", invalidNudgeReason, (p) => formatNudgeLine(p), () => ({ source: "nudge" }));
|
|
541
544
|
// DX-4340 - the pacing tick's one message for this session's Claude account (level or budget changed): advisory like a nudge.
|
|
542
545
|
if (message.event === "pacing")
|
|
543
|
-
return emitAdvisory(message, "pacing message", invalidPacingReason, (p) => formatPacingLine(p), "session");
|
|
546
|
+
return emitAdvisory(message, "pacing message", invalidPacingReason, (p) => formatPacingLine(p), (p) => ({ source: "pacing", level: p.level }), "session");
|
|
544
547
|
// DX-4342 - the auto-resume's message once the limit that paused or stopped this session has reset: advisory like a pacing message.
|
|
545
548
|
if (message.event === "resume")
|
|
546
|
-
return emitAdvisory(message, "resume message", invalidResumeReason, (p) => formatResumeLine(p), "session");
|
|
549
|
+
return emitAdvisory(message, "resume message", invalidResumeReason, (p) => formatResumeLine(p), () => ({ source: "resume" }), "session");
|
|
547
550
|
if (message.event !== "activity")
|
|
548
551
|
return null;
|
|
549
552
|
// `Number(null)` is 0, so a frame with no id must never be read as event 0: that id
|
|
@@ -577,7 +580,9 @@ export async function runListener(options, deps) {
|
|
|
577
580
|
// 10-minute digest wait just because it happened to come from an agent or
|
|
578
581
|
// a machine writer.
|
|
579
582
|
const origin = invalid === null ? parsed.origin : null;
|
|
580
|
-
|
|
583
|
+
// DX-4721: an unreadable event is a fault line; a readable one is urgent by its kind (none is today).
|
|
584
|
+
const urgent = isUrgent(invalid === null ? { source: "activity", kind: parsed.kind } : { source: "fault" });
|
|
585
|
+
deps.write({ type: "event", id: knownId ? id : null, text, origin, urgent });
|
|
581
586
|
if (knownId)
|
|
582
587
|
remember(id);
|
|
583
588
|
return null;
|
|
@@ -57,6 +57,7 @@ import { HeartbeatReporter } from "./heartbeat-reporter.js";
|
|
|
57
57
|
import { capResumeIds, DELIVERED_ID_MEMORY, HEALTHY_CONNECTION_MS, INITIAL_BACKOFF_MS, MAX_BACKOFF_MS, runListener, } from "./listen.js";
|
|
58
58
|
import { classifyAdmissionRefusal, fetchPlanInventory, HEARTBEAT_INTERVAL_MS, heartbeatConnection, MAX_REFUSED_TICKETS_IN_A_ROW, mintTicket, } from "./session-stream.js";
|
|
59
59
|
import { fixForStop } from "./stop-fixes.js";
|
|
60
|
+
import { isUrgent } from "./urgent-records.js";
|
|
60
61
|
/** The shortest and longest wait one call may ask for; a short ceiling keeps an orphaned waiter's life short. */
|
|
61
62
|
export const WAIT_MIN_MS = 1_000;
|
|
62
63
|
export const WAIT_MAX_MS = 30_000;
|
|
@@ -183,7 +184,7 @@ export class PlanEventRelay {
|
|
|
183
184
|
}
|
|
184
185
|
/** Everything buffered, each record with the cursor to store once it is delivered. */
|
|
185
186
|
answer() {
|
|
186
|
-
return { events: this.buffer.map(({ seq, recent, text }) => ({ cursor: formatCursor({ seq, ids: recent }), text })) };
|
|
187
|
+
return { events: this.buffer.map(({ seq, recent, text, urgent }) => ({ cursor: formatCursor({ seq, ids: recent }), text, urgent })) };
|
|
187
188
|
}
|
|
188
189
|
// ------------------------------------------------------------------------------------------------------------ the buffer
|
|
189
190
|
/**
|
|
@@ -200,7 +201,7 @@ export class PlanEventRelay {
|
|
|
200
201
|
this.seenIds.shift();
|
|
201
202
|
}
|
|
202
203
|
this.seq += 1;
|
|
203
|
-
this.buffer.push({ seq: this.seq, own, recent: [...this.recentIds], text: event.text, scope: event.scope ?? "plan" });
|
|
204
|
+
this.buffer.push({ seq: this.seq, own, recent: [...this.recentIds], text: event.text, scope: event.scope ?? "plan", urgent: event.urgent });
|
|
204
205
|
const waiter = this.waiter;
|
|
205
206
|
if (waiter === null)
|
|
206
207
|
return;
|
|
@@ -246,7 +247,7 @@ export class PlanEventRelay {
|
|
|
246
247
|
connection: () => heartbeatConnection(this.connectionState, this.stoppedReason, this.lastInventoryError),
|
|
247
248
|
streaming: () => this.connectionState === "streaming",
|
|
248
249
|
// `origin: null` skips the agent/machine hold: the notice reaches the session at once. It is about the session, not a plan.
|
|
249
|
-
announce: (text) => this.digest.route({ type: "event", id: null, text, origin: null, scope: "session" }),
|
|
250
|
+
announce: (text, source) => this.digest.route({ type: "event", id: null, text, origin: null, scope: "session", urgent: isUrgent({ source }) }),
|
|
250
251
|
forceReconnect: () => this.run?.forceReconnect?.(),
|
|
251
252
|
}, transcriptPath ?? null);
|
|
252
253
|
}
|
package/dist/request-bound.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* DX-4632 - the ONE bound every dashboard request of this package shares: a timeout of its own, plus an OUTER signal that ends
|
|
3
|
-
* the request early (a lookup's one deadline, a closed relay). Used by `sendSessionRequest` (`
|
|
4
|
-
* one-shot subcommands and `restart_notice`) and by `request` in `session-stream.ts` (the relay's mint, inventory and
|
|
3
|
+
* the request early (a lookup's one deadline, a closed relay). Used by `sendSessionRequest` (`restart-notice.ts`) and by `request` in `session-stream.ts` (the relay's mint, inventory and
|
|
5
4
|
* heartbeat), so the controller, the timer, the link to the outer signal and the cleanup exist once.
|
|
6
5
|
*/
|
|
7
6
|
export function boundRequest(timeoutMs, outer) {
|
package/dist/restart-notice.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* working was unattended: six operator answers sat unread for an hour on 2026-10-01. The session's own connection record cannot
|
|
8
8
|
* help (it does not exist yet); the PREVIOUS session's record can, found by the project key `plan_connect` wrote into it
|
|
9
9
|
* (`findPredecessorCandidates`). So this asks the dashboard about each predecessor as the predecessor, with the credential
|
|
10
|
-
* that predecessor's record names (`resolveDashboardAuth`, the resolver
|
|
10
|
+
* that predecessor's record names (`resolveDashboardAuth`, the resolver `session-options.ts` holds), which a signed-out
|
|
11
11
|
* session needs no key of its own to do. The tool is therefore usable signed out: it is declared like `plan_connect` (no
|
|
12
12
|
* permission) and only on a person's session (`tool-access.ts`).
|
|
13
13
|
*
|
|
@@ -41,12 +41,58 @@
|
|
|
41
41
|
* no cursor on the server.
|
|
42
42
|
*/
|
|
43
43
|
import { z } from "zod";
|
|
44
|
-
import {
|
|
45
|
-
import { resolveDashboardAuth, SessionStartError } from "./session-options.js";
|
|
44
|
+
import { boundRequest } from "./request-bound.js";
|
|
45
|
+
import { resolveDashboardAuth, SESSION_ID_HEADER, SessionStartError } from "./session-options.js";
|
|
46
46
|
import { findPredecessorCandidates, projectKeyOf, readSessionConnection } from "./session-connection.js";
|
|
47
47
|
import { checkedTranscriptPath } from "./session-stats.js";
|
|
48
48
|
import { readTranscriptLineage, TranscriptReadAborted, TranscriptUnreadable } from "./transcript-lineage.js";
|
|
49
49
|
import { fixForStop } from "./stop-fixes.js";
|
|
50
|
+
/**
|
|
51
|
+
* One authenticated HTTP call to this session's dashboard, with a hard
|
|
52
|
+
* timeout. Never throws — a network failure or an abort is reported as
|
|
53
|
+
* `RequestFailure`, exactly like any other classified failure the caller
|
|
54
|
+
* might produce from the status/body it gets back on success.
|
|
55
|
+
*/
|
|
56
|
+
export async function sendSessionRequest(options, path, init, deps) {
|
|
57
|
+
const bound = boundRequest(deps.requestTimeoutMs, deps.signal);
|
|
58
|
+
try {
|
|
59
|
+
try {
|
|
60
|
+
const response = await deps.fetch(`${options.dashboardUrl}${path}`, {
|
|
61
|
+
method: init.method,
|
|
62
|
+
headers: {
|
|
63
|
+
Authorization: `Bearer ${options.token}`,
|
|
64
|
+
Accept: "application/json",
|
|
65
|
+
...(init.body !== undefined ? { "Content-Type": "application/json" } : {}),
|
|
66
|
+
[SESSION_ID_HEADER]: options.sessionId,
|
|
67
|
+
},
|
|
68
|
+
body: init.body,
|
|
69
|
+
signal: bound.signal,
|
|
70
|
+
});
|
|
71
|
+
const text = await response.text();
|
|
72
|
+
return { status: response.status, text };
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
// DX-3367 fix-up — the caught value is never inspected (an aborted
|
|
76
|
+
// fetch's own rejection shape varies by runtime), so binding it to a
|
|
77
|
+
// name the code never reads was dead weight the linter never caught.
|
|
78
|
+
return { ok: false, reason: bound.aborted() ? "timeout" : "request_failed" };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
finally {
|
|
82
|
+
bound.release();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/** `JSON.parse`, classified rather than thrown — the ONE shared parse point
|
|
86
|
+
* every caller uses once it has decided (from `status`) that the body is
|
|
87
|
+
* worth reading at all. */
|
|
88
|
+
export function tryParseJsonBody(text) {
|
|
89
|
+
try {
|
|
90
|
+
return { ok: true, body: text === "" ? null : JSON.parse(text) };
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return { ok: false };
|
|
94
|
+
}
|
|
95
|
+
}
|
|
50
96
|
export const RESTART_NOTICE_REQUEST_TIMEOUT_MS = 5_000;
|
|
51
97
|
/** DX-4632: the whole lookup, every candidate included (the old hook's `timeout 8s`). */
|
|
52
98
|
export const RESTART_NOTICE_DEADLINE_MS = 8_000;
|
|
@@ -58,7 +104,7 @@ export const RESTART_NOTICE_DEADLINE_MS = 8_000;
|
|
|
58
104
|
export const RESTART_NOTICE_MAX_CANDIDATES = 10;
|
|
59
105
|
/** The registry row whose text is the notice (`src/issues/reminders/seed.yaml`). */
|
|
60
106
|
export const RESTART_NOTICE_REMINDER_KEY = "plan_restart.waiting_events";
|
|
61
|
-
/** One authenticated GET (
|
|
107
|
+
/** One authenticated GET (`sendSessionRequest` above, so its bound and its failure names are the same), answering the notice text or `null`. */
|
|
62
108
|
export async function fetchRestartNotice(target, deps) {
|
|
63
109
|
// The bearer is the predecessor's; the session id header names the ASKING session.
|
|
64
110
|
const auth = { dashboardUrl: target.dashboardUrl, token: target.token, sessionId: target.askingSessionId };
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* DX-2862 — the record that hands ONE Claude Code session's dashboard
|
|
3
|
-
* connection from its MCP server to the
|
|
3
|
+
* connection from its MCP server to its other readers (`restart_notice`, the listener reminder).
|
|
4
4
|
*
|
|
5
|
-
* THE PROBLEM THIS SOLVES. A
|
|
6
|
-
* `ready-cards`) runs in the SESSION's environment, not in the MCP server's, which is the only
|
|
5
|
+
* THE PROBLEM THIS SOLVES. A reader outside the server process runs in the SESSION's environment, not in the MCP server's, which is the only
|
|
7
6
|
* place the server's `.mcp.json` env block exists. So it cannot see what the server was configured with, and before this it
|
|
8
7
|
* simply used its own ambient env credential. When that was a different credential from the server's, its report was
|
|
9
8
|
* dropped silently (the card's incident). The plan event relay (`plan_events_wait`) needs none of this: it runs inside the
|
|
@@ -11,8 +10,8 @@
|
|
|
11
10
|
*
|
|
12
11
|
* WHAT IS WRITTEN, AND WHAT IS NOT. On a successful `plan_connect` the server
|
|
13
12
|
* writes the dashboard URL, the credential SOURCE (a session key file path, or the name of
|
|
14
|
-
* an env var — never a credential) and the credential's FINGERPRINT. A
|
|
15
|
-
* reads that record, resolves the SAME source through the same resolver
|
|
13
|
+
* an env var — never a credential) and the credential's FINGERPRINT. A reader
|
|
14
|
+
* (`restart_notice`) reads that record, resolves the SAME source through the same resolver
|
|
16
15
|
* (`credential.ts`), and refuses to run unless what it resolved fingerprints
|
|
17
16
|
* identically. A rotated file or a session environment that disagrees with the
|
|
18
17
|
* server's therefore fails loud instead of reporting as somebody else.
|
|
@@ -91,7 +90,7 @@ export function pruneStaleSessionFiles(dir, now) {
|
|
|
91
90
|
}
|
|
92
91
|
}
|
|
93
92
|
/**
|
|
94
|
-
* Record this session's connection. Write-then-rename, so a
|
|
93
|
+
* Record this session's connection. Write-then-rename, so a reader
|
|
95
94
|
* concurrently sees the old record or the new one, never a torn one.
|
|
96
95
|
*/
|
|
97
96
|
export function writeSessionConnection(record, options = {}) {
|
|
@@ -146,8 +145,8 @@ export function readSessionConnection(sessionId, options = {}) {
|
|
|
146
145
|
* that tool's own answer when it could not be recorded.
|
|
147
146
|
*
|
|
148
147
|
* FAIL LOUD, IN THE ONE PLACE THE AGENT IS LOOKING. A connect whose record is
|
|
149
|
-
* missing produces a session that is connected but whose
|
|
150
|
-
*
|
|
148
|
+
* missing produces a session that is connected but whose `restart_notice` lookup and listener reminder cannot
|
|
149
|
+
* find its connection — so the failure belongs in the connect's answer, not only in a file
|
|
151
150
|
* nobody reads. It does NOT fail the connect itself: the session IS connected,
|
|
152
151
|
* and reporting otherwise would be a second lie on top of the first.
|
|
153
152
|
*/
|
|
@@ -157,7 +156,7 @@ export function recordSessionConnectionAfterConnect(result, inputs, write = writ
|
|
|
157
156
|
if (inputs.sessionId === undefined || inputs.sessionId === "") {
|
|
158
157
|
return {
|
|
159
158
|
connection_record_error: "connected, but this server has no CLAUDE_CODE_SESSION_ID, so no connection record can be written — " +
|
|
160
|
-
"
|
|
159
|
+
"`restart_notice` and the listener reminder will not find this session's connection",
|
|
161
160
|
};
|
|
162
161
|
}
|
|
163
162
|
try {
|
|
@@ -173,7 +172,7 @@ export function recordSessionConnectionAfterConnect(result, inputs, write = writ
|
|
|
173
172
|
catch (err) {
|
|
174
173
|
return {
|
|
175
174
|
connection_record_error: `connected, but recording this session's connection failed (${err.message}) — ` +
|
|
176
|
-
"
|
|
175
|
+
"`restart_notice` and the listener reminder will not find this session's connection until it succeeds",
|
|
177
176
|
};
|
|
178
177
|
}
|
|
179
178
|
}
|
package/dist/session-options.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* DX-2862 / DX-4233 - how
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* unless it fingerprints identically.
|
|
6
|
-
* server that holds the credential.
|
|
2
|
+
* DX-2862 / DX-4233 - how `restart_notice` (`restart-notice.ts`, this file's only caller) learns which dashboard to call and
|
|
3
|
+
* as whom when it asks about an earlier session. It reads the connection record that session's own MCP server wrote when it
|
|
4
|
+
* connected a plan (`session-connection.ts`), resolves the credential the record names through the same resolver the server
|
|
5
|
+
* used, and refuses unless it fingerprints identically. The in-process relay behind `plan_events_wait` needs none of this: it
|
|
6
|
+
* lives inside the server that holds the credential.
|
|
7
7
|
*/
|
|
8
8
|
import { homedir } from "node:os";
|
|
9
9
|
import { credentialFingerprint, describeCredentialSource, readCredential } from "./credential.js";
|
|
@@ -52,7 +52,7 @@ export function resolveDashboardAuth(sessionId, env, options = {}) {
|
|
|
52
52
|
const fingerprint = credentialFingerprint(token);
|
|
53
53
|
if (fingerprint !== record.fingerprint) {
|
|
54
54
|
throw new SessionStartError("credential_mismatch", `${describeCredentialSource(record.source)} now yields credential ${fingerprint}, but this session's ` +
|
|
55
|
-
`danx-dashboard MCP server is using ${record.fingerprint} — a
|
|
55
|
+
`danx-dashboard MCP server is using ${record.fingerprint} — a lookup would authenticate as somebody else`, fixForStop("credential_mismatch"));
|
|
56
56
|
}
|
|
57
57
|
return { dashboardUrl: record.dashboardUrl.replace(/\/+$/, ""), token, sessionId };
|
|
58
58
|
}
|
package/dist/stop-fixes.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* DX-4233 - the remedy a session is shown for each way its dashboard event stream can stop, in ONE place.
|
|
3
3
|
*
|
|
4
|
-
* The client that ends in these texts is the in-process relay behind `plan_events_wait` (`plan-events-relay.ts`);
|
|
5
|
-
*
|
|
4
|
+
* The client that ends in these texts is the in-process relay behind `plan_events_wait` (`plan-events-relay.ts`); `session-options.ts` uses
|
|
5
|
+
* `fixForStop` for a start it cannot make. Pure data and one function with no imports.
|
|
6
6
|
*/
|
|
7
7
|
/**
|
|
8
8
|
* The remedy per terminal reason. ONE map, so the message a session is shown and the message a log carries can never drift.
|
|
@@ -15,10 +15,10 @@
|
|
|
15
15
|
const RESTART_NOTICE_STOP_LEAD = "continue without it";
|
|
16
16
|
const RESTART_NOTICE_STOP_TAIL = "call plan_connect to connect this session to a plan";
|
|
17
17
|
export const STOP_FIXES = {
|
|
18
|
-
no_connection_record: "call plan_connect again in this session — its danx-dashboard MCP server records the connection
|
|
18
|
+
no_connection_record: "call plan_connect again in this session — its danx-dashboard MCP server records the connection that restart_notice and the listener reminder read " +
|
|
19
19
|
"(a server older than the one that introduced the record writes none)",
|
|
20
20
|
credential_unavailable: "call plan_connect again to request access: this session's key file is missing or unreadable",
|
|
21
|
-
credential_mismatch: "restart this Claude Code session so its MCP server
|
|
21
|
+
credential_mismatch: "restart this Claude Code session so its MCP server resolves its credential afresh, then call plan_connect again",
|
|
22
22
|
not_connected: "call plan_connect to connect this session to a plan",
|
|
23
23
|
plan_archived: "the plan was archived or deleted, so this session was disconnected; ask the operator to restore it, then call plan_connect again",
|
|
24
24
|
unauthorized: "the dashboard no longer accepts this session's key (it lapsed after a day unused): call plan_connect again to request access",
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DX-4721 - THE ONE PLACE that decides whether a relayed plan-event record is URGENT.
|
|
3
|
+
*
|
|
4
|
+
* Background. `plan_events_wait` hands the caller (the danxbot plugin's relay, or a plan orchestrator's waiter sub-agent) a
|
|
5
|
+
* stream of records. The caller delivers an ordinary record after the session's running turn ends, and adds an URGENT one to
|
|
6
|
+
* the running turn at once. Which record is which is decided here, by the record's KIND, and nowhere else: every site that
|
|
7
|
+
* creates a record names its source and calls `isUrgent`; none writes an `urgent` literal of its own. The flag is a required
|
|
8
|
+
* boolean on `ListenEvent`, so a site that forgets it fails `make typecheck`, and no reader defaults a missing one.
|
|
9
|
+
*
|
|
10
|
+
* Urgent: a critical usage-pacing stop (the account is about to hit its limit and the session must wind down now), and the
|
|
11
|
+
* notice that this session's key was revoked (it can no longer call the dashboard). Everything else is not: activity events
|
|
12
|
+
* (comments, answers, problems, blocks, design and pull-request outcomes), the idle nudge, the auto-resume message, a digest,
|
|
13
|
+
* the relay's own failure notice, and any line that reports an unreadable event or frame (a fault line).
|
|
14
|
+
*
|
|
15
|
+
* The operator's direct stop. The dashboard writes no stop record today (the activity vocabulary, `IssueActivityKind` in
|
|
16
|
+
* `src/issues/db/issue-activity.ts`, and the stream's frames have none), so there is no kind to key on. When the dashboard adds
|
|
17
|
+
* one, it becomes a `RecordSource` here, urgent, in this same function: never a text match.
|
|
18
|
+
*/
|
|
19
|
+
/** DX-4721: true for a critical pacing stop and a revoked key; false for every other source (see the module docblock). */
|
|
20
|
+
export function isUrgent(record) {
|
|
21
|
+
switch (record.source) {
|
|
22
|
+
case "pacing":
|
|
23
|
+
return record.level === "critical";
|
|
24
|
+
case "key_revoked":
|
|
25
|
+
return true;
|
|
26
|
+
case "activity":
|
|
27
|
+
case "nudge":
|
|
28
|
+
case "resume":
|
|
29
|
+
case "digest":
|
|
30
|
+
case "relay_notice":
|
|
31
|
+
case "fault":
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
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.287",
|
|
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",
|