@flowriki/agent 0.1.0-beta.58 → 0.1.0-beta.59

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.
@@ -68,7 +68,7 @@ import {
68
68
  textIndependent,
69
69
  validateActors,
70
70
  webCloudOf
71
- } from "./chunk-O34WXNH6.js";
71
+ } from "./chunk-MJXAVVGP.js";
72
72
 
73
73
  // src/engine/machine.ts
74
74
  import fs from "fs";
@@ -76,7 +76,7 @@ import os from "os";
76
76
  import path from "path";
77
77
  import { createRequire } from "module";
78
78
  import { spawnSync } from "child_process";
79
- var AGENT_VERSION = "0.1.0-beta.58";
79
+ var AGENT_VERSION = "0.1.0-beta.59";
80
80
  async function prepareBrowsers() {
81
81
  if (browsersReady()) {
82
82
  return { ok: true, already: true, message: "chromium is already installed" };
@@ -136,7 +136,7 @@ async function checkMachine() {
136
136
  }
137
137
  } catch {
138
138
  }
139
- const { mobileDoctor: mobileDoctor2 } = await import("./dist-RDJIIBQV.js");
139
+ const { mobileDoctor: mobileDoctor2 } = await import("./dist-BB5LTG3H.js");
140
140
  return {
141
141
  agentVersion: AGENT_VERSION,
142
142
  node: process.versions.node,
@@ -10210,6 +10210,16 @@ ${brief}` : ""
10210
10210
  ...opts.sessionId ? ["--resume", opts.sessionId] : []
10211
10211
  ];
10212
10212
  }
10213
+ function clearOld(dir) {
10214
+ try {
10215
+ for (const name of fs43.readdirSync(dir)) {
10216
+ if (!/^mcp-.+\.json$/.test(name)) continue;
10217
+ const file2 = path42.join(dir, name);
10218
+ if (Date.now() - fs43.statSync(file2).mtimeMs > 24 * 60 * 6e4) fs43.rmSync(file2, { force: true });
10219
+ }
10220
+ } catch {
10221
+ }
10222
+ }
10213
10223
  function eventsFromLine(askId, line) {
10214
10224
  let parsed;
10215
10225
  try {
@@ -10378,10 +10388,11 @@ function createClaudeCli(options) {
10378
10388
  fs43.mkdirSync(scratchRoot2, { recursive: true, mode: 448 });
10379
10389
  const dir = path42.join(scratchRoot2, `project-${ask.projectId.replace(/[^A-Za-z0-9_-]/g, "_")}`);
10380
10390
  fs43.mkdirSync(dir, { recursive: true, mode: 448 });
10381
- const mcpConfigPath = path42.join(dir, "mcp.json");
10391
+ clearOld(dir);
10392
+ const mcpConfigPath = path42.join(dir, `mcp-${ask.askId.replace(/[^A-Za-z0-9_-]/g, "_").slice(0, 80)}.json`);
10382
10393
  fs43.writeFileSync(
10383
10394
  mcpConfigPath,
10384
- JSON.stringify({ mcpServers: { [MCP_SERVER_NAME]: { command: options.mcp.command, args: options.mcp.args } } }),
10395
+ JSON.stringify({ mcpServers: { [MCP_SERVER_NAME]: { command: options.mcp.command, args: options.mcp.args, env: { FLOWRIKI_ASK_ID: ask.askId } } } }),
10385
10396
  { mode: 384 }
10386
10397
  );
10387
10398
  return launch(ask, dir, mcpConfigPath, laneSession(ask, dir));
@@ -687,7 +687,13 @@ var ExecutionConfigSchema = z3.object({
687
687
  var BUSINESS_KEYWORDS = ["Given", "When", "Then", "And", "But"];
688
688
  var BusinessStepSchema = z22.object({
689
689
  keyword: z22.enum(BUSINESS_KEYWORDS),
690
- text: z22.string().min(1).max(2e3)
690
+ text: z22.string().min(1).max(2e3),
691
+ /** THE CASE STEPS THAT CARRY IT OUT, once it is automated — each as
692
+ * `<occurrenceId>/<stepId>`, so a person reads the case line by line with
693
+ * the steps under the words they prove. Said by whoever automated it (the
694
+ * assistant, when it builds the case). A step whose row is gone is simply
695
+ * not found, and shows with the steps no line claims. */
696
+ steps: z22.array(z22.string().min(1).max(200)).max(500).optional()
691
697
  }).strict();
692
698
  var TestCaseOriginSchema = z22.object({
693
699
  kind: z22.enum(["import", "assistant"]),
package/dist/cli.js CHANGED
@@ -24,7 +24,7 @@ import {
24
24
  runtimeValue,
25
25
  start,
26
26
  status
27
- } from "./chunk-RLRHWYSJ.js";
27
+ } from "./chunk-HXUFFE2E.js";
28
28
  import {
29
29
  CLOUD_PROVIDERS,
30
30
  CLOUD_PROVIDER_NAME,
@@ -51,7 +51,7 @@ import {
51
51
  slugify,
52
52
  stepRefusedOn,
53
53
  targetFor
54
- } from "./chunk-O34WXNH6.js";
54
+ } from "./chunk-MJXAVVGP.js";
55
55
 
56
56
  // src/cli.ts
57
57
  import { parseArgs } from "util";
@@ -137,6 +137,7 @@ function serve(tools, info, io = {}) {
137
137
  }
138
138
 
139
139
  // src/mcp/service-client.ts
140
+ var ASK_ID = (process.env["FLOWRIKI_ASK_ID"] ?? "").trim().slice(0, 200);
140
141
  var ServiceClient = class {
141
142
  constructor(connection, fetcher = fetch) {
142
143
  this.connection = connection;
@@ -159,7 +160,10 @@ var ServiceClient = class {
159
160
  headers: {
160
161
  "x-flowriki-device": this.connection.deviceId,
161
162
  "x-flowriki-device-secret": this.connection.secret,
162
- "content-type": "application/json"
163
+ "content-type": "application/json",
164
+ // The dashboard's question these tools are answering, when they are:
165
+ // what they build is filed on it, for the person to watch.
166
+ ...ASK_ID ? { "x-flowriki-ask": ASK_ID } : {}
163
167
  }
164
168
  });
165
169
  const text2 = await res.text();
@@ -255,6 +259,12 @@ var ServiceClient = class {
255
259
  );
256
260
  }
257
261
  /** Build a test case from flows — as a dry run, or saved. */
262
+ /** The flow being recorded, as it stands, for the dashboard's job. Only
263
+ * when these tools answer a question asked in the dashboard. */
264
+ async reportDraft(projectId, draft) {
265
+ if (!ASK_ID) return;
266
+ await this.call(`/api/agent/jobs/draft?projectId=${encodeURIComponent(projectId)}`, { method: "POST", body: JSON.stringify({ draft }) });
267
+ }
258
268
  buildCase(projectId, spec, dryRun) {
259
269
  return this.call(`/api/agent/cases?projectId=${encodeURIComponent(projectId)}`, { method: "POST", body: JSON.stringify({ ...spec, dryRun }) });
260
270
  }
@@ -497,11 +507,21 @@ function deviceFrom(said, platform, devices, mobile) {
497
507
  async function devicesSummary() {
498
508
  return listDevices().catch(() => []);
499
509
  }
500
- function describeRows(rows) {
501
- return rows.map((r) => [
502
- `Row ${r.row}: "${r.flow}" (${r.browser})`,
503
- ...r.checks.length ? r.checks.map((c) => ` ${c.on ? "[on] " : "[off]"} ${c.n}. ${c.check}`) : [" (no checks in this flow)"]
504
- ].join("\n")).join("\n");
510
+ function describeRows(rows, testCase) {
511
+ return rows.map((r, i) => {
512
+ const steps = testCase?.occurrences[i]?.steps;
513
+ if (r.steps?.length && steps?.length === r.steps.length) {
514
+ const on = new Map(r.checks.map((c) => [c.n, c.on]));
515
+ return [
516
+ `Row ${r.row}: "${r.flow}" (${r.browser})`,
517
+ ...r.steps.map((st, j) => ` step ${st.n}. ${describe(steps[j])}${st.check !== void 0 ? ` \u2014 check ${st.check} ${on.get(st.check) ? "[on]" : "[off]"}` : ""}`)
518
+ ].join("\n");
519
+ }
520
+ return [
521
+ `Row ${r.row}: "${r.flow}" (${r.browser})`,
522
+ ...r.checks.length ? r.checks.map((c) => ` ${c.on ? "[on] " : "[off]"} ${c.n}. ${c.check}`) : [" (no checks in this flow)"]
523
+ ].join("\n");
524
+ }).join("\n");
505
525
  }
506
526
  function describe(step) {
507
527
  const target = (step.name ? `${step.role || "element"} "${step.name}"` : step.role || "") + (step.nth !== void 0 ? ` (nth ${step.nth})` : "");
@@ -634,6 +654,7 @@ var AuthoringSession = class _AuthoringSession {
634
654
  const left = opts.fresh ? null : this.readDraft(input.projectId, input.name);
635
655
  if (opts.fresh) this.dropDraft(input.projectId, input.name);
636
656
  const opened = isMobile(input.platform) ? await this.startMobile(input.platform, catalog) : await this.startWeb(input, catalog);
657
+ this.report();
637
658
  if (!left?.steps.length) return opened;
638
659
  return this.resume(left.steps, opened);
639
660
  }
@@ -686,6 +707,7 @@ ${view}`;
686
707
  const draft = this.draft;
687
708
  if (!draft) return;
688
709
  this.verified = null;
710
+ this.report();
689
711
  try {
690
712
  const file = _AuthoringSession.draftFile(draft.projectId, draft.name);
691
713
  fs.mkdirSync(path.dirname(file), { recursive: true, mode: 448 });
@@ -694,6 +716,36 @@ ${view}`;
694
716
  } catch {
695
717
  }
696
718
  }
719
+ /** The draft as it stands, to the dashboard question this answers — at
720
+ * most every half second, a recording keeps a step a second. */
721
+ reportTimer = null;
722
+ report() {
723
+ if (this.reportTimer) return;
724
+ this.reportTimer = setTimeout(() => void this.sendReport(), 500);
725
+ this.reportTimer.unref?.();
726
+ }
727
+ /** Whatever is waiting to be reported, now — before a save or a close, so
728
+ * the dashboard ends on the flow as it was saved. */
729
+ async sendReport() {
730
+ if (this.reportTimer) {
731
+ clearTimeout(this.reportTimer);
732
+ this.reportTimer = null;
733
+ }
734
+ {
735
+ const draft = this.draft;
736
+ if (!draft) return;
737
+ const featureName = draft.featureName ?? this.catalog?.features?.find((f) => f.id === draft.featureId)?.name ?? "";
738
+ if (typeof this.service.reportDraft !== "function") return;
739
+ await this.service.reportDraft(draft.projectId, {
740
+ name: draft.name,
741
+ feature: featureName,
742
+ platform: draft.platform ?? "web",
743
+ after: (draft.after ?? []).map((a) => a.name),
744
+ signsIn: Boolean(draft.signIn),
745
+ steps: withoutTyped(draft.steps).map((st) => st.secret ? { ...st, value: "" } : st)
746
+ }).catch(() => void 0);
747
+ }
748
+ }
697
749
  readDraft(projectId, name) {
698
750
  try {
699
751
  const raw = JSON.parse(fs.readFileSync(_AuthoringSession.draftFile(projectId, name), "utf8"));
@@ -1019,6 +1071,7 @@ Fix the step that failed (remove_step, then act again) and save again \u2014 or
1019
1071
  replayed = " It was replayed from the start first and passed.";
1020
1072
  }
1021
1073
  const checksOff = draft.steps.map((step) => step.type === "assert" ? { ...step, proposals: step.proposals.map((p) => ({ ...p, enabled: false })) } : step);
1074
+ if (this.reportTimer) await this.sendReport();
1022
1075
  const checkCount = checksOff.filter((step) => step.type === "assert").length;
1023
1076
  const secretValues = {};
1024
1077
  for (const step of checksOff) {
@@ -1052,7 +1105,7 @@ Review and publish it here: ${review}${note}`;
1052
1105
  async previewCase(projectId, spec) {
1053
1106
  const built = await this.service.buildCase(projectId, spec, true);
1054
1107
  return `"${spec.name}" in ${spec.folder} would be:
1055
- ${describeRows(built.rows)}`;
1108
+ ${describeRows(built.rows, built.testCase)}`;
1056
1109
  }
1057
1110
  /** RUN THE CASE, THEN SAVE IT. Built as a dry run, replayed in a fresh
1058
1111
  * browser exactly as a test run does it, and saved onto Claude's working
@@ -1102,7 +1155,7 @@ ${describeRows(built.rows)}`;
1102
1155
  return `The case did not pass, so it was NOT saved.
1103
1156
  ${lines.join("\n")}
1104
1157
 
1105
- ${describeRows(built.rows)}`;
1158
+ ${describeRows(built.rows, testCase)}`;
1106
1159
  }
1107
1160
  const saved = await this.service.buildCase(projectId, spec, false);
1108
1161
  const review = `${this.service.origin}${saved.path}`;
@@ -1111,6 +1164,7 @@ ${lines.join("\n")}
1111
1164
  Review and publish it here: ${review}`;
1112
1165
  }
1113
1166
  async close() {
1167
+ if (this.reportTimer) await this.sendReport();
1114
1168
  const run = this.run;
1115
1169
  const mobile = this.mobile;
1116
1170
  this.run = null;
@@ -1445,10 +1499,18 @@ HOW TO WORK:
1445
1499
  catalog, then type {{its saveAs name}} into the field that needs it.
1446
1500
  5. Add checks for what each step proves (a heading, a message, a value, the
1447
1501
  URL). replay_flow, then save_flow. A flow must record at least one check.
1448
- 6. Build each test case: preview_case lists its rows and every check, numbered;
1449
- save_case with the numbers to switch on runs the whole case and saves it only
1450
- if it passed. Give the person the review link.
1451
- 7. Saving the same name again replaces it on the working branch. Old branches
1502
+ 6. Build each test case: preview_case lists its rows and every step, numbered
1503
+ across the case, with each check's number in its row; save_case with the
1504
+ checks to switch on runs the whole case and saves it only if it passed.
1505
+ Pass businessSteps to both: the case's Given / When / Then lines (its own,
1506
+ from read_test_cases, when it has them) each with the step numbers that
1507
+ carry it out, so the person reads every line with its steps under it.
1508
+ Give the person the review link.
1509
+ 7. Saving the same name again replaces it on the working branch. ONE VERSION
1510
+ of each scenario per platform: redo a flow or case under its existing name
1511
+ (project_catalog lists them) rather than saving another beside it. When the
1512
+ project tests several platforms, name flows "<scenario> \u2014 Web", "\u2014 Android",
1513
+ "\u2014 iOS". Old branches
1452
1514
  of yours? gather_branches puts them into one; discard_branch deletes one \u2014
1453
1515
  only after the person asks or agrees.
1454
1516
  Test data you invent (a new user's email or password for a sign-up) is fine to
@@ -1480,6 +1542,19 @@ var CASE_INPUT = {
1480
1542
  additionalProperties: false
1481
1543
  }
1482
1544
  },
1545
+ businessSteps: {
1546
+ type: "array",
1547
+ description: 'The case in Given / When / Then lines, in order, each with the step numbers from preview_case that carry it out \u2014 e.g. {"step": "Given the user is on the sign-in page", "steps": [1, 2]}. A case that already has business steps keeps its own words: give the same lines.',
1548
+ items: {
1549
+ type: "object",
1550
+ properties: {
1551
+ step: { type: "string", description: "One line, starting Given, When, Then, And or But." },
1552
+ steps: { type: "array", items: { type: "number" }, description: "The step numbers (across the case) that carry it out." }
1553
+ },
1554
+ required: ["step"],
1555
+ additionalProperties: false
1556
+ }
1557
+ },
1483
1558
  branch: { type: "string", description: "Optional: save onto this branch (chg_\u2026) instead of the working one." },
1484
1559
  projectId: { type: "string" }
1485
1560
  },
@@ -1491,6 +1566,7 @@ var caseSpec = (args) => ({
1491
1566
  folder: String(args["folder"] ?? ""),
1492
1567
  ...args["description"] ? { description: String(args["description"]) } : {},
1493
1568
  rows: Array.isArray(args["rows"]) ? args["rows"] : [],
1569
+ ...Array.isArray(args["businessSteps"]) ? { businessSteps: args["businessSteps"] } : {},
1494
1570
  ...args["branch"] ? { changeId: String(args["branch"]) } : {}
1495
1571
  });
1496
1572
  var text = (value, isError = false) => ({ content: [{ type: "text", text: value }], ...isError ? { isError } : {} });
@@ -46,7 +46,7 @@ import {
46
46
  textOf,
47
47
  toolEnv,
48
48
  typesSecrets
49
- } from "./chunk-O34WXNH6.js";
49
+ } from "./chunk-MJXAVVGP.js";
50
50
  export {
51
51
  AGENT_HOME,
52
52
  APPIUM_DIR,
package/dist/engine.js CHANGED
@@ -9,8 +9,8 @@ import {
9
9
  resolveCheckouts,
10
10
  start,
11
11
  status
12
- } from "./chunk-RLRHWYSJ.js";
13
- import "./chunk-O34WXNH6.js";
12
+ } from "./chunk-HXUFFE2E.js";
13
+ import "./chunk-MJXAVVGP.js";
14
14
  export {
15
15
  AGENT_VERSION,
16
16
  browsersReady,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flowriki/agent",
3
- "version": "0.1.0-beta.58",
3
+ "version": "0.1.0-beta.59",
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",