@flowriki/agent 0.1.0-beta.32 → 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-4EYN6N5Q.js → chunk-7OODSC7P.js} +1 -1
- package/dist/cli.js +179 -21
- 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));
|
|
@@ -154,16 +158,50 @@ var ServiceClient = class {
|
|
|
154
158
|
body: JSON.stringify({ steps, ...signIn ? { signIn } : {} })
|
|
155
159
|
});
|
|
156
160
|
}
|
|
157
|
-
|
|
161
|
+
/** This person's open branches that Claude wrote on this project. */
|
|
162
|
+
agentChanges(projectId) {
|
|
163
|
+
return this.call(
|
|
164
|
+
`/api/agent/changes?projectId=${encodeURIComponent(projectId)}`
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
/** Delete one — refused by the service unless it is all Claude's own,
|
|
168
|
+
* unedited, unpublished work. */
|
|
169
|
+
discardChange(projectId, changeId) {
|
|
170
|
+
return this.call(
|
|
171
|
+
`/api/agent/changes/${encodeURIComponent(changeId)}?projectId=${encodeURIComponent(projectId)}`,
|
|
172
|
+
{ method: "DELETE" }
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
saveFlow(projectId, title, flow, where = {}) {
|
|
158
176
|
return this.call(
|
|
159
177
|
`/api/agent/flows?projectId=${encodeURIComponent(projectId)}`,
|
|
160
|
-
{ 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) }
|
|
161
186
|
);
|
|
162
187
|
}
|
|
163
188
|
};
|
|
164
189
|
|
|
165
190
|
// src/mcp/session.ts
|
|
166
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
|
+
}
|
|
167
205
|
var STEP_TIMEOUT_MS = 1e4;
|
|
168
206
|
var ACTIONS = ["click", "dblclick", "rightclick", "hover", "fill", "select", "check", "press", "navigate", "back", "forward"];
|
|
169
207
|
var isAction = (value) => ACTIONS.includes(String(value));
|
|
@@ -224,6 +262,7 @@ var AuthoringSession = class {
|
|
|
224
262
|
page = null;
|
|
225
263
|
values = /* @__PURE__ */ new Map();
|
|
226
264
|
resolvers = null;
|
|
265
|
+
lastTree = null;
|
|
227
266
|
draft = null;
|
|
228
267
|
catalog = null;
|
|
229
268
|
get open() {
|
|
@@ -258,6 +297,7 @@ var AuthoringSession = class {
|
|
|
258
297
|
this.catalog = catalog;
|
|
259
298
|
this.draft = { ...input, steps: [] };
|
|
260
299
|
this.values = /* @__PURE__ */ new Map();
|
|
300
|
+
this.lastTree = null;
|
|
261
301
|
const base = this.baseUrl();
|
|
262
302
|
this.run = await openRunSession({ steps: [], headless: this.opts.headless ?? false });
|
|
263
303
|
this.page = this.run.page;
|
|
@@ -290,19 +330,52 @@ var AuthoringSession = class {
|
|
|
290
330
|
}
|
|
291
331
|
/** The page as Claude reads it: where it is, and what is on it by role and name. */
|
|
292
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) {
|
|
293
345
|
const { page, draft } = this.requireOpen();
|
|
294
346
|
await page.waitForLoadState("domcontentloaded", { timeout: 1e4 }).catch(() => void 0);
|
|
295
|
-
|
|
347
|
+
const tree = labelTree(
|
|
296
348
|
await page.locator("body").ariaSnapshot({ timeout: 8e3 }).catch((e) => `(could not read the page: ${e.message})`),
|
|
297
349
|
await fieldNames(page)
|
|
298
350
|
);
|
|
299
|
-
if (tree.length > MAX_TREE) tree = `${tree.slice(0, MAX_TREE)}
|
|
300
|
-
\u2026 (cut \u2014 the page is long; act on what is above, or scroll with press "PageDown")`;
|
|
301
351
|
const title = await page.title().catch(() => "");
|
|
302
|
-
|
|
352
|
+
const head = `Page: ${page.url()}${title ? ` \u2014 "${title}"` : ""}
|
|
303
353
|
Steps so far: ${draft.steps.length}
|
|
304
354
|
|
|
305
|
-
|
|
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");
|
|
306
379
|
}
|
|
307
380
|
/** Do one thing on the page, and keep it as a step if it worked. */
|
|
308
381
|
async act(input) {
|
|
@@ -340,13 +413,13 @@ ${tree}`;
|
|
|
340
413
|
} catch (error) {
|
|
341
414
|
throw new Error(`That did not work, and was not kept: ${error.message.split("\n")[0]}
|
|
342
415
|
|
|
343
|
-
${await this.
|
|
416
|
+
${await this.view(false)}`);
|
|
344
417
|
}
|
|
345
418
|
await this.settle();
|
|
346
419
|
draft.steps.push(step);
|
|
347
420
|
return `Step ${draft.steps.length} kept: ${describe(step)}.
|
|
348
421
|
|
|
349
|
-
${await this.
|
|
422
|
+
${await this.view(false)}`;
|
|
350
423
|
}
|
|
351
424
|
/** Check something about the page now, and keep the check if it holds. */
|
|
352
425
|
async check(input) {
|
|
@@ -423,7 +496,7 @@ ${await this.snapshot()}`;
|
|
|
423
496
|
draft.steps.push(step);
|
|
424
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" ? `
|
|
425
498
|
|
|
426
|
-
${await this.
|
|
499
|
+
${await this.view(false)}` : "");
|
|
427
500
|
}
|
|
428
501
|
list() {
|
|
429
502
|
const draft = this.draft;
|
|
@@ -459,22 +532,27 @@ ${this.list()}`;
|
|
|
459
532
|
${lines.join("\n")}`;
|
|
460
533
|
}
|
|
461
534
|
/** Save the draft as a flow on a new branch, for a person to review. */
|
|
462
|
-
async save(title) {
|
|
535
|
+
async save(title, where = {}) {
|
|
463
536
|
const { draft } = this.requireOpen();
|
|
464
537
|
if (draft.steps.length === 0) throw new Error("there are no steps to save yet");
|
|
465
538
|
if (!draft.steps.some((s) => s.type === "assert" || s.proposals.length > 0)) {
|
|
466
539
|
throw new Error("the flow checks nothing \u2014 add at least one check (add_check) that proves it worked, then save");
|
|
467
540
|
}
|
|
468
|
-
const saved = await this.service.saveFlow(draft.projectId, title ||
|
|
541
|
+
const saved = await this.service.saveFlow(draft.projectId, title || "Tests written with Claude", {
|
|
469
542
|
name: draft.name,
|
|
470
543
|
...draft.featureId ? { featureId: draft.featureId } : { featureName: draft.featureName },
|
|
471
544
|
startPath: draft.startPath,
|
|
472
545
|
steps: draft.steps,
|
|
473
546
|
...draft.signIn ? { signIn: draft.signIn } : {}
|
|
474
|
-
});
|
|
547
|
+
}, where);
|
|
475
548
|
const review = `${this.service.origin}/projects/${draft.projectId}/changes/${saved.changeId}`;
|
|
476
|
-
|
|
477
|
-
|
|
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}`;
|
|
551
|
+
const others = await this.service.agentChanges(draft.projectId).then((r) => r.changes.filter((c) => c.id !== saved.changeId)).catch(() => []);
|
|
552
|
+
const note = others.length ? `
|
|
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.
|
|
555
|
+
Review and publish it here: ${review}${note}`;
|
|
478
556
|
}
|
|
479
557
|
async close() {
|
|
480
558
|
const run = this.run;
|
|
@@ -520,7 +598,15 @@ How to write a good Flowriki test:
|
|
|
520
598
|
a heading, a message, the URL. Saving is refused for a flow that checks nothing.
|
|
521
599
|
7. Before saving, call replay_flow to run the whole flow from the start. If a
|
|
522
600
|
step fails there, fix it (remove_step, act again) and replay again.
|
|
523
|
-
8. Save with save_flow and give the person the review link it returns.
|
|
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.
|
|
524
610
|
Test data you invent (a new user's email or password for a sign-up) is fine to
|
|
525
611
|
type. For anything that must be NEW on every run \u2014 a sign-up's email or
|
|
526
612
|
username \u2014 use {{unique}}: it is different on every run and the same within
|
|
@@ -600,6 +686,12 @@ It is now the project these tools work on. Open it: ${service.origin}/projects/$
|
|
|
600
686
|
Features:${c.features.length ? "" : " none yet"}`,
|
|
601
687
|
...c.features.map((f) => ` - ${f.name} (id ${f.id})${f.description ? ` \u2014 ${f.description}` : ""}
|
|
602
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
|
+
] : [],
|
|
603
695
|
`
|
|
604
696
|
Sign-in profiles:${c.signIns.length ? "" : " none"}`,
|
|
605
697
|
...c.signIns.map((s) => ` - ${s.name} (profileId ${s.profileId}): ${s.accounts.map((a) => `${a.name} (accountId ${a.accountId})`).join(", ") || "no accounts yet"}`),
|
|
@@ -762,9 +854,75 @@ ${page}`);
|
|
|
762
854
|
{
|
|
763
855
|
name: "save_flow",
|
|
764
856
|
title: "Save the flow to a branch",
|
|
765
|
-
description: "Save the flow
|
|
766
|
-
inputSchema: {
|
|
767
|
-
|
|
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
|
+
}))
|
|
871
|
+
},
|
|
872
|
+
{
|
|
873
|
+
name: "list_my_branches",
|
|
874
|
+
title: "List Claude's open branches",
|
|
875
|
+
description: "The project's open, unpublished branches that Claude wrote for this person, newest first, with review links. Use it to find a superseded branch to discard.",
|
|
876
|
+
inputSchema: { type: "object", properties: { projectId: { type: "string", description: "Optional when you are on one project." } }, additionalProperties: false },
|
|
877
|
+
annotations: { readOnlyHint: true },
|
|
878
|
+
run: async (args) => {
|
|
879
|
+
const projectId = await projectFor(args["projectId"]);
|
|
880
|
+
const { changes } = await service.agentChanges(projectId);
|
|
881
|
+
if (!changes.length) return text("Claude has no open branches on this project.");
|
|
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"));
|
|
883
|
+
}
|
|
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
|
+
},
|
|
909
|
+
{
|
|
910
|
+
name: "discard_branch",
|
|
911
|
+
title: "Delete one of Claude's branches",
|
|
912
|
+
description: "Delete an open branch Claude wrote, with the flows on it \u2014 e.g. one a redo replaced. Only after the person asked for it or agreed. Refused for a branch a person started, edited, or published; those are deleted in the dashboard.",
|
|
913
|
+
inputSchema: {
|
|
914
|
+
type: "object",
|
|
915
|
+
properties: { changeId: { type: "string", description: "The branch's id from list_my_branches (chg_\u2026)." }, projectId: { type: "string" } },
|
|
916
|
+
required: ["changeId"],
|
|
917
|
+
additionalProperties: false
|
|
918
|
+
},
|
|
919
|
+
annotations: { destructiveHint: true },
|
|
920
|
+
run: async (args) => {
|
|
921
|
+
const projectId = await projectFor(args["projectId"]);
|
|
922
|
+
const changeId = str(args["changeId"]) ?? "";
|
|
923
|
+
const { deleted } = await service.discardChange(projectId, changeId);
|
|
924
|
+
return text(`Deleted ${changeId} and its branch ${deleted}.`);
|
|
925
|
+
}
|
|
768
926
|
},
|
|
769
927
|
{
|
|
770
928
|
name: "close_browser",
|
package/dist/engine.js
CHANGED
package/package.json
CHANGED