stuntdouble 0.1.3 → 0.2.0

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.
Files changed (46) hide show
  1. package/README.md +31 -6
  2. package/dist/chunk-BFI6P5DO.js +278 -0
  3. package/dist/chunk-BFI6P5DO.js.map +1 -0
  4. package/dist/chunk-DFXASIGV.js +431 -0
  5. package/dist/chunk-DFXASIGV.js.map +1 -0
  6. package/dist/{chunk-7IC2G3Z3.js → chunk-F3R3QL7V.js} +19 -252
  7. package/dist/chunk-F3R3QL7V.js.map +1 -0
  8. package/dist/{mcp-MOXTYXZU.js → chunk-FMZTT2MC.js} +72 -1160
  9. package/dist/chunk-FMZTT2MC.js.map +1 -0
  10. package/dist/chunk-MSV5OLSY.js +117 -0
  11. package/dist/chunk-MSV5OLSY.js.map +1 -0
  12. package/dist/chunk-P3CI4FTE.js +469 -0
  13. package/dist/chunk-P3CI4FTE.js.map +1 -0
  14. package/dist/{chunk-JXTUCMO2.js → chunk-QDA5FTUH.js} +5 -3
  15. package/dist/{chunk-JXTUCMO2.js.map → chunk-QDA5FTUH.js.map} +1 -1
  16. package/dist/chunk-XHSDRB3X.js +996 -0
  17. package/dist/chunk-XHSDRB3X.js.map +1 -0
  18. package/dist/cli.js +179 -22
  19. package/dist/cli.js.map +1 -1
  20. package/dist/cloud-DLLNKYND.js +34 -0
  21. package/dist/credentials-QHESDY4K.js +27 -0
  22. package/dist/doctor-ZAYOOCPT.js +13 -0
  23. package/dist/doctor-ZAYOOCPT.js.map +1 -0
  24. package/dist/files-UBPF7PKW.js +45 -0
  25. package/dist/files-UBPF7PKW.js.map +1 -0
  26. package/dist/mcp-HFIAQ6HP.js +133 -0
  27. package/dist/mcp-HFIAQ6HP.js.map +1 -0
  28. package/dist/open-browser-CTOGH5HQ.js +17 -0
  29. package/dist/open-browser-CTOGH5HQ.js.map +1 -0
  30. package/dist/run-MPIAR3LQ.js +20 -0
  31. package/dist/run-MPIAR3LQ.js.map +1 -0
  32. package/dist/runner-E45EJ76B.js +17 -0
  33. package/dist/runner-E45EJ76B.js.map +1 -0
  34. package/dist/studio-FBC66BER.js +360 -0
  35. package/dist/studio-FBC66BER.js.map +1 -0
  36. package/dist/studio.js +1621 -0
  37. package/dist/ui-server-C7CFJWRD.js +12 -0
  38. package/dist/ui-server-C7CFJWRD.js.map +1 -0
  39. package/dist/ui.html +100 -56
  40. package/package.json +2 -2
  41. package/dist/chunk-7IC2G3Z3.js.map +0 -1
  42. package/dist/doctor-7PW2QD3M.js +0 -12
  43. package/dist/mcp-MOXTYXZU.js.map +0 -1
  44. package/dist/ui-server-ZUJ5WJ7Q.js +0 -9
  45. /package/dist/{doctor-7PW2QD3M.js.map → cloud-DLLNKYND.js.map} +0 -0
  46. /package/dist/{ui-server-ZUJ5WJ7Q.js.map → credentials-QHESDY4K.js.map} +0 -0
