@flowriki/agent 0.1.0-beta.34 → 0.1.0-beta.36
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/dist/{chunk-7OODSC7P.js → chunk-N3P4O3OH.js} +2 -1
- package/dist/cli.js +396 -49
- package/dist/engine.js +1 -1
- package/package.json +1 -1
|
@@ -6,7 +6,7 @@ import os from "os";
|
|
|
6
6
|
import path from "path";
|
|
7
7
|
import { createRequire } from "module";
|
|
8
8
|
import { spawnSync } from "child_process";
|
|
9
|
-
var AGENT_VERSION = "0.1.0-beta.
|
|
9
|
+
var AGENT_VERSION = "0.1.0-beta.36";
|
|
10
10
|
async function prepareBrowsers() {
|
|
11
11
|
if (browsersReady()) {
|
|
12
12
|
return { ok: true, already: true, message: "chromium is already installed" };
|
|
@@ -11278,6 +11278,7 @@ export {
|
|
|
11278
11278
|
closeRunSession,
|
|
11279
11279
|
replayFlow,
|
|
11280
11280
|
executeStep,
|
|
11281
|
+
replayCase,
|
|
11281
11282
|
fieldNames,
|
|
11282
11283
|
helperRunner,
|
|
11283
11284
|
resolveCheckouts,
|
package/dist/cli.js
CHANGED
|
@@ -15,13 +15,14 @@ import {
|
|
|
15
15
|
mirrorCheckTargets,
|
|
16
16
|
openRunSession,
|
|
17
17
|
prepareBrowsers,
|
|
18
|
+
replayCase,
|
|
18
19
|
replayFlow,
|
|
19
20
|
resolveCheckouts,
|
|
20
21
|
resolveService,
|
|
21
22
|
runtimeValue,
|
|
22
23
|
start,
|
|
23
24
|
status
|
|
24
|
-
} from "./chunk-
|
|
25
|
+
} from "./chunk-N3P4O3OH.js";
|
|
25
26
|
|
|
26
27
|
// src/cli.ts
|
|
27
28
|
import { parseArgs } from "util";
|
|
@@ -172,12 +173,45 @@ var ServiceClient = class {
|
|
|
172
173
|
{ method: "DELETE" }
|
|
173
174
|
);
|
|
174
175
|
}
|
|
175
|
-
saveFlow(projectId, title, flow, where = {}) {
|
|
176
|
+
saveFlow(projectId, title, flow, where = {}, secretValues = {}) {
|
|
176
177
|
return this.call(
|
|
177
178
|
`/api/agent/flows?projectId=${encodeURIComponent(projectId)}`,
|
|
178
|
-
{ method: "POST", body: JSON.stringify({ title, flow, ...where }) }
|
|
179
|
+
{ method: "POST", body: JSON.stringify({ title, flow, ...where, secretValues }) }
|
|
179
180
|
);
|
|
180
181
|
}
|
|
182
|
+
/** A sign-in made from a recorded login flow. */
|
|
183
|
+
createSignIn(projectId, input) {
|
|
184
|
+
return this.call(
|
|
185
|
+
`/api/agent/sign-ins?projectId=${encodeURIComponent(projectId)}`,
|
|
186
|
+
{ method: "POST", body: JSON.stringify(input) }
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
/** An account for a sign-in; secret values are sealed by the service and
|
|
190
|
+
* never come back. */
|
|
191
|
+
createAccount(projectId, input) {
|
|
192
|
+
return this.call(
|
|
193
|
+
`/api/agent/accounts?projectId=${encodeURIComponent(projectId)}`,
|
|
194
|
+
{ method: "POST", body: JSON.stringify(input) }
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
/** A helper, its secrets sealed by the service. */
|
|
198
|
+
createHelper(projectId, input) {
|
|
199
|
+
return this.call(
|
|
200
|
+
`/api/agent/helpers?projectId=${encodeURIComponent(projectId)}`,
|
|
201
|
+
{ method: "POST", body: JSON.stringify(input) }
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
/** What a new flow starts after: the flows, in run order, and their steps. */
|
|
205
|
+
flowsAfter(projectId, after) {
|
|
206
|
+
return this.call(
|
|
207
|
+
`/api/agent/flows/after?projectId=${encodeURIComponent(projectId)}`,
|
|
208
|
+
{ method: "POST", body: JSON.stringify({ after }) }
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
/** Build a test case from flows — as a dry run, or saved. */
|
|
212
|
+
buildCase(projectId, spec, dryRun) {
|
|
213
|
+
return this.call(`/api/agent/cases?projectId=${encodeURIComponent(projectId)}`, { method: "POST", body: JSON.stringify({ ...spec, dryRun }) });
|
|
214
|
+
}
|
|
181
215
|
/** Gather Claude's open branches here into one. */
|
|
182
216
|
gatherChanges(projectId, opts = {}) {
|
|
183
217
|
return this.call(
|
|
@@ -203,6 +237,9 @@ function topLevelBlocks(tree) {
|
|
|
203
237
|
};
|
|
204
238
|
}
|
|
205
239
|
var STEP_TIMEOUT_MS = 1e4;
|
|
240
|
+
var SECRET_WORDS = /password|passwd|pass[-_\s]?code|pwd|secret|otp|one[-_\s]?time[-_\s]?(code|pass)|security[-_\s]?(code|answer)|cvv|cvc|\bpin\b/i;
|
|
241
|
+
var TYPED = "typed_";
|
|
242
|
+
var withoutTyped = (steps) => steps.map((st) => st.secretRef?.startsWith(TYPED) ? (({ secretRef: _, ...rest }) => rest)(st) : st);
|
|
206
243
|
var ACTIONS = ["click", "dblclick", "rightclick", "hover", "fill", "select", "check", "press", "navigate", "back", "forward"];
|
|
207
244
|
var isAction = (value) => ACTIONS.includes(String(value));
|
|
208
245
|
var LANDMARKS = /* @__PURE__ */ new Set(["main", "navigation", "banner", "contentinfo", "complementary", "search", "form", "dialog", "alertdialog", "region", "group", "list", "table", "menu", "toolbar", "tablist"]);
|
|
@@ -226,12 +263,18 @@ function labelTree(tree, fields) {
|
|
|
226
263
|
for (const f of fields) out = out.split(`- ${f.role} ${JSON.stringify(f.hint)}`).join(`- ${f.role} ${JSON.stringify(f.label)}`);
|
|
227
264
|
return out;
|
|
228
265
|
}
|
|
266
|
+
function describeRows(rows) {
|
|
267
|
+
return rows.map((r) => [
|
|
268
|
+
`Row ${r.row}: "${r.flow}" (${r.browser})`,
|
|
269
|
+
...r.checks.length ? r.checks.map((c) => ` ${c.on ? "[on] " : "[off]"} ${c.n}. ${c.check}`) : [" (no checks in this flow)"]
|
|
270
|
+
].join("\n")).join("\n");
|
|
271
|
+
}
|
|
229
272
|
function describe(step) {
|
|
230
273
|
const target = step.name ? `${step.role || "element"} "${step.name}"` : step.role || "";
|
|
231
274
|
const where = step.scope ? ` in "${step.scope}"` : step.scopeRole ? ` in the ${step.scopeRole}` : "";
|
|
232
275
|
switch (step.type) {
|
|
233
276
|
case "fill":
|
|
234
|
-
return `type "${step.value}" into ${target}${where}`;
|
|
277
|
+
return step.secret ? `type a secret (\u2022\u2022\u2022\u2022\u2022\u2022) into ${target}${where}` : `type "${step.value}" into ${target}${where}`;
|
|
235
278
|
case "select":
|
|
236
279
|
return `choose "${step.value}" in ${target}${where}`;
|
|
237
280
|
case "check":
|
|
@@ -263,8 +306,23 @@ var AuthoringSession = class {
|
|
|
263
306
|
values = /* @__PURE__ */ new Map();
|
|
264
307
|
resolvers = null;
|
|
265
308
|
lastTree = null;
|
|
309
|
+
/** Values typed into secret fields, by their `typed:` reference. */
|
|
310
|
+
typed = /* @__PURE__ */ new Map();
|
|
266
311
|
draft = null;
|
|
267
312
|
catalog = null;
|
|
313
|
+
/** Text going back to Claude, with every secret typed here masked — a
|
|
314
|
+
* password box's value is in the page's tree, and a careless form puts it
|
|
315
|
+
* in the address. */
|
|
316
|
+
redact(text2) {
|
|
317
|
+
let out = text2;
|
|
318
|
+
for (const value of this.typed.values()) {
|
|
319
|
+
if (value.length < 3) continue;
|
|
320
|
+
out = out.split(value).join("\u2022\u2022\u2022\u2022\u2022\u2022");
|
|
321
|
+
const encoded = encodeURIComponent(value);
|
|
322
|
+
if (encoded !== value) out = out.split(encoded).join("\u2022\u2022\u2022\u2022\u2022\u2022");
|
|
323
|
+
}
|
|
324
|
+
return out;
|
|
325
|
+
}
|
|
268
326
|
get open() {
|
|
269
327
|
return this.run !== null;
|
|
270
328
|
}
|
|
@@ -277,11 +335,11 @@ var AuthoringSession = class {
|
|
|
277
335
|
* helper or sign-in appears, never kept beyond this process. */
|
|
278
336
|
async prepare(extra = []) {
|
|
279
337
|
const draft = this.draft;
|
|
280
|
-
const prepared = await this.service.prepare(draft.projectId, [...draft.steps, ...extra], draft.signIn ?? null);
|
|
338
|
+
const prepared = await this.service.prepare(draft.projectId, withoutTyped([...draft.preparation ?? [], ...draft.steps, ...extra]), draft.signIn ?? null);
|
|
281
339
|
const runHelper = helperRunner(prepared.helpers, prepared.secrets);
|
|
282
340
|
this.resolvers = {
|
|
283
341
|
resolveAuth: authenticationResolver(prepared.authentications),
|
|
284
|
-
resolveSecret: (ref) => prepared.secrets[ref],
|
|
342
|
+
resolveSecret: (ref) => this.typed.get(ref) ?? prepared.secrets[ref],
|
|
285
343
|
...runHelper ? { runHelper } : {}
|
|
286
344
|
};
|
|
287
345
|
return this.resolvers;
|
|
@@ -297,6 +355,7 @@ var AuthoringSession = class {
|
|
|
297
355
|
this.catalog = catalog;
|
|
298
356
|
this.draft = { ...input, steps: [] };
|
|
299
357
|
this.values = /* @__PURE__ */ new Map();
|
|
358
|
+
this.typed = /* @__PURE__ */ new Map();
|
|
300
359
|
this.lastTree = null;
|
|
301
360
|
const base = this.baseUrl();
|
|
302
361
|
this.run = await openRunSession({ steps: [], headless: this.opts.headless ?? false });
|
|
@@ -326,6 +385,24 @@ var AuthoringSession = class {
|
|
|
326
385
|
} else {
|
|
327
386
|
await this.page.goto(new URL(input.startPath || "/", base).href, { waitUntil: "domcontentloaded", timeout: 45e3 });
|
|
328
387
|
}
|
|
388
|
+
if (input.preparation?.length) {
|
|
389
|
+
const resolvers = await this.prepare(input.preparation);
|
|
390
|
+
for (const step of input.preparation) {
|
|
391
|
+
try {
|
|
392
|
+
this.page = await executeStep(this.page, step, {
|
|
393
|
+
pages: this.run.pages,
|
|
394
|
+
...resolvers,
|
|
395
|
+
values: this.values,
|
|
396
|
+
baseUrl: base,
|
|
397
|
+
timeoutMs: 2e4,
|
|
398
|
+
navigationTimeoutMs: 45e3
|
|
399
|
+
});
|
|
400
|
+
} catch (error) {
|
|
401
|
+
throw new Error(`The flows this one starts after did not finish, at "${step.name || step.type}": ${error.message.split("\n")[0]}`);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
await this.settle();
|
|
405
|
+
}
|
|
329
406
|
return this.snapshot();
|
|
330
407
|
}
|
|
331
408
|
/** The page as Claude reads it: where it is, and what is on it by role and name. */
|
|
@@ -388,13 +465,17 @@ ${removed.join("\n")}` : "",
|
|
|
388
465
|
throw new Error(`${input.action} needs a value`);
|
|
389
466
|
}
|
|
390
467
|
const target = navigating ? { role: "", name: "" } : asLabelled({ role: input.role ?? "", name: input.name ?? "" }, await fieldNames(page));
|
|
468
|
+
const secret = input.action === "fill" && (input.secret ?? (SECRET_WORDS.test(input.name ?? "") || SECRET_WORDS.test(target.name)));
|
|
469
|
+
const id = `s${draft.steps.length + 1}_${Date.now().toString(36)}`;
|
|
470
|
+
if (secret) this.typed.set(`${TYPED}${id}`, input.value ?? "");
|
|
391
471
|
const step = RecordedStepSchema.parse({
|
|
392
|
-
id
|
|
472
|
+
id,
|
|
393
473
|
type: input.action,
|
|
394
474
|
...target,
|
|
395
475
|
...scopeOf(input.within),
|
|
396
|
-
value: input.action === "check" ? input.value === "false" ? "false" : "true" : input.value ?? "",
|
|
397
|
-
secret
|
|
476
|
+
value: secret ? "" : input.action === "check" ? input.value === "false" ? "false" : "true" : input.value ?? "",
|
|
477
|
+
secret,
|
|
478
|
+
...secret ? { secretRef: `${TYPED}${id}` } : {},
|
|
398
479
|
legacySelector: "",
|
|
399
480
|
proposals: [],
|
|
400
481
|
// A path is the application's own, so the step replays on whichever
|
|
@@ -407,10 +488,12 @@ ${removed.join("\n")}` : "",
|
|
|
407
488
|
values: this.values,
|
|
408
489
|
baseUrl: this.baseUrl(),
|
|
409
490
|
...this.resolvers ?? {},
|
|
491
|
+
resolveSecret: (ref) => this.typed.get(ref) ?? this.resolvers?.resolveSecret(ref),
|
|
410
492
|
timeoutMs: STEP_TIMEOUT_MS,
|
|
411
493
|
navigationTimeoutMs: 45e3
|
|
412
494
|
});
|
|
413
495
|
} catch (error) {
|
|
496
|
+
if (secret) this.typed.delete(`${TYPED}${id}`);
|
|
414
497
|
throw new Error(`That did not work, and was not kept: ${error.message.split("\n")[0]}
|
|
415
498
|
|
|
416
499
|
${await this.view(false)}`);
|
|
@@ -524,6 +607,7 @@ ${this.list()}`;
|
|
|
524
607
|
const resolvers = await this.prepare();
|
|
525
608
|
const steps = [
|
|
526
609
|
...draft.signIn ? [RecordedStepSchema.parse({ id: "sign-in", type: "signIn", auth: { ...draft.signIn, from: "loginPage" }, role: "", name: "Sign in", scope: "", value: "", secret: false, legacySelector: "", proposals: [] })] : [],
|
|
610
|
+
...draft.preparation ?? [],
|
|
527
611
|
...draft.steps
|
|
528
612
|
];
|
|
529
613
|
const outcome = await replayFlow({ steps, baseUrl: this.baseUrl(), startPath: draft.startPath, headless: true, ...resolvers });
|
|
@@ -538,21 +622,77 @@ ${lines.join("\n")}`;
|
|
|
538
622
|
if (!draft.steps.some((s) => s.type === "assert" || s.proposals.length > 0)) {
|
|
539
623
|
throw new Error("the flow checks nothing \u2014 add at least one check (add_check) that proves it worked, then save");
|
|
540
624
|
}
|
|
625
|
+
const checksOff = draft.steps.map((step) => step.type === "assert" ? { ...step, proposals: step.proposals.map((p) => ({ ...p, enabled: false })) } : step);
|
|
626
|
+
const checkCount = checksOff.filter((step) => step.type === "assert").length;
|
|
627
|
+
const secretValues = {};
|
|
628
|
+
for (const step of checksOff) {
|
|
629
|
+
const value = step.secretRef ? this.typed.get(step.secretRef) : void 0;
|
|
630
|
+
if (value !== void 0) secretValues[step.id] = value;
|
|
631
|
+
}
|
|
541
632
|
const saved = await this.service.saveFlow(draft.projectId, title || "Tests written with Claude", {
|
|
542
633
|
name: draft.name,
|
|
543
634
|
...draft.featureId ? { featureId: draft.featureId } : { featureName: draft.featureName },
|
|
544
635
|
startPath: draft.startPath,
|
|
545
|
-
steps:
|
|
546
|
-
...draft.signIn ? { signIn: draft.signIn } : {}
|
|
547
|
-
|
|
636
|
+
steps: withoutTyped(checksOff),
|
|
637
|
+
...draft.signIn ? { signIn: draft.signIn } : {},
|
|
638
|
+
...draft.after?.length ? { prerequisites: draft.after.map((a) => ({ flowId: a.flowId })) } : {}
|
|
639
|
+
}, where, secretValues);
|
|
548
640
|
const review = `${this.service.origin}/projects/${draft.projectId}/changes/${saved.changeId}`;
|
|
549
641
|
const what = saved.replaced ? `Replaced the earlier "${draft.name}"` : `Saved "${draft.name}"`;
|
|
642
|
+
const sealed = Object.keys(secretValues).length;
|
|
643
|
+
const vault = sealed ? ` ${sealed} secret value${sealed === 1 ? " was" : "s were"} put in the project's vault, not the flow.` : "";
|
|
550
644
|
const where_ = saved.newBranch ? `on a new branch, ${saved.branch}` : `on Claude's working branch, ${saved.branch}`;
|
|
551
645
|
const others = await this.service.agentChanges(draft.projectId).then((r) => r.changes.filter((c) => c.id !== saved.changeId)).catch(() => []);
|
|
552
646
|
const note = others.length ? `
|
|
553
647
|
Claude has ${others.length} other open branch${others.length === 1 ? "" : "es"} on this project \u2014 gather_branches puts them into this one, for one review.` : "";
|
|
554
|
-
|
|
648
|
+
const after = draft.after?.length ? ` It starts after ${draft.after.map((a) => `"${a.name}"`).join(", ")}.` : "";
|
|
649
|
+
return `${what} ${where_}.${after}${vault} Its ${checkCount} check${checkCount === 1 ? " is" : "s are"} saved switched off \u2014 a test case switches on the ones it is about. Nothing is published yet.
|
|
555
650
|
Review and publish it here: ${review}${note}`;
|
|
651
|
+
}
|
|
652
|
+
/** A test case built from flows, without saving: its rows and every check,
|
|
653
|
+
* numbered, with which are on — what save_case's `checks` refer to. */
|
|
654
|
+
async previewCase(projectId, spec) {
|
|
655
|
+
const built = await this.service.buildCase(projectId, spec, true);
|
|
656
|
+
return `"${spec.name}" in ${spec.folder} would be:
|
|
657
|
+
${describeRows(built.rows)}`;
|
|
658
|
+
}
|
|
659
|
+
/** RUN THE CASE, THEN SAVE IT. Built as a dry run, replayed in a fresh
|
|
660
|
+
* browser exactly as a test run does it, and saved onto Claude's working
|
|
661
|
+
* branch only when every row passed. */
|
|
662
|
+
async saveCase(projectId, spec, catalog) {
|
|
663
|
+
const built = await this.service.buildCase(projectId, spec, true);
|
|
664
|
+
const testCase = built.testCase;
|
|
665
|
+
const steps = testCase.occurrences.flatMap((o) => o.steps);
|
|
666
|
+
const prepared = await this.service.prepare(projectId, steps, null);
|
|
667
|
+
const runHelper = helperRunner(prepared.helpers, prepared.secrets);
|
|
668
|
+
const base = catalog.baseUrl;
|
|
669
|
+
if (!base) throw new Error("this project has no base URL \u2014 set one in the project's settings first");
|
|
670
|
+
const outcome = await replayCase({
|
|
671
|
+
occurrences: testCase.occurrences,
|
|
672
|
+
...testCase.actors ? { actors: testCase.actors } : {},
|
|
673
|
+
...testCase.defaultActorId ? { defaultActorId: testCase.defaultActorId } : {},
|
|
674
|
+
baseUrl: base,
|
|
675
|
+
headless: true,
|
|
676
|
+
resolveAuth: authenticationResolver(prepared.authentications),
|
|
677
|
+
resolveSecret: (ref) => prepared.secrets[ref],
|
|
678
|
+
...runHelper ? { runHelper } : {}
|
|
679
|
+
});
|
|
680
|
+
const lines = outcome.occurrences.map((o, i) => {
|
|
681
|
+
const failed = o.steps.find((st) => st.status === "failed");
|
|
682
|
+
const why = o.status === "failed" ? `: ${failed ? `step "${failed.name || failed.type}" \u2014 ${(failed.detail ?? "").split("\n")[0]}` : (o.error ?? "").split("\n")[0]}` : "";
|
|
683
|
+
return `${o.status === "passed" ? "\u2713" : o.status === "failed" ? "\u2717" : "\xB7"} row ${i + 1} \u2014 ${o.name}${why}`;
|
|
684
|
+
});
|
|
685
|
+
if (outcome.status !== "passed") {
|
|
686
|
+
return `The case did not pass, so it was NOT saved.
|
|
687
|
+
${lines.join("\n")}
|
|
688
|
+
|
|
689
|
+
${describeRows(built.rows)}`;
|
|
690
|
+
}
|
|
691
|
+
const saved = await this.service.buildCase(projectId, spec, false);
|
|
692
|
+
const review = `${this.service.origin}${saved.path}`;
|
|
693
|
+
return `The case passed from the start${saved.replaced ? " and replaced the earlier version" : ""}, and is saved on ${saved.newBranch ? "a new branch" : "Claude's working branch"}. Nothing is published yet.
|
|
694
|
+
${lines.join("\n")}
|
|
695
|
+
Review and publish it here: ${review}`;
|
|
556
696
|
}
|
|
557
697
|
async close() {
|
|
558
698
|
const run = this.run;
|
|
@@ -572,47 +712,107 @@ Review and publish it here: ${review}${note}`;
|
|
|
572
712
|
|
|
573
713
|
// src/mcp/tools.ts
|
|
574
714
|
var INSTRUCTIONS = `Flowriki tools write UI tests by driving a real browser on this computer.
|
|
575
|
-
Every step you take is performed first and kept only if it worked, so
|
|
576
|
-
|
|
577
|
-
and publishes in the Flowriki dashboard \u2014 nothing you do is
|
|
715
|
+
Every step you take is performed first and kept only if it worked, so what you
|
|
716
|
+
save already ran once. Everything goes to ONE working branch per project that a
|
|
717
|
+
person reviews and publishes in the Flowriki dashboard \u2014 nothing you do is
|
|
718
|
+
published.
|
|
578
719
|
|
|
579
720
|
A new project \u2014 "set up Flowriki for this site, in this repository" \u2014 is made
|
|
580
721
|
with create_project; it then becomes the project the other tools work on.
|
|
581
722
|
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
723
|
+
THE MODEL \u2014 get this right before recording anything:
|
|
724
|
+
- A FLOW is a reusable building block: one piece of a journey ("Open the account
|
|
725
|
+
dialog", "Fill the sign-up form", "Sign in with a wrong password"). A flow can
|
|
726
|
+
START AFTER other flows (start_flow's "after"), so it holds only its own new
|
|
727
|
+
steps and links to the ones before it. A path that behaves differently \u2014 a
|
|
728
|
+
wrong password, a duplicate email \u2014 is its own flow, because the page does
|
|
729
|
+
something different.
|
|
730
|
+
- A flow records checks (add_check) that prove each thing happened, but they
|
|
731
|
+
are SAVED SWITCHED OFF: which ones matter depends on the test.
|
|
732
|
+
- A TEST CASE is the test. It strings flows together as rows (in the same
|
|
733
|
+
browser, or a new one for a second person) and switches on the checks it is
|
|
734
|
+
about. Build it with preview_case, then save_case \u2014 which runs it first.
|
|
735
|
+
- SIGN-IN is shared setup: a flow or case that needs somebody signed in uses a
|
|
736
|
+
sign-in profile from the catalog (start_flow's signIn), never typed login steps.
|
|
737
|
+
None yet? Make it: record the login as a flow that ends with a check proving
|
|
738
|
+
it worked (the account menu, a heading, the URL), then create_sign_in from it
|
|
739
|
+
and create_account for each person who signs in (a customer, an admin). The
|
|
740
|
+
login flow types into its fields; the account supplies the values.
|
|
741
|
+
- HELPERS (an inbox for email codes, an authenticator, SMS, an API call) are made
|
|
742
|
+
with create_helper when the catalog has none that fits.
|
|
743
|
+
- PASSWORDS AND KEYS never go into a flow or a reply. Typed into a password
|
|
744
|
+
field, a value is kept as a secret and saved to the project's vault; given to
|
|
745
|
+
create_account or create_helper, it is sealed there too and never shown back.
|
|
746
|
+
Only use credentials the person gave you or that the app itself shows for
|
|
747
|
+
testing (a demo login) \u2014 never guess or reuse anyone's real password.
|
|
748
|
+
- FOLDERS do not nest. Name each one after the product area and what is in it,
|
|
749
|
+
the way a tester would look for it \u2014 "Account \u2013 Sign up", "Account \u2013 Sign in",
|
|
750
|
+
"Ordering \u2013 Pizza builder", "Checkout \u2013 Payment". Flows (features) and the
|
|
751
|
+
cases built from them use the same names. Keep one area per folder.
|
|
752
|
+
|
|
753
|
+
HOW TO WORK:
|
|
754
|
+
1. Call project_catalog. Reuse what exists \u2014 published, and on Claude's working
|
|
755
|
+
branch \u2014 rather than writing it again.
|
|
756
|
+
2. Before recording, say the plan to the person: the folders, the building-block
|
|
757
|
+
flows (and what each starts after), and the test cases (rows + what each
|
|
758
|
+
checks). Then record the blocks, earliest first.
|
|
759
|
+
3. Record with start_flow (pass "after" for the flows it continues from), read
|
|
760
|
+
the page with read_page, and act using the role and name EXACTLY as the page
|
|
761
|
+
shows them. If a name appears more than once, pass "within" with the name of
|
|
762
|
+
the region, dialog or form \u2014 or "dialog" / "navigation".
|
|
763
|
+
4. For things outside the page \u2014 an emailed code or link, an authenticator
|
|
595
764
|
code, a text message, an API call \u2014 use use_helper with a helper from the
|
|
596
765
|
catalog, then type {{its saveAs name}} into the field that needs it.
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
do not write a flow twice.
|
|
606
|
-
9. Old branches of yours from before (one per flow)? gather_branches puts them
|
|
607
|
-
into one. discard_branch deletes one \u2014 only after the person asks for it or
|
|
608
|
-
agrees. Only Claude's own unedited branches can be gathered or deleted;
|
|
609
|
-
anything a person touched is theirs.
|
|
766
|
+
5. Add checks for what each step proves (a heading, a message, a value, the
|
|
767
|
+
URL). replay_flow, then save_flow. A flow must record at least one check.
|
|
768
|
+
6. Build each test case: preview_case lists its rows and every check, numbered;
|
|
769
|
+
save_case with the numbers to switch on runs the whole case and saves it only
|
|
770
|
+
if it passed. Give the person the review link.
|
|
771
|
+
7. Saving the same name again replaces it on the working branch. Old branches
|
|
772
|
+
of yours? gather_branches puts them into one; discard_branch deletes one \u2014
|
|
773
|
+
only after the person asks or agrees.
|
|
610
774
|
Test data you invent (a new user's email or password for a sign-up) is fine to
|
|
611
775
|
type. For anything that must be NEW on every run \u2014 a sign-up's email or
|
|
612
776
|
username \u2014 use {{unique}}: it is different on every run and the same within
|
|
613
777
|
one, e.g. qa+{{unique}}@ the domain of the project's mailbox helper, so the
|
|
614
778
|
helper can read that address's verification email with {"email": "qa+{{unique}}@\u2026"}.
|
|
615
779
|
A test that types a fixed new email passes once and fails every run after.`;
|
|
780
|
+
var CASE_INPUT = {
|
|
781
|
+
type: "object",
|
|
782
|
+
properties: {
|
|
783
|
+
name: { type: "string", description: 'The test case: the scenario it proves, e.g. "Wrong password is refused".' },
|
|
784
|
+
folder: { type: "string", description: 'The case folder \u2014 area and topic, e.g. "Account \u2013 Sign in". Made if new.' },
|
|
785
|
+
description: { type: "string" },
|
|
786
|
+
rows: {
|
|
787
|
+
type: "array",
|
|
788
|
+
description: "The flows to run, in order. Each row runs in the same browser as the row above unless newBrowser is true (a second person).",
|
|
789
|
+
items: {
|
|
790
|
+
type: "object",
|
|
791
|
+
properties: {
|
|
792
|
+
flow: { type: "string", description: "A flow's name or id from project_catalog." },
|
|
793
|
+
newBrowser: { type: "boolean" },
|
|
794
|
+
checks: {
|
|
795
|
+
description: `Which of this row's checks to switch on: their numbers from preview_case, or "all". The rest stay off.`,
|
|
796
|
+
anyOf: [{ type: "array", items: { type: "number" } }, { type: "string", enum: ["all"] }]
|
|
797
|
+
}
|
|
798
|
+
},
|
|
799
|
+
required: ["flow"],
|
|
800
|
+
additionalProperties: false
|
|
801
|
+
}
|
|
802
|
+
},
|
|
803
|
+
branch: { type: "string", description: "Optional: save onto this branch (chg_\u2026) instead of the working one." },
|
|
804
|
+
projectId: { type: "string" }
|
|
805
|
+
},
|
|
806
|
+
required: ["name", "folder", "rows"],
|
|
807
|
+
additionalProperties: false
|
|
808
|
+
};
|
|
809
|
+
var caseSpec = (args) => ({
|
|
810
|
+
name: String(args["name"] ?? ""),
|
|
811
|
+
folder: String(args["folder"] ?? ""),
|
|
812
|
+
...args["description"] ? { description: String(args["description"]) } : {},
|
|
813
|
+
rows: Array.isArray(args["rows"]) ? args["rows"] : [],
|
|
814
|
+
...args["branch"] ? { changeId: String(args["branch"]) } : {}
|
|
815
|
+
});
|
|
616
816
|
var text = (value, isError = false) => ({ content: [{ type: "text", text: value }], ...isError ? { isError } : {} });
|
|
617
817
|
var str = (value) => typeof value === "string" ? value : value === void 0 || value === null ? void 0 : String(value);
|
|
618
818
|
function authoringTools(service, session) {
|
|
@@ -634,7 +834,7 @@ function authoringTools(service, session) {
|
|
|
634
834
|
catalogs.set(projectId, catalog);
|
|
635
835
|
return catalog;
|
|
636
836
|
};
|
|
637
|
-
|
|
837
|
+
const tools = [
|
|
638
838
|
{
|
|
639
839
|
name: "list_projects",
|
|
640
840
|
title: "List Flowriki projects",
|
|
@@ -690,13 +890,19 @@ ${flowsBy(f.id)}`),
|
|
|
690
890
|
`
|
|
691
891
|
On Claude's working branch "${c.draft.title}" (${c.draft.changeId}), not yet published:`,
|
|
692
892
|
...c.draft.features.length ? [` New features: ${c.draft.features.map((f) => `${f.name} (id ${f.id})`).join(", ")}`] : [],
|
|
693
|
-
...c.draft.flows.length ? c.draft.flows.map((f) => ` - ${f.name} (${f.steps} steps, feature ${f.featureId})`) : [" (no flows yet)"]
|
|
893
|
+
...c.draft.flows.length ? c.draft.flows.map((f) => ` - flow: ${f.name} (${f.steps} steps, feature ${f.featureId}${f.after?.length ? `, after ${f.after.map((a) => `"${a}"`).join(", ")}` : ""})`) : [" (no flows yet)"],
|
|
894
|
+
...c.draft.caseFolders?.length ? [` New case folders: ${c.draft.caseFolders.map((f) => f.name).join(", ")}`] : [],
|
|
895
|
+
...(c.draft.cases ?? []).map((t) => ` - case: ${t.name} (folder ${t.folderId}) \u2014 rows: ${t.rows.join(" \u2192 ")}`)
|
|
694
896
|
] : [],
|
|
695
897
|
`
|
|
898
|
+
Test cases:${(c.cases ?? []).length ? "" : " none published"}`,
|
|
899
|
+
...(c.caseFolders ?? []).map((f) => ` - ${f.name} (id ${f.id})
|
|
900
|
+
${(c.cases ?? []).filter((t) => t.folderId === f.id).map((t) => ` - ${t.name} \u2014 rows: ${t.rows.join(" \u2192 ")}`).join("\n")}`),
|
|
901
|
+
`
|
|
696
902
|
Sign-in profiles:${c.signIns.length ? "" : " none"}`,
|
|
697
|
-
...c.signIns.map((s) => ` - ${s.name} (profileId ${s.profileId}): ${s.accounts.map((a) => `${a.name} (accountId ${a.accountId})`).join(", ") || "no accounts yet"}`),
|
|
903
|
+
...c.signIns.map((s) => ` - ${s.name} (profileId ${s.profileId})${s.fields?.length ? ` \u2014 fields ${s.fields.map((f) => `"${f}"`).join(", ")}` : ""}: ${s.accounts.map((a) => `${a.name} (accountId ${a.accountId})`).join(", ") || "no accounts yet"}`),
|
|
698
904
|
`
|
|
699
|
-
Helpers:${c.helpers.length ? "" : " none \u2014
|
|
905
|
+
Helpers:${c.helpers.length ? "" : " none \u2014 create_helper makes one"}`,
|
|
700
906
|
...c.helpers.map((h) => ` - ${h.name} (id ${h.id}) \u2014 ${h.what}${h.inputs.length ? `; needs ${h.inputs.map((n) => `{{${n}}}`).join(", ")}` : ""}${h.description ? ` \u2014 ${h.description}` : ""}`)
|
|
701
907
|
].filter(Boolean).join("\n"));
|
|
702
908
|
}
|
|
@@ -711,6 +917,11 @@ Helpers:${c.helpers.length ? "" : " none \u2014 they are made on the Helpers scr
|
|
|
711
917
|
name: { type: "string", description: `The flow's name: the user journey, e.g. "Sign up and verify email".` },
|
|
712
918
|
feature: { type: "string", description: "The feature it belongs to: an existing feature's id or name, or a new name." },
|
|
713
919
|
startPath: { type: "string", description: 'Where it starts, relative to the base URL. Default "/".' },
|
|
920
|
+
after: {
|
|
921
|
+
type: "array",
|
|
922
|
+
items: { type: "string" },
|
|
923
|
+
description: "Flows this one starts after (names or ids from project_catalog). They are replayed first, so recording begins where they end; what THEY start after is added too. The saved flow links to them."
|
|
924
|
+
},
|
|
714
925
|
signIn: {
|
|
715
926
|
type: "object",
|
|
716
927
|
description: "Start signed in, with a profile (and optionally an account) from project_catalog.",
|
|
@@ -733,14 +944,18 @@ Helpers:${c.helpers.length ? "" : " none \u2014 they are made on the Helpers scr
|
|
|
733
944
|
if (signIn && !catalog.signIns.some((s) => s.profileId === signIn.profileId)) {
|
|
734
945
|
throw new Error(`no sign-in profile ${signIn.profileId} \u2014 project_catalog lists them`);
|
|
735
946
|
}
|
|
947
|
+
const asked = Array.isArray(args["after"]) ? args["after"].map(String).filter(Boolean) : [];
|
|
948
|
+
const plan = asked.length ? await service.flowsAfter(projectId, asked) : null;
|
|
736
949
|
const page = await session.start({
|
|
737
950
|
projectId,
|
|
951
|
+
...plan ? { after: plan.prerequisites, preparation: plan.steps } : {},
|
|
738
952
|
name: str(args["name"]) ?? "Untitled flow",
|
|
739
953
|
...feature ? { featureId: feature.id } : { featureName: featureAsked },
|
|
740
954
|
startPath: str(args["startPath"]) || "/",
|
|
741
955
|
...signIn?.profileId ? { signIn: { profileId: signIn.profileId, ...signIn.accountId ? { accountId: signIn.accountId } : {} } } : {}
|
|
742
956
|
}, catalog);
|
|
743
|
-
|
|
957
|
+
const after = plan?.prerequisites.length ? ` It starts after ${plan.prerequisites.map((p) => `"${p.name}"`).join(" \u2192 ")}, which ran first.` : "";
|
|
958
|
+
return text(`Started "${str(args["name"])}"${feature ? ` in ${feature.name}` : ` in a new feature, ${featureAsked}`}.${after} The browser is open.
|
|
744
959
|
|
|
745
960
|
${page}`);
|
|
746
961
|
}
|
|
@@ -764,7 +979,8 @@ ${page}`);
|
|
|
764
979
|
role: { type: "string", description: "The element's role as read_page shows it, e.g. button, link, textbox, checkbox, combobox." },
|
|
765
980
|
name: { type: "string", description: "The element's accessible name, exactly as read_page shows it." },
|
|
766
981
|
within: { type: "string", description: "When the name is not unique: the name of the region, dialog, form or navigation that holds it." },
|
|
767
|
-
value: { type: "string" }
|
|
982
|
+
value: { type: "string" },
|
|
983
|
+
secret: { type: "boolean", description: "fill only: the value is a secret (a password, PIN, key) \u2014 kept out of the flow and saved to the vault. Fields named like a password are secret without this." }
|
|
768
984
|
},
|
|
769
985
|
required: ["action"],
|
|
770
986
|
additionalProperties: false
|
|
@@ -776,7 +992,8 @@ ${page}`);
|
|
|
776
992
|
...str(args["role"]) ? { role: str(args["role"]) } : {},
|
|
777
993
|
...str(args["name"]) !== void 0 ? { name: str(args["name"]) } : {},
|
|
778
994
|
...str(args["within"]) ? { within: str(args["within"]) } : {},
|
|
779
|
-
...str(args["value"]) !== void 0 ? { value: str(args["value"]) } : {}
|
|
995
|
+
...str(args["value"]) !== void 0 ? { value: str(args["value"]) } : {},
|
|
996
|
+
...typeof args["secret"] === "boolean" ? { secret: args["secret"] } : {}
|
|
780
997
|
}));
|
|
781
998
|
}
|
|
782
999
|
},
|
|
@@ -924,6 +1141,124 @@ ${page}`);
|
|
|
924
1141
|
return text(`Deleted ${changeId} and its branch ${deleted}.`);
|
|
925
1142
|
}
|
|
926
1143
|
},
|
|
1144
|
+
{
|
|
1145
|
+
name: "preview_case",
|
|
1146
|
+
title: "Preview a test case",
|
|
1147
|
+
description: "Build a test case from flows without saving or running it: its rows (each flow with what it starts after folded in, and which browser it runs in) and every check, numbered, with which are on. Use the numbers in save_case.",
|
|
1148
|
+
inputSchema: CASE_INPUT,
|
|
1149
|
+
annotations: { readOnlyHint: true },
|
|
1150
|
+
run: async (args) => {
|
|
1151
|
+
const projectId = await projectFor(args["projectId"]);
|
|
1152
|
+
return text(await session.previewCase(projectId, caseSpec(args)));
|
|
1153
|
+
}
|
|
1154
|
+
},
|
|
1155
|
+
{
|
|
1156
|
+
name: "save_case",
|
|
1157
|
+
title: "Run and save a test case",
|
|
1158
|
+
description: "Build the test case, run it from the start in a fresh browser exactly as a test run will, and \u2014 only if every row passed \u2014 save it onto Claude's working branch. Saving the same name in the same folder again replaces it. Refused if it switches on no checks.",
|
|
1159
|
+
inputSchema: CASE_INPUT,
|
|
1160
|
+
annotations: { openWorldHint: true },
|
|
1161
|
+
run: async (args) => {
|
|
1162
|
+
const projectId = await projectFor(args["projectId"]);
|
|
1163
|
+
const out = await session.saveCase(projectId, caseSpec(args), await catalogOf(projectId, true));
|
|
1164
|
+
catalogs.delete(projectId);
|
|
1165
|
+
return text(out);
|
|
1166
|
+
}
|
|
1167
|
+
},
|
|
1168
|
+
{
|
|
1169
|
+
name: "create_sign_in",
|
|
1170
|
+
title: "Make a sign-in profile",
|
|
1171
|
+
description: "Turn a saved login flow into a sign-in profile that flows and test cases can start signed in with. The flow must not itself sign in or start after other flows, and must end with a check proving the login worked \u2014 its last check is switched on here. Returns the fields an account fills in. Making the same name again replaces Claude's earlier one.",
|
|
1172
|
+
inputSchema: {
|
|
1173
|
+
type: "object",
|
|
1174
|
+
properties: {
|
|
1175
|
+
projectId: { type: "string" },
|
|
1176
|
+
flow: { type: "string", description: "The login flow's name or id (project_catalog)." },
|
|
1177
|
+
name: { type: "string", description: `What a tester calls this way of signing in, e.g. "Customer sign-in". Default: the flow's name.` },
|
|
1178
|
+
description: { type: "string" },
|
|
1179
|
+
signOutFlow: { type: "string", description: "Optional: a flow that signs out, by name or id." },
|
|
1180
|
+
branch: { type: "string", description: "Optional: the id of one of Claude's open branches to write to (list_my_branches). Default: the working branch." }
|
|
1181
|
+
},
|
|
1182
|
+
required: ["flow"],
|
|
1183
|
+
additionalProperties: false
|
|
1184
|
+
},
|
|
1185
|
+
run: async (args) => {
|
|
1186
|
+
const projectId = await projectFor(args["projectId"]);
|
|
1187
|
+
const made = await service.createSignIn(projectId, {
|
|
1188
|
+
flow: str(args["flow"]) ?? "",
|
|
1189
|
+
...str(args["name"]) ? { name: str(args["name"]) } : {},
|
|
1190
|
+
...str(args["description"]) ? { description: str(args["description"]) } : {},
|
|
1191
|
+
...str(args["signOutFlow"]) ? { signOutFlow: str(args["signOutFlow"]) } : {},
|
|
1192
|
+
...str(args["branch"]) ? { changeId: str(args["branch"]) } : {}
|
|
1193
|
+
});
|
|
1194
|
+
catalogs.delete(projectId);
|
|
1195
|
+
return text(`${made.replaced ? "Replaced" : "Made"} the sign-in "${made.name}" (profileId ${made.profileId}) from "${made.flow}", on ${made.newBranch ? "a new branch" : "Claude's working branch"} ${made.branch}.
|
|
1196
|
+
An account fills in: ${made.fields.map((f) => `"${f.label}"${f.secret ? " (secret)" : ""}`).join(", ") || "nothing \u2014 the flow types no values"}.
|
|
1197
|
+
Next: create_account for each person who signs in this way.`);
|
|
1198
|
+
}
|
|
1199
|
+
},
|
|
1200
|
+
{
|
|
1201
|
+
name: "create_account",
|
|
1202
|
+
title: "Make a sign-in account",
|
|
1203
|
+
description: "An account for a sign-in profile: who signs in, and the values its login flow types, by field label (create_sign_in and project_catalog list them). Secret fields are sealed in the project's vault and never shown again. Then start_flow / test cases can use signIn {profileId, accountId}. Making the same name again updates Claude's earlier one.",
|
|
1204
|
+
inputSchema: {
|
|
1205
|
+
type: "object",
|
|
1206
|
+
properties: {
|
|
1207
|
+
projectId: { type: "string" },
|
|
1208
|
+
signIn: { type: "string", description: "The sign-in profile's name or id." },
|
|
1209
|
+
name: { type: "string", description: 'Who this is, e.g. "Demo customer", "Kitchen admin".' },
|
|
1210
|
+
values: { type: "object", additionalProperties: { type: "string" }, description: 'Field label \u2192 value, e.g. {"Email": "demo@example.com", "Password": "\u2026"}.' },
|
|
1211
|
+
environments: { type: "array", items: { type: "string" }, description: "Optional: the environments this account exists on. Default: all." },
|
|
1212
|
+
branch: { type: "string", description: "Optional: the id of one of Claude's open branches to write to (list_my_branches). Default: the working branch." }
|
|
1213
|
+
},
|
|
1214
|
+
required: ["signIn", "name", "values"],
|
|
1215
|
+
additionalProperties: false
|
|
1216
|
+
},
|
|
1217
|
+
run: async (args) => {
|
|
1218
|
+
const projectId = await projectFor(args["projectId"]);
|
|
1219
|
+
const values = Object.fromEntries(Object.entries(args["values"] && typeof args["values"] === "object" ? args["values"] : {}).map(([k, v]) => [k, String(v)]));
|
|
1220
|
+
const made = await service.createAccount(projectId, {
|
|
1221
|
+
signIn: str(args["signIn"]) ?? "",
|
|
1222
|
+
name: str(args["name"]) ?? "",
|
|
1223
|
+
values,
|
|
1224
|
+
...Array.isArray(args["environments"]) ? { environments: args["environments"].map(String) } : {},
|
|
1225
|
+
...str(args["branch"]) ? { changeId: str(args["branch"]) } : {}
|
|
1226
|
+
});
|
|
1227
|
+
catalogs.delete(projectId);
|
|
1228
|
+
return text(`${made.replaced ? "Updated" : "Made"} the account "${made.name}" (accountId ${made.accountId}) for "${made.signIn}", on Claude's working branch ${made.branch}. Filled: ${made.filled.join(", ")} \u2014 secrets are in the vault, not the repository.
|
|
1229
|
+
Use it with start_flow signIn {"profileId": "\u2026", "accountId": "${made.accountId}"}.`);
|
|
1230
|
+
}
|
|
1231
|
+
},
|
|
1232
|
+
{
|
|
1233
|
+
name: "create_helper",
|
|
1234
|
+
title: "Make a helper",
|
|
1235
|
+
description: `A helper for things outside the page, as the Helpers screen makes one. config by kind \u2014 mailbox: {kind, host, port?, secure?, username, folder?, match?: {to, from, subject}, extract?: {take: code|link|subject|body, pattern}, waitSeconds?} with secretValues {passwordRef: the mailbox password}. totp: {kind, digits?, period?, algorithm?} with secretValues {secretRef: the base32 secret}. sms: {kind, accountSid, to, match?, extract?} with secretValues {authTokenRef: the Twilio token}. http: {kind, method, url, headers?, secretHeaders?: {name: ""}, body?, expectStatus?, extract?: {take: json|pattern|status|nothing, path, pattern}} with secretValues {"header:<name>": value}. Use {{name}} in fields for inputs a step supplies. Secrets are sealed in the vault and never shown again. Making the same name again updates Claude's earlier one.`,
|
|
1236
|
+
inputSchema: {
|
|
1237
|
+
type: "object",
|
|
1238
|
+
properties: {
|
|
1239
|
+
projectId: { type: "string" },
|
|
1240
|
+
name: { type: "string", description: 'e.g. "QA inbox", "Admin authenticator".' },
|
|
1241
|
+
description: { type: "string" },
|
|
1242
|
+
config: { type: "object", description: "The helper's settings, by kind (see above). Never put a secret here." },
|
|
1243
|
+
secretValues: { type: "object", additionalProperties: { type: "string" }, description: "The secret for the kind, keyed as above." },
|
|
1244
|
+
branch: { type: "string", description: "Optional: the id of one of Claude's open branches to write to (list_my_branches). Default: the working branch." }
|
|
1245
|
+
},
|
|
1246
|
+
required: ["name", "config"],
|
|
1247
|
+
additionalProperties: false
|
|
1248
|
+
},
|
|
1249
|
+
run: async (args) => {
|
|
1250
|
+
const projectId = await projectFor(args["projectId"]);
|
|
1251
|
+
const made = await service.createHelper(projectId, {
|
|
1252
|
+
name: str(args["name"]) ?? "",
|
|
1253
|
+
...str(args["description"]) ? { description: str(args["description"]) } : {},
|
|
1254
|
+
config: args["config"] && typeof args["config"] === "object" ? args["config"] : {},
|
|
1255
|
+
...args["secretValues"] && typeof args["secretValues"] === "object" ? { secretValues: Object.fromEntries(Object.entries(args["secretValues"]).map(([k, v]) => [k, String(v)])) } : {},
|
|
1256
|
+
...str(args["branch"]) ? { changeId: str(args["branch"]) } : {}
|
|
1257
|
+
});
|
|
1258
|
+
catalogs.delete(projectId);
|
|
1259
|
+
return text(`${made.replaced ? "Updated" : "Made"} the ${made.kind} helper "${made.name}" (id ${made.helperId}) on Claude's working branch ${made.branch}.${made.inputs.length ? ` A step supplies ${made.inputs.map((n) => `{{${n}}}`).join(", ")}.` : ""} Use it with use_helper.`);
|
|
1260
|
+
}
|
|
1261
|
+
},
|
|
927
1262
|
{
|
|
928
1263
|
name: "close_browser",
|
|
929
1264
|
title: "Close the browser",
|
|
@@ -937,6 +1272,18 @@ ${page}`);
|
|
|
937
1272
|
}
|
|
938
1273
|
}
|
|
939
1274
|
];
|
|
1275
|
+
return tools.map((tool) => ({
|
|
1276
|
+
...tool,
|
|
1277
|
+
run: async (args) => {
|
|
1278
|
+
let result;
|
|
1279
|
+
try {
|
|
1280
|
+
result = await tool.run(args);
|
|
1281
|
+
} catch (error) {
|
|
1282
|
+
throw new Error(session.redact(error.message));
|
|
1283
|
+
}
|
|
1284
|
+
return { ...result, content: result.content.map((c) => c.type === "text" && c.text !== void 0 ? { ...c, text: session.redact(c.text) } : c) };
|
|
1285
|
+
}
|
|
1286
|
+
}));
|
|
940
1287
|
}
|
|
941
1288
|
|
|
942
1289
|
// src/mcp/index.ts
|
package/dist/engine.js
CHANGED
package/package.json
CHANGED