@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.
@@ -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.32";
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-4EYN6N5Q.js";
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: false } },
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
- saveFlow(projectId, title, flow) {
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
- let tree = labelTree(
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
- return `Page: ${page.url()}${title ? ` \u2014 "${title}"` : ""}
352
+ const head = `Page: ${page.url()}${title ? ` \u2014 "${title}"` : ""}
303
353
  Steps so far: ${draft.steps.length}
304
354
 
305
- ${tree}`;
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.snapshot()}`);
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.snapshot()}`;
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.snapshot()}` : "");
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 || `Write "${draft.name}" with Claude`, {
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
- return `Saved "${draft.name}" on a new branch, ${saved.branch}. Nothing is published yet.
477
- Review and publish it here: ${review}`;
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 to a new branch in the project's repository, for a person to review and publish. Returns the review link. Refused if the flow checks nothing.",
766
- inputSchema: { type: "object", properties: { title: { type: "string", description: "The branch's title \u2014 what this change adds." } }, additionalProperties: false },
767
- run: async (args) => text(await session.save(str(args["title"])))
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
@@ -9,7 +9,7 @@ import {
9
9
  resolveCheckouts,
10
10
  start,
11
11
  status
12
- } from "./chunk-4EYN6N5Q.js";
12
+ } from "./chunk-7OODSC7P.js";
13
13
  export {
14
14
  AGENT_VERSION,
15
15
  browsersReady,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flowriki/agent",
3
- "version": "0.1.0-beta.32",
3
+ "version": "0.1.0-beta.34",
4
4
  "description": "Run Flowriki UI tests on your own computer. Connects this machine to your Flowriki account.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "type": "module",