@thehammer/danx-dashboard-mcp 0.1.204 → 0.1.205
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 +0 -4
- package/dist/index.js +4 -27
- package/dist/session-connection.js +4 -57
- package/dist/session-stats.js +1 -1
- package/package.json +2 -2
- package/dist/restart-notice.js +0 -140
- package/dist/terms-halt.js +0 -33
package/README.md
CHANGED
|
@@ -46,10 +46,6 @@ CLAUDE_CODE_SESSION_ID=<session> npx -y @thehammer/danx-dashboard-mcp@<version>
|
|
|
46
46
|
- **Stopping.** Last, one `{"type":"stopped","reason":"…","detail":"…","fix":"…"}` and exit, only on a terminal outcome. `fix` is the remedy shown to the session (`STOP_FIXES` in `src/bridge.ts` — never a log-only hint), so a session that hits a stop it cannot otherwise see still knows what to do about it. Terminal reasons: `no_connection_record` / `credential_unavailable` / `credential_mismatch` (the start-time checks above), `not_connected` (a `409 session_not_connected` at mint OR at stream admission, or a live `end("not_connected")` — all three mapped identically), `plan_archived` (DX-3809 — the plan was archived or deleted while the stream was live; the session is disconnected, and reconnects by hand after a restore), `unauthorized` (401/403), `mint_refused` (any other non-transient mint refusal), `mint_bad_response`, `superseded` / `replaced` (exit 0 — a newer or replacing connection already serves this session, so `fix` says no action is needed), `revoked`, or `refused` (two freshly minted tickets refused in a row with no more specific classification). A transient mint failure (network, timeout, 408, 429, 5xx) backs off and retries; a lapsed ticket lease re-mints.
|
|
47
47
|
- **Reconnect and resume.** A read-idle timeout (three missed keep-alives) turns a silently dead connection into a drop. Capped exponential backoff (1s → 30s) that resets only after a healthy connection, with `Last-Event-ID`, so a dashboard restart replays what was missed and nothing is emitted twice. `--resume-ids` carries the same guarantee across a process restart: the ids already delivered seed the duplicate guard, and the highest is the first `Last-Event-ID`. The dashboard floors that replay at the later of the session's first ticket and when it joined its current plan, so a restart loses nothing and a plan move replays nothing from before the move.
|
|
48
48
|
|
|
49
|
-
## `restart-notice` — what a restarted session was working on (DX-3928)
|
|
50
|
-
|
|
51
|
-
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.
|
|
52
|
-
|
|
53
49
|
## Build + test
|
|
54
50
|
|
|
55
51
|
```bash
|
package/dist/index.js
CHANGED
|
@@ -77,9 +77,8 @@ import { BRIDGE_SUBCOMMAND, runBridgeCommand } from "./bridge.js";
|
|
|
77
77
|
import { BACKGROUND_WORK_SUBCOMMAND, runBackgroundWorkCommand } from "./background-work.js";
|
|
78
78
|
import { ACTIVITY_SUBCOMMAND, runActivityCommand } from "./activity.js";
|
|
79
79
|
import { EVENT_TEXT_SUBCOMMAND, runEventTextCommand } from "./event-text.js";
|
|
80
|
-
import { RESTART_NOTICE_SUBCOMMAND, runRestartNoticeCommand } from "./restart-notice.js";
|
|
81
80
|
import { resolveDeclaredCredential } from "./credential.js";
|
|
82
|
-
import {
|
|
81
|
+
import { recordSessionConnectionAfterConnect } from "./session-connection.js";
|
|
83
82
|
import { createListenerReminder, isPlanConnectedOnThisMachine, } from "./listener-reminder.js";
|
|
84
83
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
85
84
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
@@ -87,7 +86,6 @@ import { z } from "zod";
|
|
|
87
86
|
import { DashboardHttpClient } from "./http-client.js";
|
|
88
87
|
import { planConnect, danxbotApi, danxbotApiList, danxbotApiSpec, attachFile, DANXBOT_API_METHODS, } from "./handlers.js";
|
|
89
88
|
import { bodyValueSchema, queryValueSchema } from "./types.js";
|
|
90
|
-
import { termsAcceptanceCallHalt, termsHaltFor } from "./terms-halt.js";
|
|
91
89
|
import { danxbotApiExamplesText } from "./danxbot-api-examples.js";
|
|
92
90
|
import { registerLifecycleTools, applyLifecycleVisibility, readLifecycleUrlsFromEnv, } from "./lifecycle.js";
|
|
93
91
|
function readEnvOrDie(name) {
|
|
@@ -280,9 +278,6 @@ export const server = new McpServer({
|
|
|
280
278
|
name: "danx-dashboard-mcp",
|
|
281
279
|
version: "0.1.0",
|
|
282
280
|
}, { instructions: SERVER_INSTRUCTIONS });
|
|
283
|
-
function haltResult(text) {
|
|
284
|
-
return { content: [{ type: "text", text }], isError: true };
|
|
285
|
-
}
|
|
286
281
|
function jsonResult(value) {
|
|
287
282
|
return {
|
|
288
283
|
content: [{ type: "text", text: JSON.stringify(value, null, 2) }],
|
|
@@ -390,17 +385,7 @@ strictTool("danxbot_api", "Call any dashboard API route your credential may reac
|
|
|
390
385
|
},
|
|
391
386
|
// DX-3997 — the result rides back verbatim, plus the dashboard's own `reminders[]` entries
|
|
392
387
|
// for this plan-connected session (the server decides; `listener-reminder.ts` only relays).
|
|
393
|
-
async (args) =>
|
|
394
|
-
// DX-4148 — an agent never accepts the Terms, and a terms refusal halts it (see terms-halt.ts).
|
|
395
|
-
const refused = termsAcceptanceCallHalt(args, config.baseUrl);
|
|
396
|
-
if (refused)
|
|
397
|
-
return haltResult(refused);
|
|
398
|
-
const result = await danxbotApi(client, args);
|
|
399
|
-
const halt = termsHaltFor(result);
|
|
400
|
-
if (halt)
|
|
401
|
-
return haltResult(halt);
|
|
402
|
-
return jsonResult(await listenerReminder.apply(result));
|
|
403
|
-
});
|
|
388
|
+
async (args) => jsonResult(await listenerReminder.apply(await danxbotApi(client, args))));
|
|
404
389
|
// ---------------- danxbot_api_list (DX-3465 A5, PLN-11 R-25) ----------------
|
|
405
390
|
strictTool("danxbot_api_list", "Lists the dashboard API routes your credential may call right now — `[{method, path, summary, permission}]`, " +
|
|
406
391
|
"one line each, in the route TEMPLATE form `danxbot_api_spec` also expects (e.g. " +
|
|
@@ -463,7 +448,7 @@ strictTool("attach_file", "Attach a LOCAL file to a card: `card_id` + `path` (an
|
|
|
463
448
|
// no existing plan for an id to name yet.
|
|
464
449
|
strictTool("plan_connect",
|
|
465
450
|
// 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
|
|
451
|
+
"Connect THIS session to a plan. ONE CALL IS ENOUGH TO START: the reply carries `{session, movedFrom, browserInstruction, briefing, listenerHealth}` — `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
452
|
plan_id: z.number().int().positive().describe("The plan id, from the plans list (`danxbot_api` `GET /api/plans`)."),
|
|
468
453
|
title: z
|
|
469
454
|
.string()
|
|
@@ -483,7 +468,6 @@ async (args) => {
|
|
|
483
468
|
...result,
|
|
484
469
|
...recordSessionConnectionAfterConnect(result, {
|
|
485
470
|
sessionId: config.session?.id,
|
|
486
|
-
projectKey: projectKeyOf(process.cwd()),
|
|
487
471
|
dashboardUrl: connection.dashboardUrl,
|
|
488
472
|
source: connection.source,
|
|
489
473
|
fingerprint: connection.fingerprint,
|
|
@@ -550,16 +534,9 @@ if (isEntrypointModule(import.meta.url, process.argv[1])) {
|
|
|
550
534
|
process.exit(1);
|
|
551
535
|
});
|
|
552
536
|
}
|
|
553
|
-
else if (subcommand === RESTART_NOTICE_SUBCOMMAND) {
|
|
554
|
-
// DX-3928 — same exit-code contract as event-text: 0 (text, or nothing to say) / 1 (lookup failed).
|
|
555
|
-
runRestartNoticeCommand(rest).then((code) => process.exit(code), (err) => {
|
|
556
|
-
console.error(`[danx-dashboard-mcp] restart-notice fatal: ${err.message}`);
|
|
557
|
-
process.exit(1);
|
|
558
|
-
});
|
|
559
|
-
}
|
|
560
537
|
else if (subcommand !== undefined) {
|
|
561
538
|
console.error(`[danx-dashboard-mcp] unknown subcommand "${subcommand}" (the only ones are ` +
|
|
562
|
-
`"${BRIDGE_SUBCOMMAND}", "${BACKGROUND_WORK_SUBCOMMAND}", "${ACTIVITY_SUBCOMMAND}",
|
|
539
|
+
`"${BRIDGE_SUBCOMMAND}", "${BACKGROUND_WORK_SUBCOMMAND}", "${ACTIVITY_SUBCOMMAND}", and "${EVENT_TEXT_SUBCOMMAND}")`);
|
|
563
540
|
process.exit(2);
|
|
564
541
|
}
|
|
565
542
|
else {
|
|
@@ -31,17 +31,10 @@
|
|
|
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
|
|
34
|
+
import { join } from "node:path";
|
|
35
35
|
/** One canonical shape (CLAUDE.md Core Principle 1) — a record of any other version is refused, never adapted. */
|
|
36
|
-
|
|
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 (`findPredecessorConnection`).
|
|
39
|
-
export const SESSION_CONNECTION_SCHEMA_VERSION = 2;
|
|
36
|
+
export const SESSION_CONNECTION_SCHEMA_VERSION = 1;
|
|
40
37
|
export const STALE_RECORD_MS = 7 * 24 * 60 * 60 * 1000;
|
|
41
|
-
/** DX-3928 — the one definition of "the same project": the resolved working directory. */
|
|
42
|
-
export function projectKeyOf(cwd) {
|
|
43
|
-
return resolve(cwd);
|
|
44
|
-
}
|
|
45
38
|
export function sessionConnectionDir(home = homedir()) {
|
|
46
39
|
return join(home, ".config", "danxbot", "plan-sessions");
|
|
47
40
|
}
|
|
@@ -147,7 +140,6 @@ export function recordSessionConnectionAfterConnect(result, inputs, write = writ
|
|
|
147
140
|
try {
|
|
148
141
|
write({
|
|
149
142
|
sessionId: inputs.sessionId,
|
|
150
|
-
projectKey: inputs.projectKey,
|
|
151
143
|
dashboardUrl: inputs.dashboardUrl,
|
|
152
144
|
source: inputs.source,
|
|
153
145
|
fingerprint: inputs.fingerprint,
|
|
@@ -167,10 +159,9 @@ export function invalidRecordReason(value) {
|
|
|
167
159
|
return "it is not an object";
|
|
168
160
|
const r = value;
|
|
169
161
|
if (r.schemaVersion !== SESSION_CONNECTION_SCHEMA_VERSION) {
|
|
170
|
-
return
|
|
171
|
-
"a session connected by an older MCP server wrote it; call plan_connect again to rewrite it");
|
|
162
|
+
return `schemaVersion is ${JSON.stringify(r.schemaVersion)}, not ${SESSION_CONNECTION_SCHEMA_VERSION}`;
|
|
172
163
|
}
|
|
173
|
-
for (const field of ["sessionId", "
|
|
164
|
+
for (const field of ["sessionId", "dashboardUrl", "fingerprint", "connectedAt"]) {
|
|
174
165
|
if (typeof r[field] !== "string" || r[field] === "")
|
|
175
166
|
return `${field} is not a non-empty string`;
|
|
176
167
|
}
|
|
@@ -183,47 +174,3 @@ export function invalidRecordReason(value) {
|
|
|
183
174
|
return typeof source.name === "string" && source.name !== "" ? null : "source.name is not a variable name";
|
|
184
175
|
return `source.kind is ${JSON.stringify(source.kind)}, which this version does not know`;
|
|
185
176
|
}
|
|
186
|
-
/**
|
|
187
|
-
* DX-3928 — the record of the session that ran in `projectKey` before
|
|
188
|
-
* `currentSessionId`: the newest record for that project, written by another
|
|
189
|
-
* session, within `STALE_RECORD_MS`. `null` when there is none.
|
|
190
|
-
*
|
|
191
|
-
* A record that cannot be understood (another schema version, not JSON) is
|
|
192
|
-
* SKIPPED here and nowhere else: this answers "was a plan being worked in this
|
|
193
|
-
* project", and a record from an older server cannot say. Every consumer of a
|
|
194
|
-
* session's OWN record (`readSessionConnection`) still fails loud.
|
|
195
|
-
*/
|
|
196
|
-
export function findPredecessorConnection(projectKey, currentSessionId, options = {}) {
|
|
197
|
-
const dir = sessionConnectionDir(options.home ?? homedir());
|
|
198
|
-
const now = options.now ?? Date.now();
|
|
199
|
-
let names;
|
|
200
|
-
try {
|
|
201
|
-
names = readdirSync(dir);
|
|
202
|
-
}
|
|
203
|
-
catch {
|
|
204
|
-
return null; // nothing written yet
|
|
205
|
-
}
|
|
206
|
-
let newest = null;
|
|
207
|
-
for (const name of names) {
|
|
208
|
-
if (!name.endsWith(".json"))
|
|
209
|
-
continue;
|
|
210
|
-
let parsed;
|
|
211
|
-
try {
|
|
212
|
-
parsed = JSON.parse(readFileSync(join(dir, name), "utf-8"));
|
|
213
|
-
}
|
|
214
|
-
catch {
|
|
215
|
-
continue;
|
|
216
|
-
}
|
|
217
|
-
if (invalidRecordReason(parsed) !== null)
|
|
218
|
-
continue;
|
|
219
|
-
const record = parsed;
|
|
220
|
-
if (record.projectKey !== projectKey || record.sessionId === currentSessionId)
|
|
221
|
-
continue;
|
|
222
|
-
const connectedAt = Date.parse(record.connectedAt);
|
|
223
|
-
if (!Number.isFinite(connectedAt) || now - connectedAt > STALE_RECORD_MS)
|
|
224
|
-
continue;
|
|
225
|
-
if (newest === null || connectedAt > Date.parse(newest.connectedAt))
|
|
226
|
-
newest = record;
|
|
227
|
-
}
|
|
228
|
-
return newest;
|
|
229
|
-
}
|
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.205",
|
|
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": {
|
package/dist/restart-notice.js
DELETED
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `danx-dashboard-mcp restart-notice` — DX-3928. The ONE-SHOT client the
|
|
3
|
-
* claude-plugins `danxbot` plugin's SessionStart hook runs for a session that is
|
|
4
|
-
* NOT plan-connected, to tell it when the previous session of the same project
|
|
5
|
-
* was working a plan.
|
|
6
|
-
*
|
|
7
|
-
* WHY IT EXISTS. A restarted Claude Code session gets a NEW session id and no
|
|
8
|
-
* plan binding, and nothing told it (or the operator) that the plan it had been
|
|
9
|
-
* working was unattended: six operator answers sat unread for an hour on
|
|
10
|
-
* 2026-10-01. The session's own connection record cannot help (it is the new
|
|
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 (`findPredecessorConnection`).
|
|
13
|
-
*
|
|
14
|
-
* THE CONTRACT, which `event-hook.sh` reads by exit code:
|
|
15
|
-
* - exit 0, text on stdout: say this to the agent (the registry's
|
|
16
|
-
* `plan_restart.waiting_events` text — this module carries no wording).
|
|
17
|
-
* - exit 0, nothing on stdout: nothing to say — no earlier session of this
|
|
18
|
-
* project connected a plan, or the plan is archived/deleted, or the record is
|
|
19
|
-
* stale. The plugin stays silent, as PLN-11 R-10 requires of a project that
|
|
20
|
-
* never connected a plan.
|
|
21
|
-
* - exit 1, reason on stderr: the lookup itself failed (credential, network,
|
|
22
|
-
* non-2xx, bad shape). Never silent: the hook prints a could-not-load notice.
|
|
23
|
-
*
|
|
24
|
-
* The credential is the PREDECESSOR's own record's source, resolved through the
|
|
25
|
-
* same resolver the bridge uses, so the lookup reaches the dashboard that
|
|
26
|
-
* connected it. READ-ONLY: it advances no cursor on the server.
|
|
27
|
-
*/
|
|
28
|
-
import { SESSION_ID_HEADER, resolveBridgeOptions } from "./bridge.js";
|
|
29
|
-
import { findPredecessorConnection, projectKeyOf } from "./session-connection.js";
|
|
30
|
-
export const RESTART_NOTICE_SUBCOMMAND = "restart-notice";
|
|
31
|
-
export const RESTART_NOTICE_REQUEST_TIMEOUT_MS = 5_000;
|
|
32
|
-
/** The registry row whose text is the notice (`src/issues/reminders/seed.yaml`). */
|
|
33
|
-
export const RESTART_NOTICE_REMINDER_KEY = "plan_restart.waiting_events";
|
|
34
|
-
const USAGE = `usage: CLAUDE_CODE_SESSION_ID=<session-id> danx-dashboard-mcp ${RESTART_NOTICE_SUBCOMMAND} [--cwd <project directory>]`;
|
|
35
|
-
/** One authenticated GET with a hard timeout, answering the notice text or `null`. */
|
|
36
|
-
export async function fetchRestartNotice(target, deps) {
|
|
37
|
-
const controller = new AbortController();
|
|
38
|
-
const timer = setTimeout(() => controller.abort(), deps.requestTimeoutMs);
|
|
39
|
-
let status;
|
|
40
|
-
let text;
|
|
41
|
-
try {
|
|
42
|
-
try {
|
|
43
|
-
const response = await deps.fetch(`${target.dashboardUrl}/api/plan-sessions/${encodeURIComponent(target.predecessorId)}/restart-notice`, {
|
|
44
|
-
method: "GET",
|
|
45
|
-
headers: {
|
|
46
|
-
Authorization: `Bearer ${target.token}`,
|
|
47
|
-
Accept: "application/json",
|
|
48
|
-
[SESSION_ID_HEADER]: target.askingSessionId,
|
|
49
|
-
},
|
|
50
|
-
signal: controller.signal,
|
|
51
|
-
});
|
|
52
|
-
status = response.status;
|
|
53
|
-
text = await response.text();
|
|
54
|
-
}
|
|
55
|
-
catch {
|
|
56
|
-
return { ok: false, reason: controller.signal.aborted ? "timeout" : "request_failed" };
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
finally {
|
|
60
|
-
clearTimeout(timer);
|
|
61
|
-
}
|
|
62
|
-
if (status === 401 || status === 403)
|
|
63
|
-
return { ok: false, reason: "unauthorized" };
|
|
64
|
-
if (status === 404)
|
|
65
|
-
return { ok: false, reason: "not_found" };
|
|
66
|
-
if (status < 200 || status >= 300)
|
|
67
|
-
return { ok: false, reason: "http_error" };
|
|
68
|
-
let body;
|
|
69
|
-
try {
|
|
70
|
-
body = JSON.parse(text);
|
|
71
|
-
}
|
|
72
|
-
catch {
|
|
73
|
-
return { ok: false, reason: "bad_response" };
|
|
74
|
-
}
|
|
75
|
-
if (typeof body !== "object" || body === null || !("restart" in body))
|
|
76
|
-
return { ok: false, reason: "bad_response" };
|
|
77
|
-
if (body.restart === null)
|
|
78
|
-
return { ok: true, text: null };
|
|
79
|
-
// A notice exists, so the registry row MUST have answered with its text: a
|
|
80
|
-
// missing one is a server/registry fault, reported rather than papered over.
|
|
81
|
-
const reminders = Array.isArray(body.reminders) ? body.reminders : [];
|
|
82
|
-
const notice = reminders.find((r) => r.key === RESTART_NOTICE_REMINDER_KEY);
|
|
83
|
-
if (typeof notice?.text !== "string" || notice.text === "")
|
|
84
|
-
return { ok: false, reason: "bad_response" };
|
|
85
|
-
return { ok: true, text: notice.text };
|
|
86
|
-
}
|
|
87
|
-
function parseCwd(argv) {
|
|
88
|
-
if (argv.length === 0)
|
|
89
|
-
return process.cwd();
|
|
90
|
-
if (argv.length === 2 && argv[0] === "--cwd" && argv[1] !== "")
|
|
91
|
-
return argv[1];
|
|
92
|
-
return "invalid";
|
|
93
|
-
}
|
|
94
|
-
/** The bin's `restart-notice` subcommand, wired to the real process. */
|
|
95
|
-
export async function runRestartNoticeCommand(argv, env = process.env,
|
|
96
|
-
/** Test seam: the home the connection records are read from. */
|
|
97
|
-
resolveFrom = {}) {
|
|
98
|
-
const cwd = parseCwd(argv);
|
|
99
|
-
if (cwd === "invalid") {
|
|
100
|
-
process.stderr.write(`bad arguments\n${USAGE}\n`);
|
|
101
|
-
return 1;
|
|
102
|
-
}
|
|
103
|
-
const sessionId = env.CLAUDE_CODE_SESSION_ID;
|
|
104
|
-
if (!sessionId) {
|
|
105
|
-
process.stderr.write(`no_session_id: CLAUDE_CODE_SESSION_ID is not set\n${USAGE}\n`);
|
|
106
|
-
return 1;
|
|
107
|
-
}
|
|
108
|
-
let predecessor;
|
|
109
|
-
try {
|
|
110
|
-
predecessor = findPredecessorConnection(projectKeyOf(cwd), sessionId, resolveFrom);
|
|
111
|
-
}
|
|
112
|
-
catch (err) {
|
|
113
|
-
process.stderr.write(`unreadable_records: ${err.message}\n`);
|
|
114
|
-
return 1;
|
|
115
|
-
}
|
|
116
|
-
if (predecessor === null)
|
|
117
|
-
return 0;
|
|
118
|
-
let options;
|
|
119
|
-
try {
|
|
120
|
-
options = resolveBridgeOptions({ sessionId: predecessor.sessionId, resumeIds: [] }, env, resolveFrom);
|
|
121
|
-
}
|
|
122
|
-
catch (err) {
|
|
123
|
-
const start = err;
|
|
124
|
-
process.stderr.write(`${start.reason}: ${start.message}. Fix: ${start.fix}.\n`);
|
|
125
|
-
return 1;
|
|
126
|
-
}
|
|
127
|
-
const output = await fetchRestartNotice({
|
|
128
|
-
dashboardUrl: options.dashboardUrl,
|
|
129
|
-
token: options.token,
|
|
130
|
-
predecessorId: predecessor.sessionId,
|
|
131
|
-
askingSessionId: sessionId,
|
|
132
|
-
}, { fetch: (input, init) => fetch(input, init), requestTimeoutMs: RESTART_NOTICE_REQUEST_TIMEOUT_MS });
|
|
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;
|
|
136
|
-
}
|
|
137
|
-
if (output.text !== null)
|
|
138
|
-
process.stdout.write(output.text);
|
|
139
|
-
return 0;
|
|
140
|
-
}
|
package/dist/terms-halt.js
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
const TERMS_ACCEPTANCE_PATH = "/api/account/terms-acceptance";
|
|
2
|
-
// Mirrors `termsAcceptUrl("/")` in src/lib/terms-accept-path.ts (a published package cannot import it);
|
|
3
|
-
// terms-gate.integration.test.ts pins the server's link to the same literal. Only meaningful for an
|
|
4
|
-
// operator session: a dispatch's DANXBOT_DASHBOARD_URL is internal, and agent credentials are never terms-gated.
|
|
5
|
-
const ACCEPT_SCREEN_PATH = "/terms/accept?return=%2F";
|
|
6
|
-
/** Keep in step with `termsRefusalInstruction` in src/dashboard/route-policy.ts. */
|
|
7
|
-
export function haltText(acceptUrl) {
|
|
8
|
-
return ("STOP ALL WORK NOW. The dashboard refuses your access until the person you act for accepts the " +
|
|
9
|
-
"current Terms of Use.\n\n" +
|
|
10
|
-
`Give the person this link to accept them: ${acceptUrl}\n\n` +
|
|
11
|
-
"Do not retry, do not look for another route, and never accept the Terms yourself. " +
|
|
12
|
-
"A dispatched agent ends with danxbot_complete({status: \"failed\", summary}) naming this link, " +
|
|
13
|
-
"and never blocks the card.");
|
|
14
|
-
}
|
|
15
|
-
/** The halt text for a `403 terms_not_accepted` result, or null for any other result. */
|
|
16
|
-
export function termsHaltFor(result) {
|
|
17
|
-
if (result.ok || result.status !== 403)
|
|
18
|
-
return null;
|
|
19
|
-
const body = result.body;
|
|
20
|
-
if (body?.error !== "terms_not_accepted")
|
|
21
|
-
return null;
|
|
22
|
-
if (typeof body.acceptUrl !== "string") {
|
|
23
|
-
throw new Error("[danx-dashboard-mcp] a terms_not_accepted refusal carried no acceptUrl");
|
|
24
|
-
}
|
|
25
|
-
return haltText(body.acceptUrl);
|
|
26
|
-
}
|
|
27
|
-
/** The refusal for a call that would accept the Terms (never an agent's act), or null for any other call. */
|
|
28
|
-
export function termsAcceptanceCallHalt(args, dashboardUrl) {
|
|
29
|
-
const path = args.path.split("?")[0].replace(/\/+$/, "");
|
|
30
|
-
if (args.method !== "POST" || path !== TERMS_ACCEPTANCE_PATH)
|
|
31
|
-
return null;
|
|
32
|
-
return haltText(`${dashboardUrl.replace(/\/+$/, "")}${ACCEPT_SCREEN_PATH}`);
|
|
33
|
-
}
|