@@ -0,0 +1,133 @@
1
+ import {
2
+ LocalSession,
3
+ PACKAGE
4
+ } from "./chunk-XHSDRB3X.js";
5
+ import "./chunk-BFI6P5DO.js";
6
+ import {
7
+ DIRECT_TOOL_SPECS
8
+ } from "./chunk-FMZTT2MC.js";
9
+ import {
10
+ projectDir
11
+ } from "./chunk-F3R3QL7V.js";
12
+
13
+ // src/mcp.ts
14
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
15
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
16
+ import { dirname } from "path";
17
+ import { z } from "zod";
18
+ var reply = ({ text, image, isError }) => ({
19
+ content: [
20
+ { type: "text", text },
21
+ ...image ? [{ type: "image", data: image, mimeType: "image/png" }] : []
22
+ ],
23
+ ...isError ? { isError: true } : {}
24
+ });
25
+ var queue = Promise.resolve();
26
+ function attempt(work) {
27
+ const run = queue.then(async () => {
28
+ try {
29
+ const out = await work();
30
+ return reply(typeof out === "string" ? { text: out } : out);
31
+ } catch (err) {
32
+ return reply({ text: err.message, isError: true });
33
+ }
34
+ });
35
+ queue = run;
36
+ return run;
37
+ }
38
+ function shapeOf(spec) {
39
+ const required = new Set(spec.parameters.required ?? []);
40
+ return Object.fromEntries(
41
+ Object.entries(spec.parameters.properties).map(([key, raw]) => {
42
+ const prop = raw;
43
+ let schema = prop.enum ? z.enum(prop.enum) : prop.type === "number" ? z.number() : z.string();
44
+ if (prop.description) schema = schema.describe(prop.description);
45
+ return [key, required.has(key) ? schema : schema.optional()];
46
+ })
47
+ );
48
+ }
49
+ function createServer(session) {
50
+ const server = new McpServer({ name: "stuntdouble", version: PACKAGE.split("@")[1] });
51
+ for (const spec of DIRECT_TOOL_SPECS.filter((s) => s.name !== "done")) {
52
+ server.registerTool(
53
+ spec.name,
54
+ { description: spec.description, inputSchema: shapeOf(spec) },
55
+ (input) => attempt(() => session.browserTool(spec.name, input))
56
+ );
57
+ }
58
+ server.registerTool(
59
+ "start_run",
60
+ {
61
+ description: "Start a local run of a checklist from .stuntdouble/checklists/<checklist>.json: opens a fresh Chrome at the checklist's URL, and returns the viewer link, the actor's brief and the checks to judge. If a file is invalid, returns each field to fix. With resume, continues the run still open in this session, or else the newest interrupted or stopped run of that checklist, judging only the checks without a verdict.",
62
+ inputSchema: {
63
+ checklist: z.string().describe('The checklist slug, e.g. "checkout".'),
64
+ resume: z.boolean().optional().describe("Continue the last unfinished run instead.")
65
+ }
66
+ },
67
+ (input) => attempt(() => session.startRun(input))
68
+ );
69
+ server.registerTool(
70
+ "evaluate_check",
71
+ {
72
+ description: 'Record the verdict on one check, as soon as you have observed it. A passed verdict needs a locator for evidence visible on the page now; a failure may describe something missing. Use "informational" only for informational checks, and "blocked" when you could not reach what the check is about. Saves a screenshot of the evidence and returns the next check.',
73
+ inputSchema: {
74
+ check: z.string().describe('The check key, e.g. "guest-checkout".'),
75
+ status: z.enum(["passed", "failed", "informational", "blocked"]),
76
+ evidence: z.string().min(1).describe("What you observed, in a sentence or two, quoting the page where you can."),
77
+ locator: z.object({
78
+ text: z.string().optional().describe("Distinctive text visible on the page."),
79
+ selector: z.string().optional().describe("A CSS selector for the element.")
80
+ }).optional().describe("Where the evidence is on the page. Required for passed.")
81
+ }
82
+ },
83
+ (input) => attempt(() => session.evaluateCheck(input))
84
+ );
85
+ server.registerTool(
86
+ "finish_run",
87
+ {
88
+ description: "End the run: settles the checks left, scores it the way the cloud does, writes run.json, closes the browser and returns the outcome. Always call it, even when blocked or stopped.",
89
+ inputSchema: {
90
+ summary: z.string().min(1).describe("One or two sentences on what you found.")
91
+ }
92
+ },
93
+ (input) => attempt(() => session.finishRun(input))
94
+ );
95
+ server.registerTool(
96
+ "open_ui",
97
+ {
98
+ 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."
99
+ },
100
+ () => attempt(() => session.overview())
101
+ );
102
+ server.registerTool(
103
+ "doctor",
104
+ {
105
+ 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 --.",
106
+ annotations: { readOnlyHint: true }
107
+ },
108
+ () => attempt(async () => {
109
+ const { doctor } = await import("./doctor-ZAYOOCPT.js");
110
+ const { lines } = await doctor({
111
+ project: dirname(session.root),
112
+ viewerPort: session.viewerPort
113
+ });
114
+ return lines.join("\n");
115
+ })
116
+ );
117
+ return server;
118
+ }
119
+ async function serveMcp() {
120
+ const session = new LocalSession({ project: projectDir() });
121
+ const server = createServer(session);
122
+ let closing;
123
+ const shutdown = () => closing ??= session.close().finally(() => process.exit(0));
124
+ process.stdin.on("close", shutdown);
125
+ process.on("SIGINT", shutdown);
126
+ process.on("SIGTERM", shutdown);
127
+ await server.connect(new StdioServerTransport());
128
+ }
129
+ export {
130
+ createServer,
131
+ serveMcp
132
+ };
133
+ //# sourceMappingURL=mcp-HFIAQ6HP.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/mcp.ts"],"sourcesContent":["// The stdio MCP server: the browser toolset's direct tools plus the run tools,\n// over one `LocalSession`. The person's own agent session is the only model:\n// this process drives a browser and writes files, and calls no model at all.\n\nimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';\nimport type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';\nimport { DIRECT_TOOL_SPECS } from '@stuntkit/browser-toolset/direct-tools';\nimport { dirname } from 'node:path';\nimport { z } from 'zod';\n\nimport { projectDir } from './files.ts';\nimport { LocalSession, PACKAGE, type ToolOutput } from './run.ts';\n\nconst reply = ({ text, image, isError }: ToolOutput): CallToolResult => ({\n content: [\n { type: 'text', text },\n ...(image ? [{ type: 'image' as const, data: image, mimeType: 'image/png' }] : []),\n ],\n ...(isError ? { isError: true } : {}),\n});\n\nlet queue: Promise<unknown> = Promise.resolve();\n\n/**\n * Run a tool, turning a thrown error into a result the model reads and acts on.\n * Calls can arrive at once (two verdicts in one turn) but share one page and\n * one run.json, so they take turns.\n */\nfunction attempt(work: () => Promise<string | ToolOutput>): Promise<CallToolResult> {\n const run = queue.then(async () => {\n try {\n const out = await work();\n return reply(typeof out === 'string' ? { text: out } : out);\n } catch (err) {\n return reply({ text: (err as Error).message, isError: true });\n }\n });\n queue = run;\n return run;\n}\n\ntype PropertySpec = { type: string; enum?: string[]; description?: string };\n\n/** A direct tool's flat JSON Schema as the zod shape the SDK registers. */\nfunction shapeOf(spec: (typeof DIRECT_TOOL_SPECS)[number]): z.ZodRawShape {\n const required = new Set(spec.parameters.required ?? []);\n return Object.fromEntries(\n Object.entries(spec.parameters.properties).map(([key, raw]) => {\n const prop = raw as PropertySpec;\n let schema: z.ZodType = prop.enum\n ? z.enum(prop.enum as [string, ...string[]])\n : prop.type === 'number'\n ? z.number()\n : z.string();\n if (prop.description) schema = schema.describe(prop.description);\n return [key, required.has(key) ? schema : schema.optional()];\n })\n );\n}\n\nexport function createServer(session: LocalSession): McpServer {\n const server = new McpServer({ name: 'stuntdouble', version: PACKAGE.split('@')[1]! });\n\n // `done` is the toolset's end signal; here a run ends with finish_run.\n for (const spec of DIRECT_TOOL_SPECS.filter((s) => s.name !== 'done')) {\n server.registerTool(\n spec.name,\n { description: spec.description, inputSchema: shapeOf(spec) },\n (input) => attempt(() => session.browserTool(spec.name, input))\n );\n }\n\n server.registerTool(\n 'start_run',\n {\n description:\n \"Start a local run of a checklist from .stuntdouble/checklists/<checklist>.json: opens a fresh Chrome at the checklist's URL, and returns the viewer link, the actor's brief and the checks to judge. If a file is invalid, returns each field to fix. With resume, continues the run still open in this session, or else the newest interrupted or stopped run of that checklist, judging only the checks without a verdict.\",\n inputSchema: {\n checklist: z.string().describe('The checklist slug, e.g. \"checkout\".'),\n resume: z.boolean().optional().describe('Continue the last unfinished run instead.'),\n },\n },\n (input) => attempt(() => session.startRun(input))\n );\n\n server.registerTool(\n 'evaluate_check',\n {\n description:\n 'Record the verdict on one check, as soon as you have observed it. A passed verdict needs a locator for evidence visible on the page now; a failure may describe something missing. Use \"informational\" only for informational checks, and \"blocked\" when you could not reach what the check is about. Saves a screenshot of the evidence and returns the next check.',\n inputSchema: {\n check: z.string().describe('The check key, e.g. \"guest-checkout\".'),\n status: z.enum(['passed', 'failed', 'informational', 'blocked']),\n evidence: z\n .string()\n .min(1)\n .describe('What you observed, in a sentence or two, quoting the page where you can.'),\n locator: z\n .object({\n text: z.string().optional().describe('Distinctive text visible on the page.'),\n selector: z.string().optional().describe('A CSS selector for the element.'),\n })\n .optional()\n .describe('Where the evidence is on the page. Required for passed.'),\n },\n },\n (input) => attempt(() => session.evaluateCheck(input))\n );\n\n server.registerTool(\n 'finish_run',\n {\n description:\n 'End the run: settles the checks left, scores it the way the cloud does, writes run.json, closes the browser and returns the outcome. Always call it, even when blocked or stopped.',\n inputSchema: {\n summary: z.string().min(1).describe('One or two sentences on what you found.'),\n },\n },\n (input) => attempt(() => session.finishRun(input))\n );\n\n server.registerTool(\n 'open_ui',\n {\n description:\n '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.',\n },\n () => attempt(() => session.overview())\n );\n\n server.registerTool(\n 'doctor',\n {\n description:\n \"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 --.\",\n annotations: { readOnlyHint: true },\n },\n () =>\n attempt(async () => {\n const { doctor } = await import('./doctor.ts');\n // A FAIL line says what is wrong; the call itself succeeded.\n const { lines } = await doctor({\n project: dirname(session.root),\n viewerPort: session.viewerPort,\n });\n return lines.join('\\n');\n })\n );\n\n return server;\n}\n\nexport async function serveMcp(): Promise<void> {\n const session = new LocalSession({ project: projectDir() });\n const server = createServer(session);\n // stdin closing and the SIGTERM that follows both land here; close once.\n let closing: Promise<void> | undefined;\n const shutdown = () => (closing ??= session.close().finally(() => process.exit(0)));\n process.stdin.on('close', shutdown);\n process.on('SIGINT', shutdown);\n process.on('SIGTERM', shutdown);\n await server.connect(new StdioServerTransport());\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,17 @@
1
+ // src/open-browser.ts
2
+ import { spawn } from "child_process";
3
+ function openBrowser(url) {
4
+ if (!/^https?:\/\//.test(url)) return;
5
+ const [command, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["rundll32", ["url.dll,FileProtocolHandler", url]] : ["xdg-open", [url]];
6
+ try {
7
+ const child = spawn(command, args, { stdio: "ignore", detached: true });
8
+ child.on("error", () => {
9
+ });
10
+ child.unref();
11
+ } catch {
12
+ }
13
+ }
14
+ export {
15
+ openBrowser
16
+ };
17
+ //# sourceMappingURL=open-browser-CTOGH5HQ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/open-browser.ts"],"sourcesContent":["// Opens a link in the person's default browser, for `stuntdouble studio` and\n// `stuntdouble login`. The one place this package starts a process: the\n// platform's own opener, with the link as its only argument, nothing else\n// (test/no-model.test.ts holds it to that).\n\nimport { spawn } from 'node:child_process';\n\nexport function openBrowser(url: string): void {\n if (!/^https?:\\/\\//.test(url)) return;\n const [command, args] =\n process.platform === 'darwin'\n ? ['open', [url]]\n : process.platform === 'win32'\n ? ['rundll32', ['url.dll,FileProtocolHandler', url]]\n : ['xdg-open', [url]];\n try {\n const child = spawn(command, args, { stdio: 'ignore', detached: true });\n child.on('error', () => {});\n child.unref();\n } catch {\n // No opener: the link is printed as well.\n }\n}\n"],"mappings":";AAKA,SAAS,aAAa;AAEf,SAAS,YAAY,KAAmB;AAC7C,MAAI,CAAC,eAAe,KAAK,GAAG,EAAG;AAC/B,QAAM,CAAC,SAAS,IAAI,IAClB,QAAQ,aAAa,WACjB,CAAC,QAAQ,CAAC,GAAG,CAAC,IACd,QAAQ,aAAa,UACnB,CAAC,YAAY,CAAC,+BAA+B,GAAG,CAAC,IACjD,CAAC,YAAY,CAAC,GAAG,CAAC;AAC1B,MAAI;AACF,UAAM,QAAQ,MAAM,SAAS,MAAM,EAAE,OAAO,UAAU,UAAU,KAAK,CAAC;AACtE,UAAM,GAAG,SAAS,MAAM;AAAA,IAAC,CAAC;AAC1B,UAAM,MAAM;AAAA,EACd,QAAQ;AAAA,EAER;AACF;","names":[]}
@@ -0,0 +1,20 @@
1
+ import {
2
+ LocalSession,
3
+ PACKAGE,
4
+ isTrustedHost,
5
+ rebase,
6
+ scoreLocalRun,
7
+ unweightedNote
8
+ } from "./chunk-XHSDRB3X.js";
9
+ import "./chunk-BFI6P5DO.js";
10
+ import "./chunk-FMZTT2MC.js";
11
+ import "./chunk-F3R3QL7V.js";
12
+ export {
13
+ LocalSession,
14
+ PACKAGE,
15
+ isTrustedHost,
16
+ rebase,
17
+ scoreLocalRun,
18
+ unweightedNote
19
+ };
20
+ //# sourceMappingURL=run-MPIAR3LQ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,17 @@
1
+ import {
2
+ DEFAULT_LIMITS,
3
+ RUN_TOOLS,
4
+ scrub,
5
+ startStandaloneRun,
6
+ testConnection
7
+ } from "./chunk-P3CI4FTE.js";
8
+ import "./chunk-FMZTT2MC.js";
9
+ import "./chunk-F3R3QL7V.js";
10
+ export {
11
+ DEFAULT_LIMITS,
12
+ RUN_TOOLS,
13
+ scrub,
14
+ startStandaloneRun,
15
+ testConnection
16
+ };
17
+ //# sourceMappingURL=runner-E45EJ76B.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,360 @@
1
+ import {
2
+ CloudRefusal,
3
+ beginLogin,
4
+ chooseWorkspace,
5
+ cloudStatus,
6
+ completeLogin,
7
+ listWorkspaces,
8
+ logout,
9
+ startCloudRun
10
+ } from "./chunk-DFXASIGV.js";
11
+ import {
12
+ PROVIDERS,
13
+ connectionStatus,
14
+ resolveConnection,
15
+ saveConnection
16
+ } from "./chunk-MSV5OLSY.js";
17
+ import {
18
+ LocalSession
19
+ } from "./chunk-XHSDRB3X.js";
20
+ import {
21
+ send,
22
+ startUi
23
+ } from "./chunk-BFI6P5DO.js";
24
+ import {
25
+ DEFAULT_LIMITS,
26
+ startStandaloneRun,
27
+ testConnection
28
+ } from "./chunk-P3CI4FTE.js";
29
+ import "./chunk-FMZTT2MC.js";
30
+ import {
31
+ ActorSchema,
32
+ ChecklistSchema,
33
+ sdDir,
34
+ writeFileAtomic,
35
+ writeJsonAtomic
36
+ } from "./chunk-F3R3QL7V.js";
37
+
38
+ // src/studio.ts
39
+ import { readFile, stat } from "fs/promises";
40
+ import { join } from "path";
41
+ import { z } from "zod";
42
+ var SLUG = /^[a-z0-9][a-z0-9-]*$/;
43
+ var MAX_BODY = 1e6;
44
+ var json = (res, status, data) => send(res, status, JSON.stringify(data), "application/json");
45
+ var problem = (res, status, message) => json(res, status, { error: message });
46
+ var issuesOf = (error) => error.issues.map((i) => ({ path: i.path.join("."), message: i.message }));
47
+ async function readBody(req) {
48
+ let body = "";
49
+ for await (const chunk of req) {
50
+ body += chunk;
51
+ if (body.length > MAX_BODY) throw new Error("Body too large");
52
+ }
53
+ return body ? JSON.parse(body) : {};
54
+ }
55
+ var exists = (path) => stat(path).then(
56
+ () => true,
57
+ () => false
58
+ );
59
+ function usable(connection) {
60
+ if (!connection) return false;
61
+ if (connection.apiKey) return true;
62
+ const host = new URL(connection.baseUrl).hostname;
63
+ return connection.provider === "openai" && /^(localhost|127\.0\.0\.1|\[::1\])$/.test(host);
64
+ }
65
+ async function createStarter(project, url) {
66
+ const root = sdDir(project);
67
+ await writeJsonAtomic(join(root, "stuntdouble.json"), {
68
+ format: "stuntdouble/1",
69
+ local_base_url: null,
70
+ headless: false,
71
+ model_screenshots: "on_request",
72
+ safety: "third_party"
73
+ });
74
+ await writeFileAtomic(join(root, ".gitignore"), ".live/\n");
75
+ const actor = join(root, "actors", "first-time-visitor.json");
76
+ if (!await exists(actor)) {
77
+ await writeJsonAtomic(actor, {
78
+ name: "First-time visitor",
79
+ description: "Has never used the product, arrives from a search result and decides within a minute whether it is worth their time",
80
+ knowledge: [],
81
+ persist_login: false
82
+ });
83
+ }
84
+ const checklist = join(root, "checklists", "first-look.json");
85
+ if (!await exists(checklist)) {
86
+ await writeJsonAtomic(checklist, {
87
+ name: "First look",
88
+ actor: "first-time-visitor",
89
+ url,
90
+ instructions: "Arrive at the home page as someone who has never heard of the product. Work out what it is, try the main next step without paying or signing up, and follow the main navigation.",
91
+ checks: [
92
+ {
93
+ key: "says-what-it-is",
94
+ description: "The first screen says plainly what the product is and who it is for"
95
+ },
96
+ {
97
+ key: "clear-next-step",
98
+ description: "There is one clear next step, such as a sign-up button, without scrolling"
99
+ },
100
+ {
101
+ key: "navigation-works",
102
+ description: "The main navigation links open working pages, with no error or empty page"
103
+ },
104
+ {
105
+ key: "first-impression",
106
+ description: "Record what a new visitor would read or do first",
107
+ type: "informational"
108
+ }
109
+ ]
110
+ });
111
+ }
112
+ }
113
+ var InitInput = z.object({ url: z.url({ protocol: /^https?$/ }) });
114
+ var RunInput = z.object({
115
+ checklist: z.string().regex(SLUG),
116
+ where: z.enum(["local", "cloud"]).default("local"),
117
+ resume: z.boolean().optional()
118
+ });
119
+ var ConnectionInput = z.object({
120
+ provider: z.enum(PROVIDERS),
121
+ model: z.string().trim().min(1, "Name the model").max(200),
122
+ base_url: z.union([z.url({ protocol: /^https?$/ }), z.literal("")]).nullish(),
123
+ api_key: z.string().max(500).nullish(),
124
+ clear_key: z.boolean().optional()
125
+ });
126
+ async function startStudio(options) {
127
+ const project = options.project;
128
+ const root = sdDir(project);
129
+ let job = null;
130
+ let session = null;
131
+ const cloudJobs = /* @__PURE__ */ new Map();
132
+ async function status() {
133
+ return {
134
+ project,
135
+ initialised: await exists(join(root, "stuntdouble.json")),
136
+ connection: await connectionStatus(),
137
+ cloud: await cloudStatus(),
138
+ jobs: [...job ? [job] : [], ...cloudJobs.values()],
139
+ limits: DEFAULT_LIMITS
140
+ };
141
+ }
142
+ async function saveDefinition(res, folder, slug, body) {
143
+ const input = z.object({ data: z.record(z.string(), z.unknown()), create: z.boolean().optional() }).safeParse(body);
144
+ if (!input.success) return problem(res, 400, 'Send {"data": {...}}.');
145
+ if (!SLUG.test(slug)) {
146
+ return json(res, 422, {
147
+ issues: [{ path: "slug", message: "Use lowercase letters, digits and hyphens" }]
148
+ });
149
+ }
150
+ const path = join(root, folder, `${slug}.json`);
151
+ const existing = await readFile(path, "utf8").then((t) => JSON.parse(t)).catch(() => null);
152
+ if (input.data.create && existing) {
153
+ return json(res, 409, {
154
+ issues: [
155
+ { path: "slug", message: `There is already a ${folder.slice(0, -1)} called ${slug}` }
156
+ ]
157
+ });
158
+ }
159
+ const { cloud: _ignored, ...fields } = input.data.data;
160
+ const data = existing?.cloud ? { ...fields, cloud: existing.cloud } : fields;
161
+ const schema = folder === "actors" ? ActorSchema : ChecklistSchema;
162
+ const parsed = schema.safeParse(data);
163
+ const issues = parsed.success ? [] : issuesOf(parsed.error);
164
+ if (folder === "checklists" && typeof data.actor === "string" && SLUG.test(data.actor)) {
165
+ if (!await exists(join(root, "actors", `${data.actor}.json`))) {
166
+ issues.push({ path: "actor", message: `There is no actor called ${data.actor}` });
167
+ }
168
+ }
169
+ if (issues.length) return json(res, 422, { issues });
170
+ await writeJsonAtomic(path, data);
171
+ return json(res, 200, { slug });
172
+ }
173
+ async function startRun(res, body) {
174
+ const input = RunInput.safeParse(body);
175
+ if (!input.success) return problem(res, 400, "Name a checklist to run.");
176
+ const { checklist, where, resume } = input.data;
177
+ if (where === "cloud") {
178
+ const run = await startCloudRun({ project, checklist, pollMs: options.cloudPollMs });
179
+ const entry = {
180
+ kind: "cloud",
181
+ run_id: run.runId,
182
+ checklist,
183
+ model: null,
184
+ usage: null,
185
+ last: "Running in the cloud"
186
+ };
187
+ cloudJobs.set(run.runId, entry);
188
+ run.done.catch(() => {
189
+ }).finally(() => cloudJobs.delete(run.runId));
190
+ return json(res, 200, { run_id: run.runId });
191
+ }
192
+ if (job) return problem(res, 409, `A run of ${job.checklist} is still going.`);
193
+ const connection = await resolveConnection();
194
+ if (!usable(connection)) {
195
+ return problem(res, 400, "Connect a model first: Settings, then Connection.");
196
+ }
197
+ const model = { provider: connection.provider, id: connection.model };
198
+ session = new LocalSession({ project, ui, launch: options.launch, runner: { model } });
199
+ const current = { kind: "local", run_id: "", checklist, model, usage: null, last: "" };
200
+ job = current;
201
+ try {
202
+ const run = await startStandaloneRun({
203
+ session,
204
+ connection,
205
+ checklist,
206
+ resume,
207
+ fetch: options.fetch,
208
+ onProgress: (p) => {
209
+ current.usage = p.usage;
210
+ current.last = p.last;
211
+ }
212
+ });
213
+ current.run_id = run.runId;
214
+ const ended = session;
215
+ run.done.catch(() => {
216
+ }).finally(() => {
217
+ if (job === current) job = null;
218
+ void ended.close();
219
+ });
220
+ return json(res, 200, { run_id: run.runId });
221
+ } catch (err) {
222
+ job = null;
223
+ await session.close().catch(() => {
224
+ });
225
+ throw err;
226
+ }
227
+ }
228
+ async function handle(req, res, url) {
229
+ const path = url.pathname;
230
+ if (path === "/studio.js" && req.method === "GET") {
231
+ const script = await readFile(new URL("./studio.js", import.meta.url));
232
+ send(res, 200, script, "text/javascript; charset=utf-8");
233
+ return true;
234
+ }
235
+ if (!path.startsWith("/api/studio")) return false;
236
+ if (req.method === "GET") {
237
+ if (path === "/api/studio") json(res, 200, await status());
238
+ else if (path === "/api/studio/cloud/workspaces") {
239
+ json(res, 200, { workspaces: await listWorkspaces() });
240
+ } else problem(res, 404, "Not found");
241
+ return true;
242
+ }
243
+ if (req.headers.origin !== `http://${req.headers.host}`) {
244
+ problem(res, 403, "Forbidden");
245
+ return true;
246
+ }
247
+ if (!/^application\/json\b/.test(req.headers["content-type"] ?? "")) {
248
+ problem(res, 415, "Send JSON.");
249
+ return true;
250
+ }
251
+ let body;
252
+ try {
253
+ body = await readBody(req);
254
+ } catch {
255
+ problem(res, 400, "That was not JSON.");
256
+ return true;
257
+ }
258
+ const definition = path.match(/^\/api\/studio\/(actors|checklists)\/([^/]+)$/);
259
+ try {
260
+ if (definition && req.method === "PUT") {
261
+ await saveDefinition(
262
+ res,
263
+ definition[1],
264
+ decodeURIComponent(definition[2]),
265
+ body
266
+ );
267
+ } else if (req.method !== "POST") {
268
+ problem(res, 405, "Method not allowed");
269
+ } else if (path === "/api/studio/init") {
270
+ const input = InitInput.safeParse(body);
271
+ if (!input.success) {
272
+ json(res, 422, {
273
+ issues: [{ path: "url", message: "Enter a web address, http or https" }]
274
+ });
275
+ } else if (await exists(join(root, "stuntdouble.json"))) {
276
+ problem(res, 409, "This project is already set up.");
277
+ } else {
278
+ await createStarter(project, input.data.url);
279
+ json(res, 200, { checklist: "first-look" });
280
+ }
281
+ } else if (path === "/api/studio/run") {
282
+ await startRun(res, body);
283
+ } else if (path === "/api/studio/connection") {
284
+ const input = ConnectionInput.safeParse(body);
285
+ if (!input.success) json(res, 422, { issues: issuesOf(input.error) });
286
+ else {
287
+ await saveConnection(input.data);
288
+ json(res, 200, await connectionStatus());
289
+ }
290
+ } else if (path === "/api/studio/connection/test") {
291
+ const connection = await resolveConnection();
292
+ json(
293
+ res,
294
+ 200,
295
+ usable(connection) ? await testConnection(connection, options.fetch) : { ok: false, message: "There is no key for this connection yet." }
296
+ );
297
+ } else if (path === "/api/studio/cloud/login") {
298
+ json(res, 200, { url: await beginLogin(`http://127.0.0.1:${ui.port}/oauth/callback`) });
299
+ } else if (path === "/api/studio/cloud/logout") {
300
+ await logout();
301
+ json(res, 200, await cloudStatus());
302
+ } else if (path === "/api/studio/cloud/workspace") {
303
+ const id = body.id;
304
+ if (typeof id !== "string") problem(res, 400, "Name a workspace.");
305
+ else json(res, 200, { workspace: await chooseWorkspace(id) });
306
+ } else {
307
+ problem(res, 404, "Not found");
308
+ }
309
+ } catch (err) {
310
+ problem(res, err instanceof CloudRefusal ? 403 : 400, err.message);
311
+ }
312
+ return true;
313
+ }
314
+ async function oauthCallback(_req, res, url) {
315
+ const state = url.searchParams.get("state") ?? "";
316
+ const code = url.searchParams.get("code");
317
+ let message = "Signed in to Stunt Double.";
318
+ let ok = true;
319
+ try {
320
+ if (!code)
321
+ throw new Error(url.searchParams.get("error_description") ?? "Sign-in was cancelled.");
322
+ await completeLogin(state, code);
323
+ } catch (err) {
324
+ ok = false;
325
+ message = err.message;
326
+ }
327
+ const escape = (s) => s.replace(/[&<>"']/g, (c) => `&#${c.charCodeAt(0)};`);
328
+ send(
329
+ res,
330
+ ok ? 200 : 400,
331
+ `<!doctype html><meta charset="utf-8"><title>Stunt Double Studio</title>${ok ? '<meta http-equiv="refresh" content="1;url=/settings">' : ""}<body style="font:15px system-ui;padding:3rem;max-width:32rem"><p>${escape(message)}</p><p><a href="/settings">Back to the Studio</a></p></body>`,
332
+ "text/html; charset=utf-8",
333
+ { "Content-Security-Policy": "default-src 'none'; style-src 'unsafe-inline'" }
334
+ );
335
+ }
336
+ const ui = await startUi({
337
+ root,
338
+ port: options.port,
339
+ extension: {
340
+ pages: /^\/(checklists|actors|settings)(\/[^/]+(\/run)?)?$/,
341
+ script: "/studio.js",
342
+ handle,
343
+ oauthCallback
344
+ }
345
+ });
346
+ return {
347
+ ui,
348
+ link: (path) => ui.link(path),
349
+ close: async () => {
350
+ await session?.close().catch(() => {
351
+ });
352
+ await ui.close();
353
+ }
354
+ };
355
+ }
356
+ export {
357
+ createStarter,
358
+ startStudio
359
+ };
360
+ //# sourceMappingURL=studio-FBC66BER.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/studio.ts"],"sourcesContent":["// Stunt Double Studio: the viewer grown into a local app (`stuntdouble\n// studio`). It adds editing actors and checklists, starting a run on the\n// person's own model connection or in the cloud, and settings for both.\n//\n// Policy line: a Studio run uses only a connection the person configures (an\n// Anthropic API key or an OpenAI-compatible endpoint), never a Claude\n// subscription. Keys stay in ~/.stuntdouble/credentials.json (credentials.ts):\n// the page is told only whether one is set and where from. The MCP server\n// never imports this module, so inside Claude Code nothing here runs.\n//\n// The security model is the viewer's (ui-server.ts): loopback only, the\n// per-start key cookie, the Host check on every request, no CORS. Every write\n// here also needs this server's own Origin and a JSON body, so neither another\n// site nor a plain form can make one.\n\nimport { readFile, stat } from 'node:fs/promises';\nimport type { IncomingMessage, ServerResponse } from 'node:http';\nimport { join } from 'node:path';\nimport { z } from 'zod';\n\nimport {\n beginLogin,\n chooseWorkspace,\n CloudRefusal,\n cloudStatus,\n completeLogin,\n listWorkspaces,\n logout,\n startCloudRun,\n} from './cloud.ts';\nimport {\n connectionStatus,\n PROVIDERS,\n resolveConnection,\n saveConnection,\n type Connection,\n} from './credentials.ts';\nimport { ActorSchema, ChecklistSchema, sdDir, writeFileAtomic, writeJsonAtomic } from './files.ts';\nimport { LocalSession, type Launch } from './run.ts';\nimport { DEFAULT_LIMITS, startStandaloneRun, testConnection, type RunUsage } from './runner.ts';\nimport { send, startUi, type UiServer } from './ui-server.ts';\n\nconst SLUG = /^[a-z0-9][a-z0-9-]*$/;\nconst MAX_BODY = 1_000_000;\n\ntype Job = {\n kind: 'local' | 'cloud';\n run_id: string;\n checklist: string;\n model: { provider: string; id: string } | null;\n usage: RunUsage | null;\n last: string;\n};\n\nexport type Studio = {\n ui: UiServer;\n link(path?: string): string;\n close(): Promise<void>;\n};\n\nconst json = (res: ServerResponse, status: number, data: unknown) =>\n send(res, status, JSON.stringify(data), 'application/json');\nconst problem = (res: ServerResponse, status: number, message: string) =>\n json(res, status, { error: message });\n\ntype Issue = { path: string; message: string };\nconst issuesOf = (error: z.ZodError): Issue[] =>\n error.issues.map((i) => ({ path: i.path.join('.'), message: i.message }));\n\nasync function readBody(req: IncomingMessage): Promise<unknown> {\n let body = '';\n for await (const chunk of req) {\n body += chunk;\n if (body.length > MAX_BODY) throw new Error('Body too large');\n }\n return body ? JSON.parse(body) : {};\n}\n\nconst exists = (path: string) =>\n stat(path).then(\n () => true,\n () => false\n );\n\n/** A connection that can make a call: a key, unless the endpoint is on this machine. */\nfunction usable(connection: Connection | null): connection is Connection {\n if (!connection) return false;\n if (connection.apiKey) return true;\n const host = new URL(connection.baseUrl).hostname;\n return connection.provider === 'openai' && /^(localhost|127\\.0\\.0\\.1|\\[::1\\])$/.test(host);\n}\n\n/** The starter files for a project with no `.stuntdouble/` yet. */\nexport async function createStarter(project: string, url: string): Promise<void> {\n const root = sdDir(project);\n await writeJsonAtomic(join(root, 'stuntdouble.json'), {\n format: 'stuntdouble/1',\n local_base_url: null,\n headless: false,\n model_screenshots: 'on_request',\n safety: 'third_party',\n });\n await writeFileAtomic(join(root, '.gitignore'), '.live/\\n');\n const actor = join(root, 'actors', 'first-time-visitor.json');\n if (!(await exists(actor))) {\n await writeJsonAtomic(actor, {\n name: 'First-time visitor',\n description:\n 'Has never used the product, arrives from a search result and decides within a minute whether it is worth their time',\n knowledge: [],\n persist_login: false,\n });\n }\n const checklist = join(root, 'checklists', 'first-look.json');\n if (!(await exists(checklist))) {\n await writeJsonAtomic(checklist, {\n name: 'First look',\n actor: 'first-time-visitor',\n url,\n instructions:\n 'Arrive at the home page as someone who has never heard of the product. Work out what it is, try the main next step without paying or signing up, and follow the main navigation.',\n checks: [\n {\n key: 'says-what-it-is',\n description: 'The first screen says plainly what the product is and who it is for',\n },\n {\n key: 'clear-next-step',\n description: 'There is one clear next step, such as a sign-up button, without scrolling',\n },\n {\n key: 'navigation-works',\n description: 'The main navigation links open working pages, with no error or empty page',\n },\n {\n key: 'first-impression',\n description: 'Record what a new visitor would read or do first',\n type: 'informational',\n },\n ],\n });\n }\n}\n\nconst InitInput = z.object({ url: z.url({ protocol: /^https?$/ }) });\nconst RunInput = z.object({\n checklist: z.string().regex(SLUG),\n where: z.enum(['local', 'cloud']).default('local'),\n resume: z.boolean().optional(),\n});\nconst ConnectionInput = z.object({\n provider: z.enum(PROVIDERS),\n model: z.string().trim().min(1, 'Name the model').max(200),\n base_url: z.union([z.url({ protocol: /^https?$/ }), z.literal('')]).nullish(),\n api_key: z.string().max(500).nullish(),\n clear_key: z.boolean().optional(),\n});\n\nexport async function startStudio(options: {\n project: string;\n port?: number;\n launch?: Launch;\n fetch?: typeof fetch;\n cloudPollMs?: number;\n}): Promise<Studio> {\n const project = options.project;\n const root = sdDir(project);\n let job: Job | null = null;\n let session: LocalSession | null = null;\n const cloudJobs = new Map<string, Job>();\n\n async function status() {\n return {\n project,\n initialised: await exists(join(root, 'stuntdouble.json')),\n connection: await connectionStatus(),\n cloud: await cloudStatus(),\n jobs: [...(job ? [job] : []), ...cloudJobs.values()],\n limits: DEFAULT_LIMITS,\n };\n }\n\n /** Write one actor or checklist, validated by the schema `start_run` reads it with. */\n async function saveDefinition(\n res: ServerResponse,\n folder: 'actors' | 'checklists',\n slug: string,\n body: unknown\n ): Promise<void> {\n const input = z\n .object({ data: z.record(z.string(), z.unknown()), create: z.boolean().optional() })\n .safeParse(body);\n if (!input.success) return problem(res, 400, 'Send {\"data\": {...}}.');\n if (!SLUG.test(slug)) {\n return json(res, 422, {\n issues: [{ path: 'slug', message: 'Use lowercase letters, digits and hyphens' }],\n });\n }\n const path = join(root, folder, `${slug}.json`);\n const existing = await readFile(path, 'utf8')\n .then((t) => JSON.parse(t) as Record<string, unknown>)\n .catch(() => null);\n if (input.data.create && existing) {\n return json(res, 409, {\n issues: [\n { path: 'slug', message: `There is already a ${folder.slice(0, -1)} called ${slug}` },\n ],\n });\n }\n // Cloud ids are written by a push, never by the form.\n const { cloud: _ignored, ...fields } = input.data.data;\n const data = existing?.cloud ? { ...fields, cloud: existing.cloud } : fields;\n const schema = folder === 'actors' ? ActorSchema : ChecklistSchema;\n const parsed = schema.safeParse(data);\n const issues = parsed.success ? [] : issuesOf(parsed.error);\n if (folder === 'checklists' && typeof data.actor === 'string' && SLUG.test(data.actor)) {\n if (!(await exists(join(root, 'actors', `${data.actor}.json`)))) {\n issues.push({ path: 'actor', message: `There is no actor called ${data.actor}` });\n }\n }\n if (issues.length) return json(res, 422, { issues });\n await writeJsonAtomic(path, data);\n return json(res, 200, { slug });\n }\n\n async function startRun(res: ServerResponse, body: unknown): Promise<void> {\n const input = RunInput.safeParse(body);\n if (!input.success) return problem(res, 400, 'Name a checklist to run.');\n const { checklist, where, resume } = input.data;\n if (where === 'cloud') {\n const run = await startCloudRun({ project, checklist, pollMs: options.cloudPollMs });\n const entry: Job = {\n kind: 'cloud',\n run_id: run.runId,\n checklist,\n model: null,\n usage: null,\n last: 'Running in the cloud',\n };\n cloudJobs.set(run.runId, entry);\n run.done.catch(() => {}).finally(() => cloudJobs.delete(run.runId));\n return json(res, 200, { run_id: run.runId });\n }\n if (job) return problem(res, 409, `A run of ${job.checklist} is still going.`);\n const connection = await resolveConnection();\n if (!usable(connection)) {\n return problem(res, 400, 'Connect a model first: Settings, then Connection.');\n }\n const model = { provider: connection.provider, id: connection.model };\n session = new LocalSession({ project, ui, launch: options.launch, runner: { model } });\n const current: Job = { kind: 'local', run_id: '', checklist, model, usage: null, last: '' };\n job = current;\n try {\n const run = await startStandaloneRun({\n session,\n connection,\n checklist,\n resume,\n fetch: options.fetch,\n onProgress: (p) => {\n current.usage = p.usage;\n current.last = p.last;\n },\n });\n current.run_id = run.runId;\n const ended = session;\n run.done\n .catch(() => {})\n .finally(() => {\n if (job === current) job = null;\n void ended.close();\n });\n return json(res, 200, { run_id: run.runId });\n } catch (err) {\n job = null;\n await session.close().catch(() => {});\n throw err;\n }\n }\n\n async function handle(req: IncomingMessage, res: ServerResponse, url: URL): Promise<boolean> {\n const path = url.pathname;\n if (path === '/studio.js' && req.method === 'GET') {\n const script = await readFile(new URL('./studio.js', import.meta.url));\n send(res, 200, script, 'text/javascript; charset=utf-8');\n return true;\n }\n if (!path.startsWith('/api/studio')) return false;\n if (req.method === 'GET') {\n if (path === '/api/studio') json(res, 200, await status());\n else if (path === '/api/studio/cloud/workspaces') {\n json(res, 200, { workspaces: await listWorkspaces() });\n } else problem(res, 404, 'Not found');\n return true;\n }\n // Writes: only from this server's own page.\n if (req.headers.origin !== `http://${req.headers.host}`) {\n problem(res, 403, 'Forbidden');\n return true;\n }\n if (!/^application\\/json\\b/.test(req.headers['content-type'] ?? '')) {\n problem(res, 415, 'Send JSON.');\n return true;\n }\n let body: unknown;\n try {\n body = await readBody(req);\n } catch {\n problem(res, 400, 'That was not JSON.');\n return true;\n }\n const definition = path.match(/^\\/api\\/studio\\/(actors|checklists)\\/([^/]+)$/);\n try {\n if (definition && req.method === 'PUT') {\n await saveDefinition(\n res,\n definition[1] as 'actors' | 'checklists',\n decodeURIComponent(definition[2]!),\n body\n );\n } else if (req.method !== 'POST') {\n problem(res, 405, 'Method not allowed');\n } else if (path === '/api/studio/init') {\n const input = InitInput.safeParse(body);\n if (!input.success) {\n json(res, 422, {\n issues: [{ path: 'url', message: 'Enter a web address, http or https' }],\n });\n } else if (await exists(join(root, 'stuntdouble.json'))) {\n problem(res, 409, 'This project is already set up.');\n } else {\n await createStarter(project, input.data.url);\n json(res, 200, { checklist: 'first-look' });\n }\n } else if (path === '/api/studio/run') {\n await startRun(res, body);\n } else if (path === '/api/studio/connection') {\n const input = ConnectionInput.safeParse(body);\n if (!input.success) json(res, 422, { issues: issuesOf(input.error) });\n else {\n await saveConnection(input.data);\n json(res, 200, await connectionStatus());\n }\n } else if (path === '/api/studio/connection/test') {\n const connection = await resolveConnection();\n json(\n res,\n 200,\n usable(connection)\n ? await testConnection(connection, options.fetch)\n : { ok: false, message: 'There is no key for this connection yet.' }\n );\n } else if (path === '/api/studio/cloud/login') {\n json(res, 200, { url: await beginLogin(`http://127.0.0.1:${ui.port}/oauth/callback`) });\n } else if (path === '/api/studio/cloud/logout') {\n await logout();\n json(res, 200, await cloudStatus());\n } else if (path === '/api/studio/cloud/workspace') {\n const id = (body as { id?: unknown }).id;\n if (typeof id !== 'string') problem(res, 400, 'Name a workspace.');\n else json(res, 200, { workspace: await chooseWorkspace(id) });\n } else {\n problem(res, 404, 'Not found');\n }\n } catch (err) {\n // Our own errors say what to fix (a file, a refusal from Stunt Double); the page shows them as text.\n problem(res, err instanceof CloudRefusal ? 403 : 400, (err as Error).message);\n }\n return true;\n }\n\n async function oauthCallback(_req: IncomingMessage, res: ServerResponse, url: URL) {\n const state = url.searchParams.get('state') ?? '';\n const code = url.searchParams.get('code');\n let message = 'Signed in to Stunt Double.';\n let ok = true;\n try {\n if (!code)\n throw new Error(url.searchParams.get('error_description') ?? 'Sign-in was cancelled.');\n await completeLogin(state, code);\n } catch (err) {\n ok = false;\n message = (err as Error).message;\n }\n const escape = (s: string) => s.replace(/[&<>\"']/g, (c) => `&#${c.charCodeAt(0)};`);\n // A same-origin navigation from here carries the Strict cookie again.\n send(\n res,\n ok ? 200 : 400,\n `<!doctype html><meta charset=\"utf-8\"><title>Stunt Double Studio</title>${ok ? '<meta http-equiv=\"refresh\" content=\"1;url=/settings\">' : ''}<body style=\"font:15px system-ui;padding:3rem;max-width:32rem\"><p>${escape(message)}</p><p><a href=\"/settings\">Back to the Studio</a></p></body>`,\n 'text/html; charset=utf-8',\n { 'Content-Security-Policy': \"default-src 'none'; style-src 'unsafe-inline'\" }\n );\n }\n\n const ui = await startUi({\n root,\n port: options.port,\n extension: {\n pages: /^\\/(checklists|actors|settings)(\\/[^/]+(\\/run)?)?$/,\n script: '/studio.js',\n handle,\n oauthCallback,\n },\n });\n\n return {\n ui,\n link: (path) => ui.link(path),\n close: async () => {\n await session?.close().catch(() => {});\n await ui.close();\n },\n };\n}\n"],"mappings":"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","names":[]}