stuntdouble 0.1.2 → 0.1.3
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 +7 -4
- package/dist/{chunk-JAB46QV7.js → chunk-7IC2G3Z3.js} +298 -9
- package/dist/chunk-7IC2G3Z3.js.map +1 -0
- package/dist/chunk-JXTUCMO2.js +77 -0
- package/dist/chunk-JXTUCMO2.js.map +1 -0
- package/dist/cli.js +18 -63
- package/dist/cli.js.map +1 -1
- package/dist/doctor-7PW2QD3M.js +12 -0
- package/dist/{mcp-CYPMT37O.js → mcp-MOXTYXZU.js} +122 -21
- package/dist/mcp-MOXTYXZU.js.map +1 -0
- package/dist/ui-server-ZUJ5WJ7Q.js +9 -0
- package/dist/ui-server-ZUJ5WJ7Q.js.map +1 -0
- package/dist/ui.html +112 -49
- package/package.json +1 -1
- package/dist/chunk-JAB46QV7.js.map +0 -1
- package/dist/chunk-VWTYG7G4.js +0 -238
- package/dist/chunk-VWTYG7G4.js.map +0 -1
- package/dist/mcp-CYPMT37O.js.map +0 -1
- package/dist/ui-server-AYIR7DTN.js +0 -10
- /package/dist/{ui-server-AYIR7DTN.js.map → doctor-7PW2QD3M.js.map} +0 -0
|
@@ -1,24 +1,25 @@
|
|
|
1
|
-
import {
|
|
2
|
-
startUi
|
|
3
|
-
} from "./chunk-VWTYG7G4.js";
|
|
4
1
|
import {
|
|
5
2
|
ActorSchema,
|
|
6
3
|
ChecklistSchema,
|
|
7
4
|
ConfigSchema,
|
|
8
5
|
appendJsonl,
|
|
9
6
|
describeRunScore,
|
|
10
|
-
|
|
7
|
+
effectiveStatus,
|
|
8
|
+
isLive,
|
|
11
9
|
newChecklistScoring,
|
|
12
10
|
projectDir,
|
|
13
11
|
readJson,
|
|
14
12
|
readRuns,
|
|
15
13
|
resolveHaltedChecks,
|
|
16
14
|
resolveSkippedChecks,
|
|
15
|
+
runningMarker,
|
|
17
16
|
scoreChecklistRun,
|
|
18
17
|
sdDir,
|
|
18
|
+
startUi,
|
|
19
|
+
tallyRun,
|
|
19
20
|
writeFileAtomic,
|
|
20
21
|
writeJsonAtomic
|
|
21
|
-
} from "./chunk-
|
|
22
|
+
} from "./chunk-7IC2G3Z3.js";
|
|
22
23
|
|
|
23
24
|
// src/mcp.ts
|
|
24
25
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
@@ -1420,17 +1421,19 @@ ${where}` : where,
|
|
|
1420
1421
|
};
|
|
1421
1422
|
|
|
1422
1423
|
// src/mcp.ts
|
|
1424
|
+
import { dirname } from "path";
|
|
1423
1425
|
import { z } from "zod";
|
|
1424
1426
|
|
|
1425
1427
|
// src/run.ts
|
|
1426
|
-
import {
|
|
1428
|
+
import { createHash } from "crypto";
|
|
1429
|
+
import { mkdir, mkdtemp, readdir, readFile, rm, stat } from "fs/promises";
|
|
1427
1430
|
import { homedir, tmpdir } from "os";
|
|
1428
1431
|
import { join } from "path";
|
|
1429
1432
|
|
|
1430
1433
|
// package.json
|
|
1431
1434
|
var package_default = {
|
|
1432
1435
|
name: "stuntdouble",
|
|
1433
|
-
version: "0.1.
|
|
1436
|
+
version: "0.1.3",
|
|
1434
1437
|
description: "Stunt Double Local: run Stunt Double checklists in a local Chrome from your own coding agent session, with the runs kept as files and a local viewer",
|
|
1435
1438
|
keywords: [
|
|
1436
1439
|
"stunt-double",
|
|
@@ -1503,7 +1506,7 @@ function checkLine(check) {
|
|
|
1503
1506
|
function buildBrief(input) {
|
|
1504
1507
|
const { actor, checklist, url, checks, resumed } = input;
|
|
1505
1508
|
const persona = [
|
|
1506
|
-
`You are ${actor.name}${actor.description ? `: ${actor.description}` : ""}.`,
|
|
1509
|
+
`You are ${actor.name}${actor.description ? `: ${actor.description.replace(/[.\s]+$/, "")}` : ""}.`,
|
|
1507
1510
|
actor.system_prompt ?? ""
|
|
1508
1511
|
].filter(Boolean).join("\n");
|
|
1509
1512
|
const knowledge = actor.knowledge.length ? [
|
|
@@ -1522,6 +1525,7 @@ ${checks.map(checkLine).join("\n")}`,
|
|
|
1522
1525
|
"How to work:",
|
|
1523
1526
|
"- Stay in persona: act and judge as this person would, not as a tester.",
|
|
1524
1527
|
"- Read the page with read_page and find. Ask for a screenshot only when how something looks is the question.",
|
|
1528
|
+
'- get_page_text and read_page "all" return up to 20,000 characters: keep them for long text such as a policy.',
|
|
1525
1529
|
"- Judge each check with evaluate_check as soon as you have observed it, while the evidence is on screen.",
|
|
1526
1530
|
"- A passed verdict needs a locator: text visible on the page, or a CSS selector, that shows the evidence. A failure may describe something missing.",
|
|
1527
1531
|
"- Cite only what you saw in this run. Never pass a check on what a page probably does.",
|
|
@@ -1698,6 +1702,12 @@ var PlaywrightDriver = class _PlaywrightDriver {
|
|
|
1698
1702
|
channel: "chrome"
|
|
1699
1703
|
});
|
|
1700
1704
|
} catch (chromeError) {
|
|
1705
|
+
if (/ProcessSingleton|SingletonLock|already in use/i.test(chromeError.message)) {
|
|
1706
|
+
throw new Error(
|
|
1707
|
+
"Another run is already using this actor's kept login (persist_login). Finish that run first, or try again when it ends.",
|
|
1708
|
+
{ cause: chromeError }
|
|
1709
|
+
);
|
|
1710
|
+
}
|
|
1701
1711
|
try {
|
|
1702
1712
|
context = await chromium.launchPersistentContext(options.profileDir, settings);
|
|
1703
1713
|
} catch {
|
|
@@ -1746,6 +1756,7 @@ var PlaywrightDriver = class _PlaywrightDriver {
|
|
|
1746
1756
|
var MAX_LOCATOR_TEXT = 120;
|
|
1747
1757
|
var MAX_LOCATOR_SELECTOR = 400;
|
|
1748
1758
|
var SETTLE_MS2 = 350;
|
|
1759
|
+
var FIND_TIMEOUT_MS = 5e3;
|
|
1749
1760
|
function cleanLocator(locator) {
|
|
1750
1761
|
const clean = (value) => value?.replace(/\s+/g, " ").trim() || null;
|
|
1751
1762
|
const selector = clean(locator?.selector);
|
|
@@ -1754,9 +1765,13 @@ function cleanLocator(locator) {
|
|
|
1754
1765
|
text: clean(locator?.text)?.slice(0, MAX_LOCATOR_TEXT) ?? null
|
|
1755
1766
|
};
|
|
1756
1767
|
}
|
|
1757
|
-
async function focusEvidence(page, query) {
|
|
1768
|
+
async function focusEvidence(page, query, timeoutMs = FIND_TIMEOUT_MS) {
|
|
1758
1769
|
if (!query.selector && !query.text) return null;
|
|
1759
|
-
|
|
1770
|
+
let timer;
|
|
1771
|
+
const via = await Promise.race([
|
|
1772
|
+
page.evaluate(findAndScroll, query),
|
|
1773
|
+
new Promise((ok) => timer = setTimeout(() => ok(null), timeoutMs))
|
|
1774
|
+
]).catch(() => null).finally(() => clearTimeout(timer));
|
|
1760
1775
|
if (via) await page.waitForTimeout(SETTLE_MS2).catch(() => {
|
|
1761
1776
|
});
|
|
1762
1777
|
return via;
|
|
@@ -1821,6 +1836,7 @@ var PACKAGE = `${package_default.name}@${package_default.version}`;
|
|
|
1821
1836
|
var STOPPED = "This run was stopped from the viewer. Do not use the browser again: call finish_run now with a summary of what you found.";
|
|
1822
1837
|
var NOT_JUDGED = "Not judged before the run finished.";
|
|
1823
1838
|
var PRIVATE_REFUSED = "Local and private addresses other than the app under test are not reachable.";
|
|
1839
|
+
var SETUP = "Run /stuntdouble setup to create one.";
|
|
1824
1840
|
var LOG_TEXT_CHARS = 500;
|
|
1825
1841
|
var PRESETS = {
|
|
1826
1842
|
desktop: { width: 1280, height: 720, touch: false },
|
|
@@ -1837,11 +1853,21 @@ function stamp(date = /* @__PURE__ */ new Date()) {
|
|
|
1837
1853
|
return `${date.getFullYear()}${p(date.getMonth() + 1)}${p(date.getDate())}-${p(date.getHours())}${p(date.getMinutes())}${p(date.getSeconds())}`;
|
|
1838
1854
|
}
|
|
1839
1855
|
var now = () => (/* @__PURE__ */ new Date()).toISOString();
|
|
1856
|
+
function ago(iso) {
|
|
1857
|
+
if (!iso) return "not started";
|
|
1858
|
+
const minutes = Math.round((Date.now() - Date.parse(iso)) / 6e4);
|
|
1859
|
+
if (minutes < 1) return "just now";
|
|
1860
|
+
if (minutes < 60) return `${minutes} min ago`;
|
|
1861
|
+
const hours = Math.round(minutes / 60);
|
|
1862
|
+
return hours < 48 ? `${hours} h ago` : `${Math.round(hours / 24)} days ago`;
|
|
1863
|
+
}
|
|
1840
1864
|
var LocalSession = class {
|
|
1841
1865
|
root;
|
|
1842
1866
|
launch;
|
|
1843
1867
|
active = null;
|
|
1844
1868
|
ui = null;
|
|
1869
|
+
/** The run this session last finished, so a second finish_run is answered, not refused. */
|
|
1870
|
+
finished = null;
|
|
1845
1871
|
constructor(options) {
|
|
1846
1872
|
this.root = sdDir(options.project);
|
|
1847
1873
|
this.launch = options.launch ?? PlaywrightDriver.launch;
|
|
@@ -1856,6 +1882,28 @@ var LocalSession = class {
|
|
|
1856
1882
|
});
|
|
1857
1883
|
return this.ui.link(path);
|
|
1858
1884
|
}
|
|
1885
|
+
/** The viewer's port while it is running in this process. */
|
|
1886
|
+
get viewerPort() {
|
|
1887
|
+
return this.ui?.port ?? null;
|
|
1888
|
+
}
|
|
1889
|
+
/** open_ui: the link, and a line on each of the latest runs. */
|
|
1890
|
+
async overview() {
|
|
1891
|
+
const link = await this.openUi();
|
|
1892
|
+
const runs = (await readRuns(this.root)).slice(0, 5);
|
|
1893
|
+
if (runs.length === 0) return `${link}
|
|
1894
|
+
|
|
1895
|
+
No runs yet.`;
|
|
1896
|
+
const lines = runs.map((e) => {
|
|
1897
|
+
if (!("run" in e)) return `- ${e.id}: unreadable (${e.error.split("\n")[0]})`;
|
|
1898
|
+
const { run } = e;
|
|
1899
|
+
const status = effectiveStatus(this.root, run);
|
|
1900
|
+
const t = tallyRun(run);
|
|
1901
|
+
const outcome = run.score ? `${status}, ${run.score.outcome}` : status;
|
|
1902
|
+
return `- ${run.checklist}: ${outcome}, ${t.passed}/${t.total} checks passed, started ${ago(run.started_at)} (${run.id})`;
|
|
1903
|
+
});
|
|
1904
|
+
return [link, `Latest runs, newest first:
|
|
1905
|
+
${lines.join("\n")}`].join("\n\n");
|
|
1906
|
+
}
|
|
1859
1907
|
async startRun(input) {
|
|
1860
1908
|
if (this.active) return this.resumeActive(input);
|
|
1861
1909
|
if (!/^[a-z0-9][a-z0-9-]*$/.test(input.checklist)) {
|
|
@@ -1865,12 +1913,20 @@ var LocalSession = class {
|
|
|
1865
1913
|
join(this.root, "stuntdouble.json"),
|
|
1866
1914
|
ConfigSchema,
|
|
1867
1915
|
".stuntdouble/stuntdouble.json"
|
|
1868
|
-
)
|
|
1916
|
+
).catch((err) => {
|
|
1917
|
+
throw /file not found$/.test(err.message) ? new Error(`${err.message}. ${SETUP}`) : err;
|
|
1918
|
+
});
|
|
1869
1919
|
let checklist = await readJson(
|
|
1870
1920
|
join(this.root, "checklists", `${input.checklist}.json`),
|
|
1871
1921
|
ChecklistSchema,
|
|
1872
1922
|
`.stuntdouble/checklists/${input.checklist}.json`
|
|
1873
|
-
)
|
|
1923
|
+
).catch(async (err) => {
|
|
1924
|
+
if (!/file not found$/.test(err.message)) throw err;
|
|
1925
|
+
const slugs = (await readdir(join(this.root, "checklists")).catch(() => [])).filter((f) => f.endsWith(".json")).map((f) => f.slice(0, -".json".length));
|
|
1926
|
+
throw new Error(
|
|
1927
|
+
`${err.message}. ${slugs.length ? `The checklists here are: ${slugs.join(", ")}.` : SETUP}`
|
|
1928
|
+
);
|
|
1929
|
+
});
|
|
1874
1930
|
const actor = await readJson(
|
|
1875
1931
|
join(this.root, "actors", `${checklist.actor}.json`),
|
|
1876
1932
|
ActorSchema,
|
|
@@ -1932,7 +1988,8 @@ var LocalSession = class {
|
|
|
1932
1988
|
checkNavigation: refuseNavigation
|
|
1933
1989
|
};
|
|
1934
1990
|
const tempProfile = actor.persist_login ? null : await mkdtemp(join(tmpdir(), "stuntdouble-profile-"));
|
|
1935
|
-
const
|
|
1991
|
+
const project = createHash("sha256").update(this.root).digest("hex").slice(0, 12);
|
|
1992
|
+
const profileDir = tempProfile ?? join(homedir(), ".stuntdouble", "profiles", project, checklist.actor);
|
|
1936
1993
|
await mkdir(profileDir, { recursive: true });
|
|
1937
1994
|
let driver;
|
|
1938
1995
|
try {
|
|
@@ -1977,13 +2034,16 @@ var LocalSession = class {
|
|
|
1977
2034
|
everyScreenshot: config.model_screenshots === "every_action",
|
|
1978
2035
|
stopped: false
|
|
1979
2036
|
};
|
|
2037
|
+
this.finished = null;
|
|
1980
2038
|
await this.save();
|
|
2039
|
+
await this.markRunning();
|
|
1981
2040
|
await this.log("start_run", input, input.resume ? "Resumed" : "Started");
|
|
1982
2041
|
const opened = await this.browserTool("navigate", { url: run.url });
|
|
1983
2042
|
const pending = this.pending();
|
|
1984
2043
|
const viewer = await this.openUi(`/runs/${run.id}`);
|
|
1985
2044
|
return [
|
|
1986
2045
|
`Run ${run.id} ${input.resume ? "resumed" : "started"}. Watch it or stop it at ${viewer}`,
|
|
2046
|
+
unweightedNote(checklist),
|
|
1987
2047
|
buildBrief({
|
|
1988
2048
|
actor,
|
|
1989
2049
|
checklist,
|
|
@@ -1991,10 +2051,12 @@ var LocalSession = class {
|
|
|
1991
2051
|
checks: pending,
|
|
1992
2052
|
resumed: Boolean(input.resume)
|
|
1993
2053
|
}),
|
|
2054
|
+
// Not the navigate result: it carries the page title, and this reply is
|
|
2055
|
+
// read by the main session, which never reads page content.
|
|
1994
2056
|
opened.isError ? `The start page did not load: ${opened.text}
|
|
1995
|
-
Try navigate again, or judge the checks you cannot reach as "blocked".` :
|
|
2057
|
+
Try navigate again, or judge the checks you cannot reach as "blocked".` : `The browser is open at ${run.url}: read the page with read_page before acting.`,
|
|
1996
2058
|
this.nextStep()
|
|
1997
|
-
].join("\n\n");
|
|
2059
|
+
].filter(Boolean).join("\n\n");
|
|
1998
2060
|
}
|
|
1999
2061
|
/** One of the browser toolset's direct tools, on the active run's browser. */
|
|
2000
2062
|
async browserTool(name, input) {
|
|
@@ -2002,7 +2064,7 @@ Try navigate again, or judge the checks you cannot reach as "blocked".` : opened
|
|
|
2002
2064
|
if (await this.stopRequested(active)) return { text: STOPPED, isError: true };
|
|
2003
2065
|
const page = await active.driver.context.activePage();
|
|
2004
2066
|
const host = page ? URL.parse(await page.url())?.hostname ?? "" : "";
|
|
2005
|
-
const tools = active.guarded && host
|
|
2067
|
+
const tools = active.guarded && !isTrustedHost(host, active.trustedHosts) ? active.guarded : active.trusted;
|
|
2006
2068
|
const result = await tools.run(name, input);
|
|
2007
2069
|
const refusal = active.driver.takeRefusal();
|
|
2008
2070
|
if (refusal) {
|
|
@@ -2017,6 +2079,7 @@ Try navigate again, or judge the checks you cannot reach as "blocked".` : opened
|
|
|
2017
2079
|
return { text: result.text, ...image2 ? { image: image2 } : {}, isError: Boolean(result.error) };
|
|
2018
2080
|
}
|
|
2019
2081
|
async evaluateCheck(input) {
|
|
2082
|
+
if (!this.active && this.finished) return this.alreadyFinished();
|
|
2020
2083
|
const active = this.requireActive();
|
|
2021
2084
|
if (await this.stopRequested(active)) throw new Error(STOPPED);
|
|
2022
2085
|
const { run, checklist } = active;
|
|
@@ -2095,6 +2158,7 @@ Try navigate again, or judge the checks you cannot reach as "blocked".` : opened
|
|
|
2095
2158
|
return [recorded, ...notes, this.nextStep()].join("\n");
|
|
2096
2159
|
}
|
|
2097
2160
|
async finishRun(input) {
|
|
2161
|
+
if (!this.active && this.finished) return this.alreadyFinished();
|
|
2098
2162
|
const active = this.requireActive();
|
|
2099
2163
|
const stopped = await this.stopRequested(active);
|
|
2100
2164
|
const { run, checklist } = active;
|
|
@@ -2131,8 +2195,11 @@ Try navigate again, or judge the checks you cannot reach as "blocked".` : opened
|
|
|
2131
2195
|
passedCount: run.results.filter((r) => r.status === "passed").length,
|
|
2132
2196
|
totalCount: checklist.checks.length
|
|
2133
2197
|
});
|
|
2198
|
+
const t = tallyRun(run);
|
|
2199
|
+
const tally = score.weighted ? ` ${t.passed}/${t.total} checks passed.` : "";
|
|
2200
|
+
this.finished = { id: run.id, report: viewer };
|
|
2134
2201
|
return [
|
|
2135
|
-
`Run ${run.id} ${run.status}: ${score.outcome}. ${counts}. Self-judged: the actor recorded its own verdicts.`,
|
|
2202
|
+
`Run ${run.id} ${run.status}: ${score.outcome}. ${counts}.${tally} ${score.weighted ? "" : `${unweightedNote(checklist, true)} `}Self-judged: the actor recorded its own verdicts.`,
|
|
2136
2203
|
failed.length ? `Failed:
|
|
2137
2204
|
${failed.map(line).join("\n")}` : "",
|
|
2138
2205
|
blocked.length ? `Not judged:
|
|
@@ -2156,6 +2223,7 @@ ${blocked.map(line).join("\n")}` : "",
|
|
|
2156
2223
|
run.status = "running";
|
|
2157
2224
|
await this.clearStop(run.id);
|
|
2158
2225
|
await this.save();
|
|
2226
|
+
await this.markRunning();
|
|
2159
2227
|
await this.log("start_run", input, "Resumed");
|
|
2160
2228
|
const viewer = await this.openUi(`/runs/${run.id}`);
|
|
2161
2229
|
return [
|
|
@@ -2182,6 +2250,10 @@ ${blocked.map(line).join("\n")}` : "",
|
|
|
2182
2250
|
await this.ui?.close();
|
|
2183
2251
|
this.ui = null;
|
|
2184
2252
|
}
|
|
2253
|
+
alreadyFinished() {
|
|
2254
|
+
const { id, report } = this.finished;
|
|
2255
|
+
return `Run ${id} has already finished: its report is at ${report}. Start another with start_run.`;
|
|
2256
|
+
}
|
|
2185
2257
|
requireActive() {
|
|
2186
2258
|
if (!this.active) throw new Error("No run is going: call start_run first.");
|
|
2187
2259
|
return this.active;
|
|
@@ -2224,7 +2296,7 @@ ${blocked.map(line).join("\n")}` : "",
|
|
|
2224
2296
|
async markInterrupted() {
|
|
2225
2297
|
for (const entry of await readRuns(this.root)) {
|
|
2226
2298
|
if (!("run" in entry) || entry.run.status !== "running") continue;
|
|
2227
|
-
if (entry.run.pid !== process.pid &&
|
|
2299
|
+
if (entry.run.pid !== process.pid && isLive(this.root, entry.run)) continue;
|
|
2228
2300
|
await writeJsonAtomic(join(this.root, "runs", entry.id, "run.json"), {
|
|
2229
2301
|
...entry.run,
|
|
2230
2302
|
status: "interrupted"
|
|
@@ -2239,6 +2311,11 @@ ${blocked.map(line).join("\n")}` : "",
|
|
|
2239
2311
|
});
|
|
2240
2312
|
if (active.tempProfile) await rm(active.tempProfile, { recursive: true, force: true });
|
|
2241
2313
|
await this.clearStop(active.run.id);
|
|
2314
|
+
await rm(runningMarker(this.root, active.run.id), { force: true });
|
|
2315
|
+
}
|
|
2316
|
+
async markRunning() {
|
|
2317
|
+
const { run } = this.requireActive();
|
|
2318
|
+
await writeFileAtomic(runningMarker(this.root, run.id), String(run.pid));
|
|
2242
2319
|
}
|
|
2243
2320
|
async clearStop(id) {
|
|
2244
2321
|
const stopFile = join(this.root, ".live", "stop");
|
|
@@ -2264,6 +2341,15 @@ ${blocked.map(line).join("\n")}` : "",
|
|
|
2264
2341
|
});
|
|
2265
2342
|
}
|
|
2266
2343
|
};
|
|
2344
|
+
function isTrustedHost(host, trustedHosts) {
|
|
2345
|
+
return host !== "" && trustedHosts.some((t) => hostMatchesDomain(host, t));
|
|
2346
|
+
}
|
|
2347
|
+
function unweightedNote(checklist, atFinish = false) {
|
|
2348
|
+
if (checklist.checks.some((c) => c.type === "weighted")) return null;
|
|
2349
|
+
const threshold = checklist.pass_threshold == null ? "" : ` Its pass_threshold (${checklist.pass_threshold}) applies to weighted checks only.`;
|
|
2350
|
+
if (!atFinish && !threshold) return null;
|
|
2351
|
+
return `No score: this checklist has no weighted checks, so it passes only when every pass/fail check passes.${threshold}`;
|
|
2352
|
+
}
|
|
2267
2353
|
function scoreLocalRun(checklist, results) {
|
|
2268
2354
|
const scoring = newChecklistScoring({
|
|
2269
2355
|
scoreType: checklist.score_type,
|
|
@@ -2368,9 +2454,24 @@ function createServer(session) {
|
|
|
2368
2454
|
server.registerTool(
|
|
2369
2455
|
"open_ui",
|
|
2370
2456
|
{
|
|
2371
|
-
description: "The local viewer for this project: every run with its verdicts, screenshots and activity, and the checklists and actors. Returns a link to open in a browser."
|
|
2457
|
+
description: "The local viewer for this project: every run with its verdicts, screenshots and activity, and the checklists and actors. Returns a link to open in a browser and a line on each of the latest runs."
|
|
2372
2458
|
},
|
|
2373
|
-
() => attempt(() => session.
|
|
2459
|
+
() => attempt(() => session.overview())
|
|
2460
|
+
);
|
|
2461
|
+
server.registerTool(
|
|
2462
|
+
"doctor",
|
|
2463
|
+
{
|
|
2464
|
+
description: "Check what local runs need: Node, Chrome, the viewer port, this project's stuntdouble.json and how runs are billed. Read only. Returns one line per check, each starting ok, FAIL, note or --.",
|
|
2465
|
+
annotations: { readOnlyHint: true }
|
|
2466
|
+
},
|
|
2467
|
+
() => attempt(async () => {
|
|
2468
|
+
const { doctor } = await import("./doctor-7PW2QD3M.js");
|
|
2469
|
+
const { lines } = await doctor({
|
|
2470
|
+
project: dirname(session.root),
|
|
2471
|
+
viewerPort: session.viewerPort
|
|
2472
|
+
});
|
|
2473
|
+
return lines.join("\n");
|
|
2474
|
+
})
|
|
2374
2475
|
);
|
|
2375
2476
|
return server;
|
|
2376
2477
|
}
|
|
@@ -2388,4 +2489,4 @@ export {
|
|
|
2388
2489
|
createServer,
|
|
2389
2490
|
serveMcp
|
|
2390
2491
|
};
|
|
2391
|
-
//# sourceMappingURL=mcp-
|
|
2492
|
+
//# sourceMappingURL=mcp-MOXTYXZU.js.map
|