@flowriki/agent 0.1.0-beta.33 → 0.1.0-beta.34
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-3UUCI66O.js → chunk-7OODSC7P.js} +1 -1
- package/dist/cli.js +131 -25
- 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.34";
|
|
10
10
|
async function prepareBrowsers() {
|
|
11
11
|
if (browsersReady()) {
|
|
12
12
|
return { ok: true, already: true, message: "chromium is already installed" };
|
package/dist/cli.js
CHANGED
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
runtimeValue,
|
|
22
22
|
start,
|
|
23
23
|
status
|
|
24
|
-
} from "./chunk-
|
|
24
|
+
} from "./chunk-7OODSC7P.js";
|
|
25
25
|
|
|
26
26
|
// src/cli.ts
|
|
27
27
|
import { parseArgs } from "util";
|
|
@@ -39,7 +39,7 @@ async function handle(message, tools, info) {
|
|
|
39
39
|
const version = PROTOCOL_VERSIONS.includes(asked) ? asked : PROTOCOL_VERSIONS[0];
|
|
40
40
|
return reply({
|
|
41
41
|
protocolVersion: version,
|
|
42
|
-
capabilities: { tools: { listChanged:
|
|
42
|
+
capabilities: { tools: { listChanged: true } },
|
|
43
43
|
serverInfo: { name: info.name, version: info.version },
|
|
44
44
|
instructions: info.instructions
|
|
45
45
|
});
|
|
@@ -95,6 +95,10 @@ function serve(tools, info, io = {}) {
|
|
|
95
95
|
const task = handle(message, tools, info).then((answer) => {
|
|
96
96
|
if (answer) output.write(`${JSON.stringify(answer)}
|
|
97
97
|
`);
|
|
98
|
+
if (message.method === "notifications/initialized") {
|
|
99
|
+
output.write(`${JSON.stringify({ jsonrpc: "2.0", method: "notifications/tools/list_changed" })}
|
|
100
|
+
`);
|
|
101
|
+
}
|
|
98
102
|
});
|
|
99
103
|
pending.add(task);
|
|
100
104
|
void task.finally(() => pending.delete(task));
|
|
@@ -168,16 +172,36 @@ var ServiceClient = class {
|
|
|
168
172
|
{ method: "DELETE" }
|
|
169
173
|
);
|
|
170
174
|
}
|
|
171
|
-
saveFlow(projectId, title, flow) {
|
|
175
|
+
saveFlow(projectId, title, flow, where = {}) {
|
|
172
176
|
return this.call(
|
|
173
177
|
`/api/agent/flows?projectId=${encodeURIComponent(projectId)}`,
|
|
174
|
-
{ method: "POST", body: JSON.stringify({ title, flow }) }
|
|
178
|
+
{ method: "POST", body: JSON.stringify({ title, flow, ...where }) }
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
/** Gather Claude's open branches here into one. */
|
|
182
|
+
gatherChanges(projectId, opts = {}) {
|
|
183
|
+
return this.call(
|
|
184
|
+
`/api/agent/changes/gather?projectId=${encodeURIComponent(projectId)}`,
|
|
185
|
+
{ method: "POST", body: JSON.stringify(opts) }
|
|
175
186
|
);
|
|
176
187
|
}
|
|
177
188
|
};
|
|
178
189
|
|
|
179
190
|
// src/mcp/session.ts
|
|
180
191
|
var MAX_TREE = 14e3;
|
|
192
|
+
var MAX_STEP_VIEW = 6e3;
|
|
193
|
+
function topLevelBlocks(tree) {
|
|
194
|
+
const blocks = [];
|
|
195
|
+
for (const line of tree.split("\n")) {
|
|
196
|
+
if (line.startsWith("- ") || blocks.length === 0) blocks.push([line]);
|
|
197
|
+
else blocks[blocks.length - 1].push(line);
|
|
198
|
+
}
|
|
199
|
+
const named = (b, kinds) => kinds.some((k) => b[0].startsWith(`- ${k}`));
|
|
200
|
+
return {
|
|
201
|
+
dialogs: blocks.filter((b) => named(b, ["dialog", "alertdialog"])).map((b) => b.join("\n")),
|
|
202
|
+
navigation: blocks.find((b) => named(b, ["navigation", "banner"]))?.join("\n") ?? null
|
|
203
|
+
};
|
|
204
|
+
}
|
|
181
205
|
var STEP_TIMEOUT_MS = 1e4;
|
|
182
206
|
var ACTIONS = ["click", "dblclick", "rightclick", "hover", "fill", "select", "check", "press", "navigate", "back", "forward"];
|
|
183
207
|
var isAction = (value) => ACTIONS.includes(String(value));
|
|
@@ -238,6 +262,7 @@ var AuthoringSession = class {
|
|
|
238
262
|
page = null;
|
|
239
263
|
values = /* @__PURE__ */ new Map();
|
|
240
264
|
resolvers = null;
|
|
265
|
+
lastTree = null;
|
|
241
266
|
draft = null;
|
|
242
267
|
catalog = null;
|
|
243
268
|
get open() {
|
|
@@ -272,6 +297,7 @@ var AuthoringSession = class {
|
|
|
272
297
|
this.catalog = catalog;
|
|
273
298
|
this.draft = { ...input, steps: [] };
|
|
274
299
|
this.values = /* @__PURE__ */ new Map();
|
|
300
|
+
this.lastTree = null;
|
|
275
301
|
const base = this.baseUrl();
|
|
276
302
|
this.run = await openRunSession({ steps: [], headless: this.opts.headless ?? false });
|
|
277
303
|
this.page = this.run.page;
|
|
@@ -304,19 +330,52 @@ var AuthoringSession = class {
|
|
|
304
330
|
}
|
|
305
331
|
/** The page as Claude reads it: where it is, and what is on it by role and name. */
|
|
306
332
|
async snapshot() {
|
|
333
|
+
return this.view(true);
|
|
334
|
+
}
|
|
335
|
+
/** WHAT CHANGED, after a step — not the whole page again.
|
|
336
|
+
*
|
|
337
|
+
* Every step used to answer with the full tree, which on a long storefront
|
|
338
|
+
* is twenty thousand characters repeated forty times to say "the box now
|
|
339
|
+
* has text in it". So a step answers with what it is about: an open dialog
|
|
340
|
+
* (with the navigation, which says who is signed in), or the lines that
|
|
341
|
+
* appeared and disappeared — and only after a real navigation, when most of
|
|
342
|
+
* the page is new, the page itself, capped. read_page is always the whole
|
|
343
|
+
* thing. */
|
|
344
|
+
async view(full) {
|
|
307
345
|
const { page, draft } = this.requireOpen();
|
|
308
346
|
await page.waitForLoadState("domcontentloaded", { timeout: 1e4 }).catch(() => void 0);
|
|
309
|
-
|
|
347
|
+
const tree = labelTree(
|
|
310
348
|
await page.locator("body").ariaSnapshot({ timeout: 8e3 }).catch((e) => `(could not read the page: ${e.message})`),
|
|
311
349
|
await fieldNames(page)
|
|
312
350
|
);
|
|
313
|
-
if (tree.length > MAX_TREE) tree = `${tree.slice(0, MAX_TREE)}
|
|
314
|
-
\u2026 (cut \u2014 the page is long; act on what is above, or scroll with press "PageDown")`;
|
|
315
351
|
const title = await page.title().catch(() => "");
|
|
316
|
-
|
|
352
|
+
const head = `Page: ${page.url()}${title ? ` \u2014 "${title}"` : ""}
|
|
317
353
|
Steps so far: ${draft.steps.length}
|
|
318
354
|
|
|
319
|
-
|
|
355
|
+
`;
|
|
356
|
+
const before = this.lastTree;
|
|
357
|
+
this.lastTree = tree;
|
|
358
|
+
const cap = (t, max) => t.length > max ? `${t.slice(0, max)}
|
|
359
|
+
\u2026 (cut \u2014 read_page shows the whole page)` : t;
|
|
360
|
+
if (full || before === null) return head + cap(tree, MAX_TREE);
|
|
361
|
+
const { dialogs, navigation } = topLevelBlocks(tree);
|
|
362
|
+
if (dialogs.length) {
|
|
363
|
+
return head + cap([...navigation ? [navigation] : [], ...dialogs].join("\n"), MAX_STEP_VIEW) + "\n(only the open dialog and the navigation \u2014 read_page shows the whole page)";
|
|
364
|
+
}
|
|
365
|
+
const was = new Set(before.split("\n"));
|
|
366
|
+
const now = new Set(tree.split("\n"));
|
|
367
|
+
const added = tree.split("\n").filter((l) => !was.has(l));
|
|
368
|
+
const removed = before.split("\n").filter((l) => !now.has(l));
|
|
369
|
+
if (added.length === 0 && removed.length === 0) return head + "(the page did not change \u2014 read_page shows it)";
|
|
370
|
+
if (added.length + removed.length > 120) return head + cap(tree, MAX_STEP_VIEW);
|
|
371
|
+
return head + [
|
|
372
|
+
...navigation ? [navigation, ""] : [],
|
|
373
|
+
added.length ? `Appeared:
|
|
374
|
+
${added.join("\n")}` : "",
|
|
375
|
+
removed.length ? `Gone:
|
|
376
|
+
${removed.join("\n")}` : "",
|
|
377
|
+
"(changes since the last step \u2014 read_page shows the whole page)"
|
|
378
|
+
].filter(Boolean).join("\n");
|
|
320
379
|
}
|
|
321
380
|
/** Do one thing on the page, and keep it as a step if it worked. */
|
|
322
381
|
async act(input) {
|
|
@@ -354,13 +413,13 @@ ${tree}`;
|
|
|
354
413
|
} catch (error) {
|
|
355
414
|
throw new Error(`That did not work, and was not kept: ${error.message.split("\n")[0]}
|
|
356
415
|
|
|
357
|
-
${await this.
|
|
416
|
+
${await this.view(false)}`);
|
|
358
417
|
}
|
|
359
418
|
await this.settle();
|
|
360
419
|
draft.steps.push(step);
|
|
361
420
|
return `Step ${draft.steps.length} kept: ${describe(step)}.
|
|
362
421
|
|
|
363
|
-
${await this.
|
|
422
|
+
${await this.view(false)}`;
|
|
364
423
|
}
|
|
365
424
|
/** Check something about the page now, and keep the check if it holds. */
|
|
366
425
|
async check(input) {
|
|
@@ -437,7 +496,7 @@ ${await this.snapshot()}`;
|
|
|
437
496
|
draft.steps.push(step);
|
|
438
497
|
return `Step ${draft.steps.length} kept: ${describe(step)}. It got "${got.value}" (${got.detail}). Use {{${step.helper.saveAs}}} as the value of a later step.` + (step.helper.then === "open" ? `
|
|
439
498
|
|
|
440
|
-
${await this.
|
|
499
|
+
${await this.view(false)}` : "");
|
|
441
500
|
}
|
|
442
501
|
list() {
|
|
443
502
|
const draft = this.draft;
|
|
@@ -473,24 +532,26 @@ ${this.list()}`;
|
|
|
473
532
|
${lines.join("\n")}`;
|
|
474
533
|
}
|
|
475
534
|
/** Save the draft as a flow on a new branch, for a person to review. */
|
|
476
|
-
async save(title) {
|
|
535
|
+
async save(title, where = {}) {
|
|
477
536
|
const { draft } = this.requireOpen();
|
|
478
537
|
if (draft.steps.length === 0) throw new Error("there are no steps to save yet");
|
|
479
538
|
if (!draft.steps.some((s) => s.type === "assert" || s.proposals.length > 0)) {
|
|
480
539
|
throw new Error("the flow checks nothing \u2014 add at least one check (add_check) that proves it worked, then save");
|
|
481
540
|
}
|
|
482
|
-
const saved = await this.service.saveFlow(draft.projectId, title ||
|
|
541
|
+
const saved = await this.service.saveFlow(draft.projectId, title || "Tests written with Claude", {
|
|
483
542
|
name: draft.name,
|
|
484
543
|
...draft.featureId ? { featureId: draft.featureId } : { featureName: draft.featureName },
|
|
485
544
|
startPath: draft.startPath,
|
|
486
545
|
steps: draft.steps,
|
|
487
546
|
...draft.signIn ? { signIn: draft.signIn } : {}
|
|
488
|
-
});
|
|
547
|
+
}, where);
|
|
489
548
|
const review = `${this.service.origin}/projects/${draft.projectId}/changes/${saved.changeId}`;
|
|
549
|
+
const what = saved.replaced ? `Replaced the earlier "${draft.name}"` : `Saved "${draft.name}"`;
|
|
550
|
+
const where_ = saved.newBranch ? `on a new branch, ${saved.branch}` : `on Claude's working branch, ${saved.branch}`;
|
|
490
551
|
const others = await this.service.agentChanges(draft.projectId).then((r) => r.changes.filter((c) => c.id !== saved.changeId)).catch(() => []);
|
|
491
552
|
const note = others.length ? `
|
|
492
|
-
Claude
|
|
493
|
-
return
|
|
553
|
+
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
|
+
return `${what} ${where_}. Nothing is published yet.
|
|
494
555
|
Review and publish it here: ${review}${note}`;
|
|
495
556
|
}
|
|
496
557
|
async close() {
|
|
@@ -537,11 +598,15 @@ How to write a good Flowriki test:
|
|
|
537
598
|
a heading, a message, the URL. Saving is refused for a flow that checks nothing.
|
|
538
599
|
7. Before saving, call replay_flow to run the whole flow from the start. If a
|
|
539
600
|
step fails there, fix it (remove_step, act again) and replay again.
|
|
540
|
-
8. Save with save_flow and give the person the review link it returns.
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
601
|
+
8. Save with save_flow and give the person the review link it returns. Saves go
|
|
602
|
+
to ONE working branch per project \u2014 everything you write here lands on it,
|
|
603
|
+
for one review. Saving a flow with the same name again replaces it there.
|
|
604
|
+
The catalog lists what is already on that branch: reuse its features, and
|
|
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.
|
|
545
610
|
Test data you invent (a new user's email or password for a sign-up) is fine to
|
|
546
611
|
type. For anything that must be NEW on every run \u2014 a sign-up's email or
|
|
547
612
|
username \u2014 use {{unique}}: it is different on every run and the same within
|
|
@@ -621,6 +686,12 @@ It is now the project these tools work on. Open it: ${service.origin}/projects/$
|
|
|
621
686
|
Features:${c.features.length ? "" : " none yet"}`,
|
|
622
687
|
...c.features.map((f) => ` - ${f.name} (id ${f.id})${f.description ? ` \u2014 ${f.description}` : ""}
|
|
623
688
|
${flowsBy(f.id)}`),
|
|
689
|
+
...c.draft ? [
|
|
690
|
+
`
|
|
691
|
+
On Claude's working branch "${c.draft.title}" (${c.draft.changeId}), not yet published:`,
|
|
692
|
+
...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)"]
|
|
694
|
+
] : [],
|
|
624
695
|
`
|
|
625
696
|
Sign-in profiles:${c.signIns.length ? "" : " none"}`,
|
|
626
697
|
...c.signIns.map((s) => ` - ${s.name} (profileId ${s.profileId}): ${s.accounts.map((a) => `${a.name} (accountId ${a.accountId})`).join(", ") || "no accounts yet"}`),
|
|
@@ -783,9 +854,20 @@ ${page}`);
|
|
|
783
854
|
{
|
|
784
855
|
name: "save_flow",
|
|
785
856
|
title: "Save the flow to a branch",
|
|
786
|
-
description: "Save the flow
|
|
787
|
-
inputSchema: {
|
|
788
|
-
|
|
857
|
+
description: "Save the flow onto Claude's working branch for this project (made on the first save), for a person to review and publish. Saving the same name again replaces the earlier version there. Returns the review link. Refused if the flow checks nothing.",
|
|
858
|
+
inputSchema: {
|
|
859
|
+
type: "object",
|
|
860
|
+
properties: {
|
|
861
|
+
title: { type: "string", description: "The branch's title, used only when a new branch is started." },
|
|
862
|
+
branch: { type: "string", description: "Optional: a branch id (chg_\u2026) from list_my_branches to save onto instead." },
|
|
863
|
+
newBranch: { type: "boolean", description: "Start a separate branch instead of the working one. Only when the person asks for separate reviews." }
|
|
864
|
+
},
|
|
865
|
+
additionalProperties: false
|
|
866
|
+
},
|
|
867
|
+
run: async (args) => text(await session.save(str(args["title"]), {
|
|
868
|
+
...str(args["branch"]) ? { changeId: str(args["branch"]) } : {},
|
|
869
|
+
...args["newBranch"] === true ? { newBranch: true } : {}
|
|
870
|
+
}))
|
|
789
871
|
},
|
|
790
872
|
{
|
|
791
873
|
name: "list_my_branches",
|
|
@@ -800,6 +882,30 @@ ${page}`);
|
|
|
800
882
|
return text(changes.map((c) => `- ${c.id} \u2014 "${c.title}" (${new Date(c.createdAt).toISOString().slice(0, 16).replace("T", " ")}) ${service.origin}${c.path}`).join("\n"));
|
|
801
883
|
}
|
|
802
884
|
},
|
|
885
|
+
{
|
|
886
|
+
name: "gather_branches",
|
|
887
|
+
title: "Gather Claude's branches into one",
|
|
888
|
+
description: "Move the flows from Claude's open branches on this project into one branch, and delete the emptied ones \u2014 so the person reviews and publishes once. Only Claude's own, unedited branches; anything a person touched stops it.",
|
|
889
|
+
inputSchema: {
|
|
890
|
+
type: "object",
|
|
891
|
+
properties: {
|
|
892
|
+
into: { type: "string", description: "The branch to gather into (chg_\u2026). Default: Claude's oldest open branch." },
|
|
893
|
+
title: { type: "string", description: 'A title for the gathered branch, e.g. "Sign in and sign up tests".' },
|
|
894
|
+
projectId: { type: "string" }
|
|
895
|
+
},
|
|
896
|
+
additionalProperties: false
|
|
897
|
+
},
|
|
898
|
+
annotations: { destructiveHint: true },
|
|
899
|
+
run: async (args) => {
|
|
900
|
+
const projectId = await projectFor(args["projectId"]);
|
|
901
|
+
const done = await service.gatherChanges(projectId, {
|
|
902
|
+
...str(args["into"]) ? { into: str(args["into"]) } : {},
|
|
903
|
+
...str(args["title"]) ? { title: str(args["title"]) } : {}
|
|
904
|
+
});
|
|
905
|
+
catalogs.delete(projectId);
|
|
906
|
+
return text(`Gathered ${done.gathered.length} branch${done.gathered.length === 1 ? "" : "es"} into "${done.title}" (${done.changeId}). Review and publish it here: ${service.origin}${done.path}`);
|
|
907
|
+
}
|
|
908
|
+
},
|
|
803
909
|
{
|
|
804
910
|
name: "discard_branch",
|
|
805
911
|
title: "Delete one of Claude's branches",
|
package/dist/engine.js
CHANGED
package/package.json
CHANGED