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.
- package/README.md +31 -6
- package/dist/chunk-BFI6P5DO.js +278 -0
- package/dist/chunk-BFI6P5DO.js.map +1 -0
- package/dist/chunk-DFXASIGV.js +431 -0
- package/dist/chunk-DFXASIGV.js.map +1 -0
- package/dist/{chunk-7IC2G3Z3.js → chunk-F3R3QL7V.js} +19 -252
- package/dist/chunk-F3R3QL7V.js.map +1 -0
- package/dist/{mcp-MOXTYXZU.js → chunk-FMZTT2MC.js} +72 -1160
- package/dist/chunk-FMZTT2MC.js.map +1 -0
- package/dist/chunk-MSV5OLSY.js +117 -0
- package/dist/chunk-MSV5OLSY.js.map +1 -0
- package/dist/chunk-P3CI4FTE.js +469 -0
- package/dist/chunk-P3CI4FTE.js.map +1 -0
- package/dist/{chunk-JXTUCMO2.js → chunk-QDA5FTUH.js} +5 -3
- package/dist/{chunk-JXTUCMO2.js.map → chunk-QDA5FTUH.js.map} +1 -1
- package/dist/chunk-XHSDRB3X.js +996 -0
- package/dist/chunk-XHSDRB3X.js.map +1 -0
- package/dist/cli.js +179 -22
- package/dist/cli.js.map +1 -1
- package/dist/cloud-DLLNKYND.js +34 -0
- package/dist/credentials-QHESDY4K.js +27 -0
- package/dist/doctor-ZAYOOCPT.js +13 -0
- package/dist/doctor-ZAYOOCPT.js.map +1 -0
- package/dist/files-UBPF7PKW.js +45 -0
- package/dist/files-UBPF7PKW.js.map +1 -0
- package/dist/mcp-HFIAQ6HP.js +133 -0
- package/dist/mcp-HFIAQ6HP.js.map +1 -0
- package/dist/open-browser-CTOGH5HQ.js +17 -0
- package/dist/open-browser-CTOGH5HQ.js.map +1 -0
- package/dist/run-MPIAR3LQ.js +20 -0
- package/dist/run-MPIAR3LQ.js.map +1 -0
- package/dist/runner-E45EJ76B.js +17 -0
- package/dist/runner-E45EJ76B.js.map +1 -0
- package/dist/studio-FBC66BER.js +360 -0
- package/dist/studio-FBC66BER.js.map +1 -0
- package/dist/studio.js +1621 -0
- package/dist/ui-server-C7CFJWRD.js +12 -0
- package/dist/ui-server-C7CFJWRD.js.map +1 -0
- package/dist/ui.html +100 -56
- package/package.json +2 -2
- package/dist/chunk-7IC2G3Z3.js.map +0 -1
- package/dist/doctor-7PW2QD3M.js +0 -12
- package/dist/mcp-MOXTYXZU.js.map +0 -1
- package/dist/ui-server-ZUJ5WJ7Q.js +0 -9
- /package/dist/{doctor-7PW2QD3M.js.map → cloud-DLLNKYND.js.map} +0 -0
- /package/dist/{ui-server-ZUJ5WJ7Q.js.map → credentials-QHESDY4K.js.map} +0 -0
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
import {
|
|
2
|
+
readCredentials,
|
|
3
|
+
updateCredentials
|
|
4
|
+
} from "./chunk-MSV5OLSY.js";
|
|
5
|
+
import {
|
|
6
|
+
PACKAGE
|
|
7
|
+
} from "./chunk-XHSDRB3X.js";
|
|
8
|
+
import {
|
|
9
|
+
isPrivateHost
|
|
10
|
+
} from "./chunk-FMZTT2MC.js";
|
|
11
|
+
import {
|
|
12
|
+
ActorSchema,
|
|
13
|
+
ChecklistSchema,
|
|
14
|
+
ConfigSchema,
|
|
15
|
+
readJson,
|
|
16
|
+
runningMarker,
|
|
17
|
+
writeFileAtomic,
|
|
18
|
+
writeJsonAtomic
|
|
19
|
+
} from "./chunk-F3R3QL7V.js";
|
|
20
|
+
|
|
21
|
+
// src/cloud.ts
|
|
22
|
+
import { auth } from "@modelcontextprotocol/sdk/client/auth.js";
|
|
23
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
24
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
25
|
+
import { randomBytes } from "crypto";
|
|
26
|
+
import { mkdir, readFile, rm } from "fs/promises";
|
|
27
|
+
import { basename, join } from "path";
|
|
28
|
+
var cloudServer = () => process.env.STUNTDOUBLE_CLOUD_URL || "https://app.stuntdouble.io/api/mcp";
|
|
29
|
+
var appOrigin = () => new URL(cloudServer()).origin;
|
|
30
|
+
var billingUrl = (slug) => `${appOrigin()}/w/${slug}/settings/billing`;
|
|
31
|
+
var CloudRefusal = class extends Error {
|
|
32
|
+
};
|
|
33
|
+
var StudioOAuth = class {
|
|
34
|
+
stateValue = randomBytes(24).toString("base64url");
|
|
35
|
+
authorizationUrl = null;
|
|
36
|
+
verifier = "";
|
|
37
|
+
redirect;
|
|
38
|
+
server = cloudServer();
|
|
39
|
+
constructor(redirect) {
|
|
40
|
+
this.redirect = redirect;
|
|
41
|
+
}
|
|
42
|
+
get redirectUrl() {
|
|
43
|
+
return this.redirect;
|
|
44
|
+
}
|
|
45
|
+
get clientMetadata() {
|
|
46
|
+
return {
|
|
47
|
+
client_name: "Stunt Double Studio",
|
|
48
|
+
redirect_uris: this.redirect ? [this.redirect] : [],
|
|
49
|
+
grant_types: ["authorization_code"],
|
|
50
|
+
response_types: ["code"],
|
|
51
|
+
token_endpoint_auth_method: "none"
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
state() {
|
|
55
|
+
return this.stateValue;
|
|
56
|
+
}
|
|
57
|
+
async cloud() {
|
|
58
|
+
const cloud = (await readCredentials()).cloud;
|
|
59
|
+
return cloud?.server === this.server ? cloud : void 0;
|
|
60
|
+
}
|
|
61
|
+
async clientInformation() {
|
|
62
|
+
const client = (await this.cloud())?.client;
|
|
63
|
+
if (!client) return void 0;
|
|
64
|
+
const uris = client.redirect_uris;
|
|
65
|
+
if (this.redirect && !uris?.includes(this.redirect)) return void 0;
|
|
66
|
+
return client;
|
|
67
|
+
}
|
|
68
|
+
async saveClientInformation(client) {
|
|
69
|
+
await updateCredentials((c) => ({
|
|
70
|
+
...c,
|
|
71
|
+
cloud: { ...c.cloud?.server === this.server ? c.cloud : {}, server: this.server, client }
|
|
72
|
+
}));
|
|
73
|
+
}
|
|
74
|
+
async tokens() {
|
|
75
|
+
return (await this.cloud())?.tokens;
|
|
76
|
+
}
|
|
77
|
+
async saveTokens(tokens) {
|
|
78
|
+
await updateCredentials((c) => ({
|
|
79
|
+
...c,
|
|
80
|
+
cloud: { ...c.cloud?.server === this.server ? c.cloud : {}, server: this.server, tokens }
|
|
81
|
+
}));
|
|
82
|
+
}
|
|
83
|
+
redirectToAuthorization(url) {
|
|
84
|
+
this.authorizationUrl = url;
|
|
85
|
+
}
|
|
86
|
+
saveCodeVerifier(verifier) {
|
|
87
|
+
this.verifier = verifier;
|
|
88
|
+
}
|
|
89
|
+
codeVerifier() {
|
|
90
|
+
return this.verifier;
|
|
91
|
+
}
|
|
92
|
+
async invalidateCredentials(scope) {
|
|
93
|
+
if (scope === "verifier" || scope === "discovery") return;
|
|
94
|
+
await updateCredentials((c) => {
|
|
95
|
+
if (!c.cloud) return c;
|
|
96
|
+
const { tokens: _t, client: _c, ...rest } = c.cloud;
|
|
97
|
+
return {
|
|
98
|
+
...c,
|
|
99
|
+
cloud: {
|
|
100
|
+
...rest,
|
|
101
|
+
...scope === "client" ? { tokens: c.cloud.tokens } : {},
|
|
102
|
+
...scope === "tokens" ? { client: c.cloud.client } : {}
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
var pending = /* @__PURE__ */ new Map();
|
|
109
|
+
async function beginLogin(redirect) {
|
|
110
|
+
await updateCredentials(
|
|
111
|
+
(c) => c.cloud ? { ...c, cloud: { ...c.cloud, tokens: void 0, workspace: void 0 } } : c
|
|
112
|
+
);
|
|
113
|
+
const provider = new StudioOAuth(redirect);
|
|
114
|
+
await auth(provider, { serverUrl: cloudServer() });
|
|
115
|
+
if (!provider.authorizationUrl) throw new Error("Stunt Double did not offer a sign-in page.");
|
|
116
|
+
if (pending.size >= 5) pending.delete(pending.keys().next().value);
|
|
117
|
+
pending.set(provider.stateValue, provider);
|
|
118
|
+
return provider.authorizationUrl.href;
|
|
119
|
+
}
|
|
120
|
+
async function completeLogin(state, code) {
|
|
121
|
+
const provider = pending.get(state);
|
|
122
|
+
if (!provider) throw new Error("This sign-in has expired. Start it again from the Studio.");
|
|
123
|
+
pending.delete(state);
|
|
124
|
+
await auth(provider, { serverUrl: cloudServer(), authorizationCode: code });
|
|
125
|
+
const workspaces = await listWorkspaces();
|
|
126
|
+
if (workspaces.length === 1) await chooseWorkspace(workspaces[0].id);
|
|
127
|
+
}
|
|
128
|
+
async function logout() {
|
|
129
|
+
await updateCredentials((c) => {
|
|
130
|
+
const { cloud: _cloud, ...rest } = c;
|
|
131
|
+
return rest;
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
async function cloudStatus() {
|
|
135
|
+
const cloud = (await readCredentials()).cloud;
|
|
136
|
+
const signedIn = Boolean(cloud?.server === cloudServer() && cloud.tokens);
|
|
137
|
+
return { signed_in: signedIn, workspace: signedIn ? cloud?.workspace ?? null : null };
|
|
138
|
+
}
|
|
139
|
+
async function withCloud(work) {
|
|
140
|
+
const provider = new StudioOAuth(void 0);
|
|
141
|
+
if (!await provider.tokens()) {
|
|
142
|
+
throw new CloudRefusal("Sign in to Stunt Double first: Settings, or `stuntdouble login`.");
|
|
143
|
+
}
|
|
144
|
+
const client = new Client({ name: "stuntdouble-studio", version: PACKAGE.split("@")[1] });
|
|
145
|
+
try {
|
|
146
|
+
await client.connect(
|
|
147
|
+
new StreamableHTTPClientTransport(new URL(cloudServer()), { authProvider: provider })
|
|
148
|
+
);
|
|
149
|
+
} catch (err) {
|
|
150
|
+
throw new CloudRefusal(
|
|
151
|
+
/unauthori[sz]ed|401/i.test(String(err)) ? "Your Stunt Double sign-in has expired: sign in again." : `Could not reach Stunt Double: ${err.message}`
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
const call = async (name, args) => {
|
|
155
|
+
const result = await client.callTool({ name, arguments: args });
|
|
156
|
+
const text = result.content.map((c) => c.type === "text" ? c.text ?? "" : "").join("\n");
|
|
157
|
+
if (result.isError) throw new CloudRefusal(text || `${name} was refused.`);
|
|
158
|
+
if (result.structuredContent) return result.structuredContent;
|
|
159
|
+
try {
|
|
160
|
+
return JSON.parse(text);
|
|
161
|
+
} catch {
|
|
162
|
+
return { text };
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
try {
|
|
166
|
+
return await work(call);
|
|
167
|
+
} finally {
|
|
168
|
+
await client.close().catch(() => {
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
async function listWorkspaces() {
|
|
173
|
+
return withCloud(async (call) => (await call("list_workspaces", {})).workspaces);
|
|
174
|
+
}
|
|
175
|
+
async function chooseWorkspace(id) {
|
|
176
|
+
const workspace = (await listWorkspaces()).find((w) => w.id === id || w.slug === id);
|
|
177
|
+
if (!workspace) throw new CloudRefusal("That workspace is not one of yours.");
|
|
178
|
+
await updateCredentials((c) => ({
|
|
179
|
+
...c,
|
|
180
|
+
cloud: c.cloud && {
|
|
181
|
+
...c.cloud,
|
|
182
|
+
workspace: { id: workspace.id, slug: workspace.slug, name: workspace.name }
|
|
183
|
+
}
|
|
184
|
+
}));
|
|
185
|
+
return workspace;
|
|
186
|
+
}
|
|
187
|
+
function planGate(workspace) {
|
|
188
|
+
if (workspace.plan && /^free$/i.test(workspace.plan)) {
|
|
189
|
+
return `Cloud runs from the Studio need a paid plan, and ${workspace.name} is on the Free plan. Upgrade at ${billingUrl(workspace.slug)}, or run it locally.`;
|
|
190
|
+
}
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
async function patchCloud(path, patch) {
|
|
194
|
+
const raw = JSON.parse(await readFile(path, "utf8"));
|
|
195
|
+
const cloud = raw.cloud ?? {};
|
|
196
|
+
await writeJsonAtomic(path, { ...raw, cloud: { ...cloud, ...patch } });
|
|
197
|
+
}
|
|
198
|
+
function stamp(date = /* @__PURE__ */ new Date()) {
|
|
199
|
+
const p = (n) => String(n).padStart(2, "0");
|
|
200
|
+
return `${date.getFullYear()}${p(date.getMonth() + 1)}${p(date.getDate())}-${p(date.getHours())}${p(date.getMinutes())}${p(date.getSeconds())}`;
|
|
201
|
+
}
|
|
202
|
+
var CLOUD_POLL_MS = 3e4;
|
|
203
|
+
async function startCloudRun(options) {
|
|
204
|
+
const root = join(options.project, ".stuntdouble");
|
|
205
|
+
const configPath = join(root, "stuntdouble.json");
|
|
206
|
+
const checklistPath = join(root, "checklists", `${options.checklist}.json`);
|
|
207
|
+
const config = await readJson(configPath, ConfigSchema, ".stuntdouble/stuntdouble.json");
|
|
208
|
+
const checklist = await readJson(
|
|
209
|
+
checklistPath,
|
|
210
|
+
ChecklistSchema,
|
|
211
|
+
`.stuntdouble/checklists/${options.checklist}.json`
|
|
212
|
+
);
|
|
213
|
+
const actorPath = join(root, "actors", `${checklist.actor}.json`);
|
|
214
|
+
const actor = await readJson(
|
|
215
|
+
actorPath,
|
|
216
|
+
ActorSchema,
|
|
217
|
+
`.stuntdouble/actors/${checklist.actor}.json`
|
|
218
|
+
);
|
|
219
|
+
const target = new URL(checklist.url);
|
|
220
|
+
if (isPrivateHost(target.hostname)) {
|
|
221
|
+
throw new CloudRefusal(
|
|
222
|
+
`The cloud cannot reach ${checklist.url}. Set the checklist's URL to a public or preview address, and put the local one in stuntdouble.json's local_base_url so local runs keep using it.`
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
const credentials = await readCredentials();
|
|
226
|
+
const workspaceId = config.cloud?.workspace_id ?? credentials.cloud?.workspace?.id;
|
|
227
|
+
if (!workspaceId) throw new CloudRefusal("Choose a Stunt Double workspace in Settings first.");
|
|
228
|
+
return withCloud(async (call) => {
|
|
229
|
+
const workspaces = (await call("list_workspaces", {})).workspaces;
|
|
230
|
+
const workspace = workspaces.find((w) => w.id === workspaceId);
|
|
231
|
+
if (!workspace) throw new CloudRefusal("That workspace is not one of yours any more.");
|
|
232
|
+
const gate = planGate(workspace);
|
|
233
|
+
if (gate) throw new CloudRefusal(gate);
|
|
234
|
+
const billing = (err) => err instanceof CloudRefusal ? new CloudRefusal(`${err.message}
|
|
235
|
+
|
|
236
|
+
Plans and billing: ${billingUrl(workspace.slug)}`) : err;
|
|
237
|
+
try {
|
|
238
|
+
let projectId = config.cloud?.project_id;
|
|
239
|
+
if (!projectId) {
|
|
240
|
+
const project = await call("create_project", {
|
|
241
|
+
workspace_id: workspace.id,
|
|
242
|
+
name: basename(options.project),
|
|
243
|
+
url: target.origin
|
|
244
|
+
});
|
|
245
|
+
projectId = String(project.id);
|
|
246
|
+
}
|
|
247
|
+
await patchCloud(configPath, {
|
|
248
|
+
workspace_id: workspace.id,
|
|
249
|
+
workspace_slug: workspace.slug,
|
|
250
|
+
project_id: projectId
|
|
251
|
+
});
|
|
252
|
+
let actorId = actor.cloud?.actor_id;
|
|
253
|
+
if (!actorId) {
|
|
254
|
+
actorId = String(
|
|
255
|
+
(await call("create_actor", {
|
|
256
|
+
workspace_id: workspace.id,
|
|
257
|
+
name: actor.name,
|
|
258
|
+
...actor.description ? { description: actor.description } : {}
|
|
259
|
+
})).id
|
|
260
|
+
);
|
|
261
|
+
await patchCloud(actorPath, { actor_id: actorId });
|
|
262
|
+
if (actor.system_prompt) {
|
|
263
|
+
await call("update_actor", { actor_id: actorId, system_prompt: actor.system_prompt });
|
|
264
|
+
}
|
|
265
|
+
} else {
|
|
266
|
+
await call("update_actor", {
|
|
267
|
+
actor_id: actorId,
|
|
268
|
+
name: actor.name,
|
|
269
|
+
...actor.description ? { description: actor.description } : {},
|
|
270
|
+
...actor.system_prompt ? { system_prompt: actor.system_prompt } : {}
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
const known = actor.knowledge.length ? new Set(
|
|
274
|
+
((await call("list_actor_knowledge", { actor_id: actorId })).knowledge ?? []).map((k) => k.title)
|
|
275
|
+
) : /* @__PURE__ */ new Set();
|
|
276
|
+
for (const k of actor.knowledge.filter((k2) => !known.has(k2.title))) {
|
|
277
|
+
await call("add_actor_knowledge", {
|
|
278
|
+
actor_id: actorId,
|
|
279
|
+
title: k.title,
|
|
280
|
+
content: k.content,
|
|
281
|
+
kind: k.kind
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
const checkIds = checklist.cloud?.check_ids ?? {};
|
|
285
|
+
const checks = checklist.checks.map((c) => ({
|
|
286
|
+
...checkIds[c.key] ? { id: checkIds[c.key] } : {},
|
|
287
|
+
description: c.description,
|
|
288
|
+
type: c.type,
|
|
289
|
+
...c.weight != null ? { weight: c.weight } : {},
|
|
290
|
+
run_condition: c.run_condition,
|
|
291
|
+
on_failure: c.on_failure
|
|
292
|
+
}));
|
|
293
|
+
const fields = {
|
|
294
|
+
name: checklist.name,
|
|
295
|
+
actor_id: actorId,
|
|
296
|
+
instructions: checklist.instructions,
|
|
297
|
+
url: checklist.url,
|
|
298
|
+
checks,
|
|
299
|
+
...checklist.pass_threshold != null ? { pass_threshold: checklist.pass_threshold } : {},
|
|
300
|
+
score_type: checklist.score_type,
|
|
301
|
+
...checklist.score_unit != null ? { score_unit: checklist.score_unit } : {},
|
|
302
|
+
...checklist.score_bands ? { score_bands: checklist.score_bands } : {},
|
|
303
|
+
...checklist.run_mode ? { run_mode: checklist.run_mode } : {},
|
|
304
|
+
...checklist.record_session != null ? { record_session: checklist.record_session } : {}
|
|
305
|
+
};
|
|
306
|
+
let checklistId = checklist.cloud?.checklist_id;
|
|
307
|
+
if (checklistId) {
|
|
308
|
+
await call("update_checklist", { checklist_id: checklistId, ...fields });
|
|
309
|
+
} else {
|
|
310
|
+
checklistId = String(
|
|
311
|
+
(await call("create_checklist", {
|
|
312
|
+
workspace_id: workspace.id,
|
|
313
|
+
project_id: projectId,
|
|
314
|
+
...fields
|
|
315
|
+
})).checklist_id
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
await patchCloud(checklistPath, { checklist_id: checklistId });
|
|
319
|
+
const remote = (await call("get_checklist", { checklist_id: checklistId })).checks;
|
|
320
|
+
const ordered = [...remote].sort((a, b) => a.sort_order - b.sort_order);
|
|
321
|
+
const ids = Object.fromEntries(checklist.checks.map((c, i) => [c.key, ordered[i]?.id]));
|
|
322
|
+
await patchCloud(checklistPath, { check_ids: ids });
|
|
323
|
+
const cloudRunId = String(
|
|
324
|
+
(await call("run_checklist", { checklist_id: checklistId })).run_id
|
|
325
|
+
);
|
|
326
|
+
const id = `${stamp()}-${options.checklist}-cloud`;
|
|
327
|
+
const dir = join(root, "runs", id);
|
|
328
|
+
await mkdir(dir, { recursive: true });
|
|
329
|
+
const snapshot = { ...checklist, cloud: void 0 };
|
|
330
|
+
const keyOf = new Map(Object.entries(ids).map(([key, checkId]) => [checkId, key]));
|
|
331
|
+
const run = {
|
|
332
|
+
format: "stuntdouble.run/1",
|
|
333
|
+
id,
|
|
334
|
+
runner: {
|
|
335
|
+
kind: "cloud",
|
|
336
|
+
run_id: cloudRunId,
|
|
337
|
+
judged: true,
|
|
338
|
+
url: `${appOrigin()}/w/${workspace.slug}/checklists/${checklistId}/runs/${cloudRunId}`
|
|
339
|
+
},
|
|
340
|
+
status: "running",
|
|
341
|
+
pid: process.pid,
|
|
342
|
+
url: checklist.url,
|
|
343
|
+
started_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
344
|
+
finished_at: null,
|
|
345
|
+
checklist: options.checklist,
|
|
346
|
+
actor: checklist.actor,
|
|
347
|
+
checklist_snapshot: snapshot,
|
|
348
|
+
results: [],
|
|
349
|
+
score: null,
|
|
350
|
+
summary: null,
|
|
351
|
+
cloud: null
|
|
352
|
+
};
|
|
353
|
+
await writeJsonAtomic(join(dir, "run.json"), run);
|
|
354
|
+
await writeFileAtomic(runningMarker(root, id), String(process.pid));
|
|
355
|
+
const done = poll({
|
|
356
|
+
run,
|
|
357
|
+
dir,
|
|
358
|
+
root,
|
|
359
|
+
keyOf,
|
|
360
|
+
passThreshold: checklist.pass_threshold ?? 100,
|
|
361
|
+
pollMs: options.pollMs ?? CLOUD_POLL_MS
|
|
362
|
+
});
|
|
363
|
+
return { runId: id, done };
|
|
364
|
+
} catch (err) {
|
|
365
|
+
throw billing(err);
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
async function poll(input) {
|
|
370
|
+
const { run, dir, root, keyOf } = input;
|
|
371
|
+
try {
|
|
372
|
+
for (; ; ) {
|
|
373
|
+
await new Promise((ok) => setTimeout(ok, input.pollMs));
|
|
374
|
+
const remote = await withCloud(
|
|
375
|
+
(call) => call("get_checklist_run", { run_id: run.runner.run_id })
|
|
376
|
+
).catch((err) => ({ error: err.message }));
|
|
377
|
+
if ("error" in remote) continue;
|
|
378
|
+
const results = remote.results ?? [];
|
|
379
|
+
Object.assign(run, {
|
|
380
|
+
status: remote.status === "completed" || remote.status === "failed" ? remote.status : "running",
|
|
381
|
+
started_at: remote.started_at ?? run.started_at,
|
|
382
|
+
finished_at: remote.completed_at ?? null,
|
|
383
|
+
results: results.map((r) => ({
|
|
384
|
+
check: keyOf.get(r.check_id) ?? r.check_id,
|
|
385
|
+
status: r.status,
|
|
386
|
+
evidence: r.evidence ?? "",
|
|
387
|
+
at: r.evaluated_at
|
|
388
|
+
})),
|
|
389
|
+
score: remote.outcome ? {
|
|
390
|
+
score: remote.score ?? null,
|
|
391
|
+
unit: remote.score_unit ?? null,
|
|
392
|
+
outcome: remote.outcome,
|
|
393
|
+
pass_threshold: input.passThreshold,
|
|
394
|
+
grade: null
|
|
395
|
+
} : null,
|
|
396
|
+
summary: remote.summary ?? null
|
|
397
|
+
});
|
|
398
|
+
await writeJsonAtomic(join(dir, "run.json"), run);
|
|
399
|
+
if (run.status !== "running") break;
|
|
400
|
+
}
|
|
401
|
+
} finally {
|
|
402
|
+
await rm(runningMarker(root, run.id), { force: true });
|
|
403
|
+
}
|
|
404
|
+
const failed = run.results.filter((r) => r.status === "failed");
|
|
405
|
+
const scored = run.results.filter((r) => r.status !== "informational" && r.status !== "skipped");
|
|
406
|
+
return {
|
|
407
|
+
run,
|
|
408
|
+
report: [
|
|
409
|
+
`Run ${run.id} ${run.status}: ${run.score?.outcome ?? "no outcome"}. ${scored.filter((r) => r.status === "passed").length}/${scored.length} checks passed. Judged in the cloud by Stunt Double's judge.`,
|
|
410
|
+
failed.length ? `Failed:
|
|
411
|
+
${failed.map((r) => `- [${r.check}] ${r.evidence}`).join("\n")}` : "",
|
|
412
|
+
`Report: ${run.runner.url}`
|
|
413
|
+
].filter(Boolean).join("\n\n")
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
export {
|
|
418
|
+
cloudServer,
|
|
419
|
+
billingUrl,
|
|
420
|
+
CloudRefusal,
|
|
421
|
+
beginLogin,
|
|
422
|
+
completeLogin,
|
|
423
|
+
logout,
|
|
424
|
+
cloudStatus,
|
|
425
|
+
listWorkspaces,
|
|
426
|
+
chooseWorkspace,
|
|
427
|
+
planGate,
|
|
428
|
+
CLOUD_POLL_MS,
|
|
429
|
+
startCloudRun
|
|
430
|
+
};
|
|
431
|
+
//# sourceMappingURL=chunk-DFXASIGV.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/cloud.ts"],"sourcesContent":["// Cloud runs from the Studio: sign in to Stunt Double with OAuth 2.1 and PKCE\n// against the hosted MCP (dynamic client registration, a loopback redirect to\n// the Studio's own 127.0.0.1 port), then push the project's files and run them\n// through the hosted MCP tools, as `/stuntdouble cloud` does in Claude Code\n// (stuntdouble-mcp, skills/stuntdouble/SKILL.md). The token is kept in\n// `~/.stuntdouble/credentials.json` and never reaches the page.\n//\n// Cloud runs are billed to the workspace, never to anyone's model connection.\n\nimport { auth, type OAuthClientProvider } from '@modelcontextprotocol/sdk/client/auth.js';\nimport { Client } from '@modelcontextprotocol/sdk/client/index.js';\nimport { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';\nimport type {\n OAuthClientInformationMixed,\n OAuthClientMetadata,\n OAuthTokens,\n} from '@modelcontextprotocol/sdk/shared/auth.js';\nimport { isPrivateHost } from '@stuntkit/browser-toolset/executor';\nimport { randomBytes } from 'node:crypto';\nimport { mkdir, readFile, rm } from 'node:fs/promises';\nimport { basename, join } from 'node:path';\n\nimport { readCredentials, updateCredentials } from './credentials.ts';\nimport {\n ActorSchema,\n ChecklistSchema,\n ConfigSchema,\n readJson,\n type Run,\n runningMarker,\n writeFileAtomic,\n writeJsonAtomic,\n} from './files.ts';\nimport { PACKAGE } from './run.ts';\n\nexport const cloudServer = (): string =>\n process.env.STUNTDOUBLE_CLOUD_URL || 'https://app.stuntdouble.io/api/mcp';\nconst appOrigin = () => new URL(cloudServer()).origin;\nexport const billingUrl = (slug: string) => `${appOrigin()}/w/${slug}/settings/billing`;\n\n/** The hosted MCP said no: its words, relayed as they are. */\nexport class CloudRefusal extends Error {}\n\nclass StudioOAuth implements OAuthClientProvider {\n readonly stateValue = randomBytes(24).toString('base64url');\n authorizationUrl: URL | null = null;\n private verifier = '';\n\n private readonly redirect: string | undefined;\n private readonly server = cloudServer();\n\n constructor(redirect: string | undefined) {\n this.redirect = redirect;\n }\n\n get redirectUrl() {\n return this.redirect;\n }\n\n get clientMetadata(): OAuthClientMetadata {\n return {\n client_name: 'Stunt Double Studio',\n redirect_uris: this.redirect ? [this.redirect] : [],\n grant_types: ['authorization_code'],\n response_types: ['code'],\n token_endpoint_auth_method: 'none',\n };\n }\n\n state() {\n return this.stateValue;\n }\n\n private async cloud() {\n const cloud = (await readCredentials()).cloud;\n return cloud?.server === this.server ? cloud : undefined;\n }\n\n async clientInformation(): Promise<OAuthClientInformationMixed | undefined> {\n const client = (await this.cloud())?.client as OAuthClientInformationMixed | undefined;\n if (!client) return undefined;\n // Registered for another port: register again for this one.\n const uris = (client as { redirect_uris?: string[] }).redirect_uris;\n if (this.redirect && !uris?.includes(this.redirect)) return undefined;\n return client;\n }\n\n async saveClientInformation(client: OAuthClientInformationMixed) {\n await updateCredentials((c) => ({\n ...c,\n cloud: { ...(c.cloud?.server === this.server ? c.cloud : {}), server: this.server, client },\n }));\n }\n\n async tokens(): Promise<OAuthTokens | undefined> {\n return (await this.cloud())?.tokens as OAuthTokens | undefined;\n }\n\n async saveTokens(tokens: OAuthTokens) {\n await updateCredentials((c) => ({\n ...c,\n cloud: { ...(c.cloud?.server === this.server ? c.cloud : {}), server: this.server, tokens },\n }));\n }\n\n redirectToAuthorization(url: URL) {\n this.authorizationUrl = url;\n }\n\n saveCodeVerifier(verifier: string) {\n this.verifier = verifier;\n }\n\n codeVerifier() {\n return this.verifier;\n }\n\n async invalidateCredentials(scope: 'all' | 'client' | 'tokens' | 'verifier' | 'discovery') {\n if (scope === 'verifier' || scope === 'discovery') return;\n await updateCredentials((c) => {\n if (!c.cloud) return c;\n const { tokens: _t, client: _c, ...rest } = c.cloud;\n return {\n ...c,\n cloud: {\n ...rest,\n ...(scope === 'client' ? { tokens: c.cloud.tokens } : {}),\n ...(scope === 'tokens' ? { client: c.cloud.client } : {}),\n },\n };\n });\n }\n}\n\n/** Sign-ins waiting for their callback, by OAuth state. */\nconst pending = new Map<string, StudioOAuth>();\n\n/** The address to send the person to, to approve the Studio. */\nexport async function beginLogin(redirect: string): Promise<string> {\n // A fresh sign-in: an old token is dropped, a registration for this port kept.\n await updateCredentials((c) =>\n c.cloud ? { ...c, cloud: { ...c.cloud, tokens: undefined, workspace: undefined } } : c\n );\n const provider = new StudioOAuth(redirect);\n await auth(provider, { serverUrl: cloudServer() });\n if (!provider.authorizationUrl) throw new Error('Stunt Double did not offer a sign-in page.');\n // ponytail: kept in memory, a handful at most; a restart means signing in again.\n if (pending.size >= 5) pending.delete(pending.keys().next().value!);\n pending.set(provider.stateValue, provider);\n return provider.authorizationUrl.href;\n}\n\n/** The callback: the state must be one this process issued, or nothing happens. */\nexport async function completeLogin(state: string, code: string): Promise<void> {\n const provider = pending.get(state);\n if (!provider) throw new Error('This sign-in has expired. Start it again from the Studio.');\n pending.delete(state);\n await auth(provider, { serverUrl: cloudServer(), authorizationCode: code });\n const workspaces = await listWorkspaces();\n if (workspaces.length === 1) await chooseWorkspace(workspaces[0]!.id);\n}\n\nexport async function logout(): Promise<void> {\n await updateCredentials((c) => {\n const { cloud: _cloud, ...rest } = c;\n return rest;\n });\n}\n\nexport type Workspace = { id: string; name: string; slug: string; role?: string; plan?: string };\n\nexport async function cloudStatus() {\n const cloud = (await readCredentials()).cloud;\n const signedIn = Boolean(cloud?.server === cloudServer() && cloud.tokens);\n return { signed_in: signedIn, workspace: signedIn ? (cloud?.workspace ?? null) : null };\n}\n\ntype Call = (name: string, args: Record<string, unknown>) => Promise<Record<string, unknown>>;\n\nasync function withCloud<T>(work: (call: Call) => Promise<T>): Promise<T> {\n const provider = new StudioOAuth(undefined);\n if (!(await provider.tokens())) {\n throw new CloudRefusal('Sign in to Stunt Double first: Settings, or `stuntdouble login`.');\n }\n const client = new Client({ name: 'stuntdouble-studio', version: PACKAGE.split('@')[1]! });\n try {\n await client.connect(\n new StreamableHTTPClientTransport(new URL(cloudServer()), { authProvider: provider })\n );\n } catch (err) {\n throw new CloudRefusal(\n /unauthori[sz]ed|401/i.test(String(err))\n ? 'Your Stunt Double sign-in has expired: sign in again.'\n : `Could not reach Stunt Double: ${(err as Error).message}`\n );\n }\n const call: Call = async (name, args) => {\n const result = await client.callTool({ name, arguments: args });\n const text = (result.content as Array<{ type: string; text?: string }>)\n .map((c) => (c.type === 'text' ? (c.text ?? '') : ''))\n .join('\\n');\n if (result.isError) throw new CloudRefusal(text || `${name} was refused.`);\n if (result.structuredContent) return result.structuredContent as Record<string, unknown>;\n try {\n return JSON.parse(text) as Record<string, unknown>;\n } catch {\n return { text };\n }\n };\n try {\n return await work(call);\n } finally {\n await client.close().catch(() => {});\n }\n}\n\nexport async function listWorkspaces(): Promise<Workspace[]> {\n return withCloud(async (call) => (await call('list_workspaces', {})).workspaces as Workspace[]);\n}\n\nexport async function chooseWorkspace(id: string): Promise<Workspace> {\n const workspace = (await listWorkspaces()).find((w) => w.id === id || w.slug === id);\n if (!workspace) throw new CloudRefusal('That workspace is not one of yours.');\n await updateCredentials((c) => ({\n ...c,\n cloud: c.cloud && {\n ...c.cloud,\n workspace: { id: workspace.id, slug: workspace.slug, name: workspace.name },\n },\n }));\n return workspace;\n}\n\n/**\n * Whether this workspace may run in the cloud. The hosted MCP does not say\n * which plan a workspace is on yet: when it sends `plan`, a free workspace is\n * told here; when it does not, run_checklist's own allowance check decides,\n * and its refusal is relayed with the billing link.\n */\nexport function planGate(workspace: Workspace): string | null {\n if (workspace.plan && /^free$/i.test(workspace.plan)) {\n return `Cloud runs from the Studio need a paid plan, and ${workspace.name} is on the Free plan. Upgrade at ${billingUrl(workspace.slug)}, or run it locally.`;\n }\n return null;\n}\n\n/** Rewrite a file's `cloud` ids, keeping everything else exactly as it was. */\nasync function patchCloud(path: string, patch: Record<string, unknown>): Promise<void> {\n const raw = JSON.parse(await readFile(path, 'utf8')) as Record<string, unknown>;\n const cloud = (raw.cloud as Record<string, unknown> | null | undefined) ?? {};\n await writeJsonAtomic(path, { ...raw, cloud: { ...cloud, ...patch } });\n}\n\nfunction stamp(date = new Date()): string {\n const p = (n: number) => String(n).padStart(2, '0');\n return `${date.getFullYear()}${p(date.getMonth() + 1)}${p(date.getDate())}-${p(date.getHours())}${p(date.getMinutes())}${p(date.getSeconds())}`;\n}\n\nexport const CLOUD_POLL_MS = 30_000;\n\nexport type CloudRun = { runId: string; done: Promise<{ report: string; run: Run }> };\n\n/**\n * Push the checklist, its actor and the project to the workspace, reusing the\n * cloud ids already in the files, start a run and pull it back as\n * `runs/<stamp>-<checklist>-cloud/run.json` beside the local runs.\n */\nexport async function startCloudRun(options: {\n project: string;\n checklist: string;\n pollMs?: number;\n}): Promise<CloudRun> {\n const root = join(options.project, '.stuntdouble');\n const configPath = join(root, 'stuntdouble.json');\n const checklistPath = join(root, 'checklists', `${options.checklist}.json`);\n const config = await readJson(configPath, ConfigSchema, '.stuntdouble/stuntdouble.json');\n const checklist = await readJson(\n checklistPath,\n ChecklistSchema,\n `.stuntdouble/checklists/${options.checklist}.json`\n );\n const actorPath = join(root, 'actors', `${checklist.actor}.json`);\n const actor = await readJson(\n actorPath,\n ActorSchema,\n `.stuntdouble/actors/${checklist.actor}.json`\n );\n\n const target = new URL(checklist.url);\n if (isPrivateHost(target.hostname)) {\n throw new CloudRefusal(\n `The cloud cannot reach ${checklist.url}. Set the checklist's URL to a public or preview address, and put the local one in stuntdouble.json's local_base_url so local runs keep using it.`\n );\n }\n\n const credentials = await readCredentials();\n const workspaceId =\n (config.cloud?.workspace_id as string | undefined) ?? credentials.cloud?.workspace?.id;\n if (!workspaceId) throw new CloudRefusal('Choose a Stunt Double workspace in Settings first.');\n\n return withCloud(async (call) => {\n const workspaces = (await call('list_workspaces', {})).workspaces as Workspace[];\n const workspace = workspaces.find((w) => w.id === workspaceId);\n if (!workspace) throw new CloudRefusal('That workspace is not one of yours any more.');\n const gate = planGate(workspace);\n if (gate) throw new CloudRefusal(gate);\n const billing = (err: unknown) =>\n err instanceof CloudRefusal\n ? new CloudRefusal(`${err.message}\\n\\nPlans and billing: ${billingUrl(workspace.slug)}`)\n : err;\n\n try {\n // Project.\n let projectId = config.cloud?.project_id as string | undefined;\n if (!projectId) {\n const project = await call('create_project', {\n workspace_id: workspace.id,\n name: basename(options.project),\n url: target.origin,\n });\n projectId = String(project.id);\n }\n await patchCloud(configPath, {\n workspace_id: workspace.id,\n workspace_slug: workspace.slug,\n project_id: projectId,\n });\n\n // Actor, then its instructions and knowledge.\n let actorId = actor.cloud?.actor_id as string | undefined;\n if (!actorId) {\n actorId = String(\n (\n await call('create_actor', {\n workspace_id: workspace.id,\n name: actor.name,\n ...(actor.description ? { description: actor.description } : {}),\n })\n ).id\n );\n await patchCloud(actorPath, { actor_id: actorId });\n if (actor.system_prompt) {\n await call('update_actor', { actor_id: actorId, system_prompt: actor.system_prompt });\n }\n } else {\n await call('update_actor', {\n actor_id: actorId,\n name: actor.name,\n ...(actor.description ? { description: actor.description } : {}),\n ...(actor.system_prompt ? { system_prompt: actor.system_prompt } : {}),\n });\n }\n const known = actor.knowledge.length\n ? new Set(\n (\n ((await call('list_actor_knowledge', { actor_id: actorId })).knowledge ??\n []) as Array<{\n title: string;\n }>\n ).map((k) => k.title)\n )\n : new Set<string>();\n for (const k of actor.knowledge.filter((k) => !known.has(k.title))) {\n await call('add_actor_knowledge', {\n actor_id: actorId,\n title: k.title,\n content: k.content,\n kind: k.kind,\n });\n }\n\n // Checklist, its checks in file order, then their ids back by key.\n const checkIds = (checklist.cloud?.check_ids ?? {}) as Record<string, string>;\n const checks = checklist.checks.map((c) => ({\n ...(checkIds[c.key] ? { id: checkIds[c.key] } : {}),\n description: c.description,\n type: c.type,\n ...(c.weight != null ? { weight: c.weight } : {}),\n run_condition: c.run_condition,\n on_failure: c.on_failure,\n }));\n const fields = {\n name: checklist.name,\n actor_id: actorId,\n instructions: checklist.instructions,\n url: checklist.url,\n checks,\n ...(checklist.pass_threshold != null ? { pass_threshold: checklist.pass_threshold } : {}),\n score_type: checklist.score_type,\n ...(checklist.score_unit != null ? { score_unit: checklist.score_unit } : {}),\n ...(checklist.score_bands ? { score_bands: checklist.score_bands } : {}),\n ...(checklist.run_mode ? { run_mode: checklist.run_mode } : {}),\n ...(checklist.record_session != null ? { record_session: checklist.record_session } : {}),\n };\n let checklistId = checklist.cloud?.checklist_id as string | undefined;\n if (checklistId) {\n await call('update_checklist', { checklist_id: checklistId, ...fields });\n } else {\n checklistId = String(\n (\n await call('create_checklist', {\n workspace_id: workspace.id,\n project_id: projectId,\n ...fields,\n })\n ).checklist_id\n );\n }\n await patchCloud(checklistPath, { checklist_id: checklistId });\n const remote = (await call('get_checklist', { checklist_id: checklistId })).checks as Array<{\n id: string;\n sort_order: number;\n }>;\n const ordered = [...remote].sort((a, b) => a.sort_order - b.sort_order);\n const ids = Object.fromEntries(checklist.checks.map((c, i) => [c.key, ordered[i]?.id]));\n await patchCloud(checklistPath, { check_ids: ids });\n\n // The run, pulled back beside the local ones.\n const cloudRunId = String(\n (await call('run_checklist', { checklist_id: checklistId })).run_id\n );\n const id = `${stamp()}-${options.checklist}-cloud`;\n const dir = join(root, 'runs', id);\n await mkdir(dir, { recursive: true });\n const snapshot = { ...checklist, cloud: undefined };\n const keyOf = new Map(Object.entries(ids).map(([key, checkId]) => [checkId, key]));\n const run: Run = {\n format: 'stuntdouble.run/1',\n id,\n runner: {\n kind: 'cloud',\n run_id: cloudRunId,\n judged: true,\n url: `${appOrigin()}/w/${workspace.slug}/checklists/${checklistId}/runs/${cloudRunId}`,\n },\n status: 'running',\n pid: process.pid,\n url: checklist.url,\n started_at: new Date().toISOString(),\n finished_at: null,\n checklist: options.checklist,\n actor: checklist.actor,\n checklist_snapshot: snapshot,\n results: [],\n score: null,\n summary: null,\n cloud: null,\n };\n await writeJsonAtomic(join(dir, 'run.json'), run);\n // The live marker makes the viewer show it as running while this process polls.\n await writeFileAtomic(runningMarker(root, id), String(process.pid));\n const done = poll({\n run,\n dir,\n root,\n keyOf,\n passThreshold: checklist.pass_threshold ?? 100,\n pollMs: options.pollMs ?? CLOUD_POLL_MS,\n });\n return { runId: id, done };\n } catch (err) {\n throw billing(err);\n }\n });\n}\n\nasync function poll(input: {\n run: Run;\n dir: string;\n root: string;\n keyOf: Map<string, string>;\n passThreshold: number;\n pollMs: number;\n}): Promise<{ report: string; run: Run }> {\n const { run, dir, root, keyOf } = input;\n try {\n for (;;) {\n await new Promise((ok) => setTimeout(ok, input.pollMs));\n const remote = await withCloud((call) =>\n call('get_checklist_run', { run_id: run.runner.run_id })\n ).catch((err: Error) => ({ error: err.message }));\n if ('error' in remote) continue;\n const results = (remote.results ?? []) as Array<{\n check_id: string;\n status: string;\n evidence: string | null;\n evaluated_at: string;\n }>;\n Object.assign(run, {\n status:\n remote.status === 'completed' || remote.status === 'failed' ? remote.status : 'running',\n started_at: (remote.started_at as string | null) ?? run.started_at,\n finished_at: (remote.completed_at as string | null) ?? null,\n results: results.map((r) => ({\n check: keyOf.get(r.check_id) ?? r.check_id,\n status: r.status,\n evidence: r.evidence ?? '',\n at: r.evaluated_at,\n })),\n score: remote.outcome\n ? {\n score: (remote.score as number | null) ?? null,\n unit: (remote.score_unit as string | null) ?? null,\n outcome: remote.outcome,\n pass_threshold: input.passThreshold,\n grade: null,\n }\n : null,\n summary: (remote.summary as string | null) ?? null,\n });\n await writeJsonAtomic(join(dir, 'run.json'), run);\n if (run.status !== 'running') break;\n }\n } finally {\n await rm(runningMarker(root, run.id), { force: true });\n }\n const failed = run.results.filter((r) => r.status === 'failed');\n const scored = run.results.filter((r) => r.status !== 'informational' && r.status !== 'skipped');\n return {\n run,\n report: [\n `Run ${run.id} ${run.status}: ${run.score?.outcome ?? 'no outcome'}. ${scored.filter((r) => r.status === 'passed').length}/${scored.length} checks passed. Judged in the cloud by Stunt Double's judge.`,\n failed.length\n ? `Failed:\\n${failed.map((r) => `- [${r.check}] ${r.evidence}`).join('\\n')}`\n : '',\n `Report: ${run.runner.url}`,\n ]\n .filter(Boolean)\n .join('\\n\\n'),\n };\n}\n"],"mappings":"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","names":["k"]}
|