@flowriki/agent 0.1.0-beta.46 → 0.1.0-beta.47

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/README.md CHANGED
@@ -49,6 +49,14 @@ email"*. Claude reads your project's features, sign-in profiles and helpers,
49
49
  and uses them. You do not run `mcp` yourself — Claude starts it. It works
50
50
  alongside `start`, in a browser window of its own.
51
51
 
52
+ **It keeps itself up to date.** `@latest` starts the newest Flowriki each time
53
+ Claude connects, and while Claude stays connected Flowriki checks for a newer
54
+ release every half hour and switches over to it between tasks — never while a
55
+ flow is being written. The next answer says it was updated; if the new release
56
+ brings new tools and Claude does not offer them yet, reconnect Flowriki in
57
+ Claude (in Claude Code: `/mcp`, then Reconnect). The agent you run with `start`
58
+ says when it is behind: `npm install -g @flowriki/agent@latest`, then restart it.
59
+
52
60
  ## What you do not need
53
61
 
54
62
  No checkout of Flowriki, and nothing to build. `connect` and `start` are the
@@ -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.46";
9
+ var AGENT_VERSION = "0.1.0-beta.47";
10
10
  async function prepareBrowsers() {
11
11
  if (browsersReady()) {
12
12
  return { ok: true, already: true, message: "chromium is already installed" };
@@ -356,6 +356,29 @@ var ProjectMarkerSchema = z2.object({
356
356
  signature: z2.string()
357
357
  });
358
358
  var PRODUCTION_SERVICE = "https://flowriki-cloud.onrender.com";
359
+ var AGENT_LATEST_URL = "https://registry.npmjs.org/@flowriki/agent/latest";
360
+ function compareAgentVersions(a, b2) {
361
+ const split = (v) => {
362
+ const [core = "", pre = ""] = v.trim().replace(/^v/, "").split("-", 2);
363
+ return { core: core.split(".").map((n2) => Number(n2) || 0), pre: pre ? pre.split(".") : [] };
364
+ };
365
+ const x = split(a), y = split(b2);
366
+ for (let i = 0; i < Math.max(x.core.length, y.core.length); i++) {
367
+ const d = (x.core[i] ?? 0) - (y.core[i] ?? 0);
368
+ if (d) return Math.sign(d);
369
+ }
370
+ if (!x.pre.length || !y.pre.length) return x.pre.length === y.pre.length ? 0 : x.pre.length ? -1 : 1;
371
+ for (let i = 0; i < Math.max(x.pre.length, y.pre.length); i++) {
372
+ const p = x.pre[i], q = y.pre[i];
373
+ if (p === void 0) return -1;
374
+ if (q === void 0) return 1;
375
+ const np = Number(p), nq = Number(q);
376
+ const d = !Number.isNaN(np) && !Number.isNaN(nq) ? np - nq : p.localeCompare(q);
377
+ if (d) return Math.sign(d);
378
+ }
379
+ return 0;
380
+ }
381
+ var agentUpdateFor = (running2, latest) => Boolean(running2 && latest && running2 !== "test" && compareAgentVersions(latest, running2) > 0);
359
382
 
360
383
  // src/service.ts
361
384
  var BadServiceUrl = class extends Error {
@@ -482,11 +505,11 @@ async function verifyConnection(connection) {
482
505
 
483
506
  // src/engine/checkouts.ts
484
507
  import fs9 from "fs";
485
- import path11 from "path";
508
+ import path12 from "path";
486
509
 
487
510
  // ../execution/dist/index.js
488
511
  import fs8 from "fs";
489
- import path10 from "path";
512
+ import path11 from "path";
490
513
 
491
514
  // ../engine-playwright/dist/index.js
492
515
  import { randomBytes } from "crypto";
@@ -1431,11 +1454,13 @@ function sessionLanes(occurrences, defaultActorId) {
1431
1454
  return sessionId;
1432
1455
  };
1433
1456
  for (const occurrence of occurrences) {
1434
- const actorId = occurrence.actorId || defaultActorId || "default";
1457
+ const person = occurrence.actorId || defaultActorId || "default";
1458
+ const phone = occurrence.platform === "android" || occurrence.platform === "ios" ? occurrence.platform : void 0;
1459
+ const actorId = phone ? `${phone}:${person}` : person;
1435
1460
  const continues = startOf(occurrence) === "continue";
1436
1461
  const asked = occurrence.start === "continue";
1437
1462
  const theirs = mine.get(actorId);
1438
- const adoptable = previous !== null && owner.get(previous) === "default" && actorId !== "default";
1463
+ const adoptable = !phone && previous !== null && owner.get(previous) === "default" && actorId !== "default";
1439
1464
  let sessionId;
1440
1465
  if (continues && theirs) sessionId = theirs;
1441
1466
  else if (asked && adoptable) {
@@ -1447,7 +1472,8 @@ function sessionLanes(occurrences, defaultActorId) {
1447
1472
  const continuesFrom = last.get(sessionId) ?? null;
1448
1473
  rows.push({
1449
1474
  occurrenceId: occurrence.occurrenceId,
1450
- actorId,
1475
+ actorId: person,
1476
+ ...phone ? { phone } : {},
1451
1477
  incarnation: Number(sessionId.split("#")[1]),
1452
1478
  sessionId,
1453
1479
  lane: lanes.get(sessionId),
@@ -2647,8 +2673,8 @@ function installRecorder() {
2647
2673
  return Boolean(el?.closest?.("[data-flowriki],[data-flowriki-debug]"));
2648
2674
  }
2649
2675
  function actualTarget(e) {
2650
- const path14 = typeof e.composedPath === "function" ? e.composedPath() : [];
2651
- const first = path14[0];
2676
+ const path15 = typeof e.composedPath === "function" ? e.composedPath() : [];
2677
+ const first = path15[0];
2652
2678
  return (first && first.tagName ? first : e.target) ?? null;
2653
2679
  }
2654
2680
  w.__flSignature = () => {
@@ -7191,8 +7217,9 @@ import path33 from "path";
7191
7217
  import fs43 from "fs";
7192
7218
  import path42 from "path";
7193
7219
  import path5 from "path";
7194
- import fs52 from "fs";
7195
7220
  import path6 from "path";
7221
+ import fs52 from "fs";
7222
+ import path7 from "path";
7196
7223
  import { spawn as spawn3 } from "child_process";
7197
7224
  var WebDriverError = class extends Error {
7198
7225
  constructor(message, error, status2) {
@@ -7204,10 +7231,10 @@ var WebDriverError = class extends Error {
7204
7231
  status;
7205
7232
  };
7206
7233
  var ELEMENT_KEY = "element-6066-11e4-a52e-4f735466cecc";
7207
- async function call(endpoint, method, path72, body, timeoutMs = 12e4) {
7234
+ async function call(endpoint, method, path82, body, timeoutMs = 12e4) {
7208
7235
  const headers = { "content-type": "application/json; charset=utf-8" };
7209
7236
  if (endpoint.username) headers["authorization"] = `Basic ${Buffer.from(`${endpoint.username}:${endpoint.password ?? ""}`).toString("base64")}`;
7210
- const res = await fetch(`${endpoint.url.replace(/\/+$/, "")}${path72}`, {
7237
+ const res = await fetch(`${endpoint.url.replace(/\/+$/, "")}${path82}`, {
7211
7238
  method,
7212
7239
  headers,
7213
7240
  ...method === "POST" ? { body: JSON.stringify(body ?? {}) } : {},
@@ -7246,11 +7273,11 @@ var WebDriverSession = class _WebDriverSession {
7246
7273
  return new _WebDriverSession(endpoint, value.sessionId, value.capabilities ?? {});
7247
7274
  }
7248
7275
  /** Any other command — Appium's own extensions live under /appium/. */
7249
- raw(method, path72, body, timeoutMs) {
7250
- return this.req(method, path72, body, timeoutMs);
7276
+ raw(method, path82, body, timeoutMs) {
7277
+ return this.req(method, path82, body, timeoutMs);
7251
7278
  }
7252
- req(method, path72, body, timeoutMs) {
7253
- return call(this.endpoint, method, `/session/${this.id}${path72}`, body, timeoutMs);
7279
+ req(method, path82, body, timeoutMs) {
7280
+ return call(this.endpoint, method, `/session/${this.id}${path82}`, body, timeoutMs);
7254
7281
  }
7255
7282
  source() {
7256
7283
  return this.req("GET", "/source");
@@ -8494,7 +8521,37 @@ var MobileSession = class _MobileSession {
8494
8521
  await this.tap(button);
8495
8522
  return button.name;
8496
8523
  }
8524
+ // ── Video ──────────────────────────────────────────────────────────────
8525
+ recording = false;
8526
+ /** Start filming the screen. Android's own recorder stops at three minutes
8527
+ * unless Appium can join pieces with ffmpeg; iOS needs ffmpeg at all. A
8528
+ * phone in a cloud farm is filmed by the farm. Answers whether it started. */
8529
+ async startVideo() {
8530
+ if (this.cloud) return false;
8531
+ const ffmpeg = Boolean(onPath("ffmpeg"));
8532
+ if (this.platform === "ios" && !ffmpeg) return false;
8533
+ const options = this.platform === "android" ? { timeLimit: ffmpeg ? "1800" : "180", bitRate: "3000000", forceRestart: true } : { timeLimit: "1800", videoType: "libx264", videoFps: 10, videoQuality: "medium", forceRestart: true };
8534
+ try {
8535
+ await this.driver.raw("POST", "/appium/start_recording_screen", { options });
8536
+ this.recording = true;
8537
+ } catch {
8538
+ this.recording = false;
8539
+ }
8540
+ return this.recording;
8541
+ }
8542
+ /** Stop filming; keep the film at `file`, or throw it away. */
8543
+ async stopVideo(file2) {
8544
+ if (!this.recording) return null;
8545
+ this.recording = false;
8546
+ const b64 = await this.driver.raw("POST", "/appium/stop_recording_screen", { options: {} }, 12e4).catch(() => "");
8547
+ if (!file2 || !b64) return null;
8548
+ const bytes = Buffer.from(b64, "base64");
8549
+ fs43.mkdirSync(path42.dirname(file2), { recursive: true });
8550
+ fs43.writeFileSync(file2, bytes);
8551
+ return { relPath: path42.basename(file2), bytes: bytes.length };
8552
+ }
8497
8553
  async close() {
8554
+ if (this.recording) await this.stopVideo(null);
8498
8555
  await this.driver.close();
8499
8556
  }
8500
8557
  };
@@ -8767,6 +8824,13 @@ async function runMobileSteps(session, steps, opts) {
8767
8824
  }
8768
8825
  return { results, status: status2, error };
8769
8826
  }
8827
+ var typesSecrets = (steps) => steps.some((s2) => s2.secret || Boolean(s2.secretRef) || s2.type === "signIn");
8828
+ var filmsThis = (policy, steps, evidenceDir) => Boolean(evidenceDir) && Boolean(policy) && policy.mode !== "off" && (policy.allowUnmasked === true || !typesSecrets(steps));
8829
+ async function finishVideo(session, policy, evidenceDir, passed, name = "phone") {
8830
+ const keep = Boolean(evidenceDir) && (policy?.mode === "always" || policy?.mode === "on-failure" && !passed);
8831
+ const saved = await session.stopVideo(keep ? path6.join(evidenceDir, `video-${name}.mp4`) : null).catch(() => null);
8832
+ return saved ? [{ kind: "video", relPath: saved.relPath, bytes: saved.bytes }] : [];
8833
+ }
8770
8834
  async function chooseDevice(platform, mobile, asked) {
8771
8835
  const chosen = asked ?? mobile.devices[platform];
8772
8836
  if (chosen) return chosen;
@@ -8800,9 +8864,11 @@ async function replayMobileFlow(opts) {
8800
8864
  device: describeDevice(opts.device ?? null)
8801
8865
  };
8802
8866
  }
8867
+ const filming = filmsThis(opts.video, opts.steps, opts.evidenceDir) && await session.startVideo();
8868
+ let outcome = null;
8803
8869
  try {
8804
8870
  if (opts.startPath && /^[a-z][\w+.-]*:\/\//i.test(opts.startPath)) await session.openLink(opts.startPath);
8805
- const { results, status: status2, error } = await runMobileSteps(session, opts.steps, {
8871
+ outcome = await runMobileSteps(session, opts.steps, {
8806
8872
  values: /* @__PURE__ */ new Map(),
8807
8873
  ...opts.resolveSecret ? { resolveSecret: opts.resolveSecret } : {},
8808
8874
  ...opts.resolveAuth ? { resolveAuth: opts.resolveAuth } : {},
@@ -8812,6 +8878,8 @@ async function replayMobileFlow(opts) {
8812
8878
  ...opts.screenshots ? { screenshots: opts.screenshots } : {},
8813
8879
  ...opts.signal ? { signal: opts.signal } : {}
8814
8880
  });
8881
+ const { results, status: status2, error } = outcome;
8882
+ const artifacts = filming ? await finishVideo(session, opts.video, opts.evidenceDir, status2 === "passed") : [];
8815
8883
  return {
8816
8884
  status: status2,
8817
8885
  error,
@@ -8819,7 +8887,8 @@ async function replayMobileFlow(opts) {
8819
8887
  stepsRun: results.filter((r) => r.status !== "skipped").length,
8820
8888
  stepsTotal: opts.steps.length,
8821
8889
  steps: results,
8822
- device: session.deviceName
8890
+ device: session.deviceName,
8891
+ ...artifacts.length ? { artifacts } : {}
8823
8892
  };
8824
8893
  } finally {
8825
8894
  await session.close();
@@ -8841,7 +8910,14 @@ async function mobileDoctor() {
8841
8910
  const drivers = await installedDrivers();
8842
8911
  const appiumVersion = appium ? (await run(appium.bin, ["--version"], 3e4, await toolEnv()).catch(() => "")).trim().split("\n").pop() ?? "" : "";
8843
8912
  const appiumLines = [
8844
- appium ? { item: "Appium", ok: true, detail: `${appiumVersion || "installed"} (${appium.bin.startsWith(APPIUM_DIR) ? "installed by Flowriki" : appium.bin})` } : { item: "Appium", ok: false, detail: "not installed", fix: "flowriki-agent mobile setup" }
8913
+ appium ? { item: "Appium", ok: true, detail: `${appiumVersion || "installed"} (${appium.bin.startsWith(APPIUM_DIR) ? "installed by Flowriki" : appium.bin})` } : { item: "Appium", ok: false, detail: "not installed", fix: "flowriki-agent mobile setup" },
8914
+ onPath("ffmpeg") ? { item: "ffmpeg", ok: true, detail: "phone runs can be filmed", optional: true } : {
8915
+ item: "ffmpeg",
8916
+ ok: false,
8917
+ optional: true,
8918
+ detail: "not installed \u2014 Android videos stop at 3 minutes, iOS runs cannot be filmed",
8919
+ fix: process.platform === "darwin" ? "brew install ffmpeg" : process.platform === "win32" ? "winget install ffmpeg" : "install ffmpeg with your package manager"
8920
+ }
8845
8921
  ];
8846
8922
  const sdk = androidHome();
8847
8923
  const adb = adbPath();
@@ -8865,7 +8941,7 @@ async function mobileDoctor() {
8865
8941
  const iphones = devices.filter((d) => d.platform === "ios");
8866
8942
  ios.push(iphones.length ? { item: "Devices", ok: true, detail: `${iphones.filter((d) => d.kind === "simulator").length} simulators${iphones.some((d) => d.kind === "device") ? `, ${iphones.filter((d) => d.kind === "device").map((d) => d.name).join(", ")}` : ""}` } : { item: "Devices", ok: false, detail: "none", fix: "Xcode \u2192 Settings \u2192 Platforms \u2192 add an iOS simulator" });
8867
8943
  }
8868
- const okAll = (lines) => lines.every((l) => l.ok);
8944
+ const okAll = (lines) => lines.every((l) => l.ok || l.optional);
8869
8945
  return { appium: appiumLines, android, ios, androidReady: okAll(appiumLines) && okAll(android), iosReady: okAll(appiumLines) && okAll(ios) };
8870
8946
  }
8871
8947
  function stream(file2, args, env, cwd, say) {
@@ -8875,20 +8951,20 @@ function stream(file2, args, env, cwd, say) {
8875
8951
  child.stdout.on("data", each);
8876
8952
  child.stderr.on("data", each);
8877
8953
  child.on("error", reject);
8878
- child.on("close", (code) => code === 0 ? resolve2() : reject(new Error(`${path6.basename(file2)} ${args.join(" ")} failed (${code})`)));
8954
+ child.on("close", (code) => code === 0 ? resolve2() : reject(new Error(`${path7.basename(file2)} ${args.join(" ")} failed (${code})`)));
8879
8955
  });
8880
8956
  }
8881
8957
  async function mobileSetup(say = console.log) {
8882
8958
  const npm = onPath("npm");
8883
8959
  if (!npm) throw new Error("npm was not found \u2014 install Node.js 20 or newer.");
8884
- fs52.mkdirSync(path6.join(APPIUM_DIR, "home"), { recursive: true });
8885
- if (!fs52.existsSync(path6.join(APPIUM_DIR, "package.json"))) {
8886
- fs52.writeFileSync(path6.join(APPIUM_DIR, "package.json"), JSON.stringify({ name: "flowriki-appium", private: true }, null, 2));
8960
+ fs52.mkdirSync(path7.join(APPIUM_DIR, "home"), { recursive: true });
8961
+ if (!fs52.existsSync(path7.join(APPIUM_DIR, "package.json"))) {
8962
+ fs52.writeFileSync(path7.join(APPIUM_DIR, "package.json"), JSON.stringify({ name: "flowriki-appium", private: true }, null, 2));
8887
8963
  }
8888
- const env = { ...await toolEnv(), APPIUM_HOME: path6.join(APPIUM_DIR, "home") };
8964
+ const env = { ...await toolEnv(), APPIUM_HOME: path7.join(APPIUM_DIR, "home") };
8889
8965
  say("Installing Appium\u2026");
8890
8966
  await stream(npm, ["install", "--no-audit", "--no-fund", "appium@2"], env, APPIUM_DIR, say);
8891
- const bin = path6.join(APPIUM_DIR, "node_modules", ".bin", process.platform === "win32" ? "appium.cmd" : "appium");
8967
+ const bin = path7.join(APPIUM_DIR, "node_modules", ".bin", process.platform === "win32" ? "appium.cmd" : "appium");
8892
8968
  const have = await installedDrivers();
8893
8969
  const wanted = ["uiautomator2", ...process.platform === "darwin" ? ["xcuitest"] : []];
8894
8970
  for (const driver of wanted) {
@@ -8908,6 +8984,8 @@ var MobileLane = class {
8908
8984
  }
8909
8985
  opts;
8910
8986
  sessions = /* @__PURE__ */ new Map();
8987
+ filming = /* @__PURE__ */ new Set();
8988
+ failed = false;
8911
8989
  values;
8912
8990
  /** Run one phone row. `deviceKey` is the row's device — its actor id. */
8913
8991
  async run(occurrence, ctx) {
@@ -8927,6 +9005,7 @@ var MobileLane = class {
8927
9005
  ...this.opts.resolveSecret ? { resolveSecret: this.opts.resolveSecret } : {}
8928
9006
  });
8929
9007
  this.sessions.set(ctx.deviceKey, session);
9008
+ if (filmsThis(this.opts.video, this.opts.allSteps ?? occurrence.steps, this.opts.evidenceDir) && await session.startVideo()) this.filming.add(ctx.deviceKey);
8930
9009
  } else if (start2 === "fresh" || start2 === "navigate") {
8931
9010
  await session.terminateApp().catch(() => void 0);
8932
9011
  await session.launchApp();
@@ -8949,12 +9028,23 @@ var MobileLane = class {
8949
9028
  ...this.opts.screenshots ? { screenshots: this.opts.screenshots } : {},
8950
9029
  ...this.opts.signal ? { signal: this.opts.signal } : {}
8951
9030
  });
9031
+ if (done.status !== "passed") this.failed = true;
8952
9032
  return { status: done.status, error: done.error, steps: done.results };
8953
9033
  }
8954
- async close() {
8955
- const all = [...this.sessions.values()];
9034
+ /** Close every phone; answers the films kept. `passed` is the whole
9035
+ * case's result — a film is about the case, not one row. */
9036
+ async close(passed = !this.failed) {
9037
+ const all = [...this.sessions.entries()];
8956
9038
  this.sessions.clear();
8957
- await Promise.all(all.map((s2) => s2.close().catch(() => void 0)));
9039
+ const films = [];
9040
+ let n2 = 0;
9041
+ for (const [key, session] of all) {
9042
+ n2++;
9043
+ if (this.filming.has(key)) films.push(...await finishVideo(session, this.opts.video, this.opts.evidenceDir, passed, `phone-${n2}`));
9044
+ await session.close().catch(() => void 0);
9045
+ }
9046
+ this.filming.clear();
9047
+ return films;
8958
9048
  }
8959
9049
  };
8960
9050
  async function replayMobileCase(opts) {
@@ -8964,6 +9054,7 @@ async function replayMobileCase(opts) {
8964
9054
  const opened = /* @__PURE__ */ new Set();
8965
9055
  let status2 = "passed";
8966
9056
  let error = null;
9057
+ let artifacts = [];
8967
9058
  try {
8968
9059
  for (const occurrence of opts.occurrences) {
8969
9060
  if (status2 !== "passed" || opts.signal?.aborted) {
@@ -9004,13 +9095,13 @@ async function replayMobileCase(opts) {
9004
9095
  }
9005
9096
  }
9006
9097
  } finally {
9007
- await lane.close();
9098
+ artifacts = await lane.close(status2 === "passed");
9008
9099
  }
9009
9100
  const steps = outcomes.flatMap((o) => o.steps);
9010
9101
  return {
9011
9102
  status: status2,
9012
9103
  error,
9013
- artifacts: [],
9104
+ artifacts,
9014
9105
  durationMs: Date.now() - started,
9015
9106
  occurrences: outcomes,
9016
9107
  steps,
@@ -9187,15 +9278,15 @@ var RunDetailSchema = RunSummarySchema.extend({
9187
9278
 
9188
9279
  // ../results/dist/chunk-IW3PJWP6.js
9189
9280
  import os5 from "os";
9190
- import path7 from "path";
9281
+ import path8 from "path";
9191
9282
  function flowrikiHome() {
9192
- return process.env.FLOWRIKI_HOME || path7.join(os5.homedir(), ".flowriki");
9283
+ return process.env.FLOWRIKI_HOME || path8.join(os5.homedir(), ".flowriki");
9193
9284
  }
9194
9285
  function defaultDbPath() {
9195
- return path7.join(flowrikiHome(), "flowriki.db");
9286
+ return path8.join(flowrikiHome(), "flowriki.db");
9196
9287
  }
9197
9288
  function runArtifactsDir(runId, projectId = "default") {
9198
- return path7.join(flowrikiHome(), "projects", safe(projectId), "runs", safe(runId));
9289
+ return path8.join(flowrikiHome(), "projects", safe(projectId), "runs", safe(runId));
9199
9290
  }
9200
9291
  function safe(segment) {
9201
9292
  if (!/^[A-Za-z0-9][A-Za-z0-9_.-]*$/.test(segment) || segment.includes("..")) {
@@ -9204,15 +9295,15 @@ function safe(segment) {
9204
9295
  return segment;
9205
9296
  }
9206
9297
  function isInside(root, candidate) {
9207
- const base = path7.resolve(root);
9208
- const target = path7.resolve(candidate);
9209
- return target === base || target.startsWith(base + path7.sep);
9298
+ const base = path8.resolve(root);
9299
+ const target = path8.resolve(candidate);
9300
+ return target === base || target.startsWith(base + path8.sep);
9210
9301
  }
9211
9302
 
9212
9303
  // ../results/dist/node.js
9213
9304
  import fs6 from "fs";
9214
9305
  import { createRequire as createRequire3 } from "module";
9215
- import path8 from "path";
9306
+ import path9 from "path";
9216
9307
  var MIGRATIONS2 = [
9217
9308
  // 1 — initial
9218
9309
  `
@@ -9424,7 +9515,7 @@ var SqliteStorage = class {
9424
9515
  `run history needs node:sqlite, which this Node (${process.version}) does not provide \u2014 Node 22.13 or newer is required (${err.message})`
9425
9516
  );
9426
9517
  }
9427
- if (this.file !== ":memory:") fs6.mkdirSync(path8.dirname(this.file), { recursive: true });
9518
+ if (this.file !== ":memory:") fs6.mkdirSync(path9.dirname(this.file), { recursive: true });
9428
9519
  const db = new DatabaseSync2(this.file);
9429
9520
  if (this.file !== ":memory:") db.exec("PRAGMA journal_mode = WAL");
9430
9521
  db.exec("PRAGMA busy_timeout = 5000");
@@ -9977,7 +10068,7 @@ var FlowNotRunnable = class extends Error {
9977
10068
  import fs24 from "fs";
9978
10069
  import path24 from "path";
9979
10070
  import fs7 from "fs";
9980
- import path9 from "path";
10071
+ import path10 from "path";
9981
10072
  import { execFileSync } from "child_process";
9982
10073
  import { execFileSync as execFileSync2 } from "child_process";
9983
10074
  import fs34 from "fs";
@@ -10002,7 +10093,7 @@ var LocalSecrets = class {
10002
10093
  }
10003
10094
  root;
10004
10095
  get secretsPath() {
10005
- return path9.join(this.root, ".flowriki", "secrets.local.json");
10096
+ return path10.join(this.root, ".flowriki", "secrets.local.json");
10006
10097
  }
10007
10098
  readSecrets() {
10008
10099
  try {
@@ -10052,7 +10143,7 @@ var LocalSecrets = class {
10052
10143
  changed = true;
10053
10144
  }
10054
10145
  if (changed) {
10055
- fs7.mkdirSync(path9.dirname(this.secretsPath), { recursive: true });
10146
+ fs7.mkdirSync(path10.dirname(this.secretsPath), { recursive: true });
10056
10147
  fs7.writeFileSync(this.secretsPath, JSON.stringify(secrets, null, 2) + "\n", { mode: 384 });
10057
10148
  this.ensureSecretsIgnored();
10058
10149
  }
@@ -10083,7 +10174,7 @@ var LocalSecrets = class {
10083
10174
  if (!/^[a-z0-9][a-z0-9_-]*$/.test(ref)) throw new Error(`invalid secret name: ${ref}`);
10084
10175
  const secrets = this.readSecrets();
10085
10176
  secrets[ref] = value;
10086
- fs7.mkdirSync(path9.dirname(this.secretsPath), { recursive: true });
10177
+ fs7.mkdirSync(path10.dirname(this.secretsPath), { recursive: true });
10087
10178
  fs7.writeFileSync(this.secretsPath, JSON.stringify(secrets, null, 2) + "\n", { mode: 384 });
10088
10179
  this.ensureSecretsIgnored();
10089
10180
  }
@@ -10124,7 +10215,7 @@ var LocalSecrets = class {
10124
10215
  }
10125
10216
  }
10126
10217
  if (healed.length) {
10127
- fs7.mkdirSync(path9.dirname(this.secretsPath), { recursive: true });
10218
+ fs7.mkdirSync(path10.dirname(this.secretsPath), { recursive: true });
10128
10219
  fs7.writeFileSync(this.secretsPath, JSON.stringify(secrets, null, 2) + "\n", { mode: 384 });
10129
10220
  this.ensureSecretsIgnored();
10130
10221
  }
@@ -10141,10 +10232,10 @@ var LocalSecrets = class {
10141
10232
  }
10142
10233
  /** Add one entry to .flowriki/.gitignore if it is not already there. */
10143
10234
  ensureIgnored(line, why) {
10144
- const ignore = path9.join(this.root, ".flowriki", ".gitignore");
10235
+ const ignore = path10.join(this.root, ".flowriki", ".gitignore");
10145
10236
  const existing = fs7.existsSync(ignore) ? fs7.readFileSync(ignore, "utf8") : "";
10146
10237
  if (existing.split("\n").includes(line)) return;
10147
- fs7.mkdirSync(path9.dirname(ignore), { recursive: true });
10238
+ fs7.mkdirSync(path10.dirname(ignore), { recursive: true });
10148
10239
  fs7.writeFileSync(
10149
10240
  ignore,
10150
10241
  `${existing}${existing && !existing.endsWith("\n") ? "\n" : ""}# ${why}
@@ -10804,9 +10895,9 @@ async function runSms(config, ctx) {
10804
10895
  await new Promise((r) => setTimeout(r, ctx.pollMs ?? 3e3));
10805
10896
  }
10806
10897
  }
10807
- function atPath(value, path14) {
10898
+ function atPath(value, path15) {
10808
10899
  let at = value;
10809
- for (const key of path14.split(".").filter(Boolean)) {
10900
+ for (const key of path15.split(".").filter(Boolean)) {
10810
10901
  if (at === null || at === void 0) return void 0;
10811
10902
  at = Array.isArray(at) && /^\d+$/.test(key) ? at[Number(key)] : at[key];
10812
10903
  }
@@ -10943,14 +11034,15 @@ async function executeFlow(opts) {
10943
11034
  onStepResult: o.onStepResult,
10944
11035
  evidenceDir: o.evidenceDir,
10945
11036
  screenshots: o.artifacts?.screenshot ?? "on-failure",
10946
- signal: o.signal
11037
+ signal: o.signal,
11038
+ video: { mode: o.artifacts?.video ?? "off", allowUnmasked: o.artifacts?.allowUnmaskedArtifacts === true }
10947
11039
  }) : replayFlow(o);
10948
11040
  const byId = new Map(flow.steps.map((s2) => [s2.id, s2]));
10949
11041
  const attempts = [];
10950
11042
  const perStep = /* @__PURE__ */ new Map();
10951
11043
  for (let attempt = 1; attempt <= repeat; attempt++) {
10952
11044
  if (opts.signal?.aborted) break;
10953
- const evidenceDir = runDir && repeat > 1 ? path10.join(runDir, `attempt-${String(attempt).padStart(3, "0")}`) : runDir;
11045
+ const evidenceDir = runDir && repeat > 1 ? path11.join(runDir, `attempt-${String(attempt).padStart(3, "0")}`) : runDir;
10954
11046
  const relativeTo = (file2) => repeat > 1 ? `attempt-${String(attempt).padStart(3, "0")}/${file2}` : file2;
10955
11047
  const taskId = storage && run2 ? await storage.createTask(run2.id, {
10956
11048
  caseName: repeat > 1 ? `${flow.name} \xB7 attempt ${attempt}` : flow.name,
@@ -11137,7 +11229,7 @@ function report(attempts, perStep) {
11137
11229
  function sizeOf(dir, file2) {
11138
11230
  if (!dir) return null;
11139
11231
  try {
11140
- return fs8.statSync(path10.join(dir, file2)).size;
11232
+ return fs8.statSync(path11.join(dir, file2)).size;
11141
11233
  } catch {
11142
11234
  return null;
11143
11235
  }
@@ -11303,7 +11395,9 @@ async function executeCase(opts) {
11303
11395
  ...helpers ? { runHelper: helpers } : {},
11304
11396
  ...runDir ? { evidenceDir: runDir } : {},
11305
11397
  ...config.artifacts?.screenshot ? { screenshots: config.artifacts.screenshot } : {},
11306
- ...opts.signal ? { signal: opts.signal } : {}
11398
+ ...opts.signal ? { signal: opts.signal } : {},
11399
+ video: { mode: config.artifacts?.video ?? "off", allowUnmasked: config.artifacts?.allowUnmaskedArtifacts === true },
11400
+ allSteps: phoneRows.flatMap((o) => o.steps)
11307
11401
  };
11308
11402
  const lane = phoneRows.length && phoneRows.length < testCase.occurrences.length ? new MobileLane(laneOptions) : null;
11309
11403
  const opened = /* @__PURE__ */ new Set();
@@ -11367,7 +11461,7 @@ async function executeCase(opts) {
11367
11461
  }))
11368
11462
  };
11369
11463
  }
11370
- await lane?.close();
11464
+ if (lane) outcome = { ...outcome, artifacts: [...outcome.artifacts ?? [], ...await lane.close(outcome.status === "passed")] };
11371
11465
  await Promise.all(writes);
11372
11466
  if (storage && run2) {
11373
11467
  for (const each of outcome.occurrences) {
@@ -11961,7 +12055,7 @@ function resolveCheckouts(dirs) {
11961
12055
  const projects = [];
11962
12056
  const skipped = [];
11963
12057
  for (const dir of dirs) {
11964
- const root = path11.resolve(dir);
12058
+ const root = path12.resolve(dir);
11965
12059
  if (!fs9.existsSync(root)) {
11966
12060
  skipped.push({ root, why: "no such folder" });
11967
12061
  continue;
@@ -12080,7 +12174,7 @@ function dialWebSocket(opts) {
12080
12174
 
12081
12175
  // ../agent-core/dist/index.js
12082
12176
  import fs10 from "fs";
12083
- import path12 from "path";
12177
+ import path13 from "path";
12084
12178
  import crypto4 from "crypto";
12085
12179
  import { createRequire as createRequire4 } from "module";
12086
12180
  var EventJournal = class {
@@ -12173,7 +12267,7 @@ var AgentState = class {
12173
12267
  this.commandLimit = commandLimit;
12174
12268
  if (!file2 || file2 === ":memory:") throw new Error("a persistent agent state file is required");
12175
12269
  if (!Number.isInteger(commandLimit) || commandLimit < 1) throw new Error("invalid command capacity");
12176
- fs10.mkdirSync(path12.dirname(file2), { recursive: true, mode: 448 });
12270
+ fs10.mkdirSync(path13.dirname(file2), { recursive: true, mode: 448 });
12177
12271
  const fd = fs10.openSync(file2, "a", 384);
12178
12272
  fs10.closeSync(fd);
12179
12273
  fs10.chmodSync(file2, 384);
@@ -12592,7 +12686,7 @@ var AgentLink = class {
12592
12686
  // ../provider-local/dist/index.js
12593
12687
  import fs11 from "fs";
12594
12688
  import os6 from "os";
12595
- import path13 from "path";
12689
+ import path14 from "path";
12596
12690
  import fs26 from "fs";
12597
12691
  import path25 from "path";
12598
12692
  import { randomUUID as randomUUID2 } from "crypto";
@@ -12613,7 +12707,7 @@ var scratchRoots = /* @__PURE__ */ new Map();
12613
12707
  function scratchRoot(projectId) {
12614
12708
  let root = scratchRoots.get(projectId);
12615
12709
  if (!root) {
12616
- root = fs11.mkdtempSync(path13.join(os6.tmpdir(), `flowriki-record-${projectId.replace(/[^\w-]/g, "")}-`));
12710
+ root = fs11.mkdtempSync(path14.join(os6.tmpdir(), `flowriki-record-${projectId.replace(/[^\w-]/g, "")}-`));
12617
12711
  scratchRoots.set(projectId, root);
12618
12712
  }
12619
12713
  return root;
@@ -14397,7 +14491,10 @@ export {
14397
14491
  AGENT_VERSION,
14398
14492
  prepareBrowsers,
14399
14493
  browsersReady,
14494
+ dataDir,
14400
14495
  connectionFor,
14496
+ AGENT_LATEST_URL,
14497
+ agentUpdateFor,
14401
14498
  resolveService,
14402
14499
  connect,
14403
14500
  isMobile,
package/dist/cli.js CHANGED
@@ -1,16 +1,19 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ AGENT_LATEST_URL,
3
4
  AGENT_VERSION,
4
5
  CLOUD_PROVIDERS,
5
6
  CLOUD_PROVIDER_NAME,
6
7
  MobileLane,
7
8
  RecordedStepSchema,
9
+ agentUpdateFor,
8
10
  authenticationResolver,
9
11
  checkAssertion,
10
12
  checkMobileAssertion,
11
13
  closeRunSession,
12
14
  connect,
13
15
  connectionFor,
16
+ dataDir,
14
17
  disconnect,
15
18
  doctor,
16
19
  executeStep,
@@ -41,7 +44,7 @@ import {
41
44
  status,
42
45
  stepRefusedOn,
43
46
  targetFor
44
- } from "./chunk-LIURHWRP.js";
47
+ } from "./chunk-AMXSHZYE.js";
45
48
 
46
49
  // src/cli.ts
47
50
  import { parseArgs } from "util";
@@ -137,8 +140,8 @@ var ServiceClient = class {
137
140
  get origin() {
138
141
  return this.connection.service;
139
142
  }
140
- async call(path, init = {}) {
141
- const res = await this.fetcher(`${this.connection.service}${path}`, {
143
+ async call(path2, init = {}) {
144
+ const res = await this.fetcher(`${this.connection.service}${path2}`, {
142
145
  ...init,
143
146
  headers: {
144
147
  "x-flowriki-device": this.connection.deviceId,
@@ -280,9 +283,9 @@ var ServiceClient = class {
280
283
  }
281
284
  // ── Manual cycles and bugs: the dashboard's own routes, behind this
282
285
  // machine's credential, so the assistant can do what a person can. ──────
283
- scoped(path, projectId, extra = {}) {
286
+ scoped(path2, projectId, extra = {}) {
284
287
  const query = new URLSearchParams({ projectId, ...extra });
285
- return `/api/agent${path}?${query}`;
288
+ return `/api/agent${path2}?${query}`;
286
289
  }
287
290
  cycles(projectId) {
288
291
  return this.call(this.scoped("/cycles", projectId));
@@ -321,8 +324,8 @@ var ServiceClient = class {
321
324
  knowledgePages(projectId) {
322
325
  return this.call(this.scoped("/knowledge", projectId));
323
326
  }
324
- knowledgePage(projectId, path) {
325
- return this.call(this.scoped("/knowledge/page", projectId, { path }));
327
+ knowledgePage(projectId, path2) {
328
+ return this.call(this.scoped("/knowledge/page", projectId, { path: path2 }));
326
329
  }
327
330
  writeKnowledge(projectId, body) {
328
331
  return this.call(`/api/agent/knowledge?projectId=${encodeURIComponent(projectId)}`, { method: "POST", body: JSON.stringify(body) });
@@ -2003,7 +2006,7 @@ Use it with start_flow signIn {"profileId": "\u2026", "accountId": "${made.accou
2003
2006
  run: async (args) => {
2004
2007
  const projectId = await projectFor(args["projectId"]);
2005
2008
  const { pages } = await service.knowledgePages(projectId);
2006
- const read = async (path) => pages.some((p) => p.path === path) ? (await service.knowledgePage(projectId, path)).content : null;
2009
+ const read = async (path2) => pages.some((p) => p.path === path2) ? (await service.knowledgePage(projectId, path2)).content : null;
2007
2010
  const [product, glossary, decisions] = await Promise.all(["product.md", "glossary.md", "memory/decisions.md"].map(read));
2008
2011
  const codebases = pages.filter((p) => p.path.startsWith("codebase/"));
2009
2012
  const headers = await Promise.all(codebases.map(async (p) => `- ${p.path}: ${(await read(p.path))?.split("\n").filter((l) => l.trim()).slice(0, 3).join(" \xB7 ") ?? ""}`));
@@ -2097,6 +2100,212 @@ ${pages.filter((p) => !shown.has(p.path)).map((p) => `- ${p.path}`).join("\n") |
2097
2100
  }));
2098
2101
  }
2099
2102
 
2103
+ // src/mcp/upgrade.ts
2104
+ import { spawn } from "child_process";
2105
+ import readline2 from "readline";
2106
+ var PRIVATE = "flowriki-upgrade";
2107
+ var startWithNpx = (version) => spawn(process.platform === "win32" ? "npx.cmd" : "npx", ["-y", `@flowriki/agent@${version}`, "mcp"], {
2108
+ env: { ...process.env, FLOWRIKI_MCP_CHILD: "1" },
2109
+ stdio: ["pipe", "pipe", "inherit"]
2110
+ });
2111
+ var toolShape = (tools) => JSON.stringify(tools.map((t) => [t.name, t.description ?? "", t.inputSchema ?? null]).sort());
2112
+ function serveWithUpgrades(tools, info, opts) {
2113
+ const input = opts.input ?? process.stdin;
2114
+ const output = opts.output ?? process.stdout;
2115
+ const log = opts.log ?? ((line) => process.stderr.write(`flowriki: ${line}
2116
+ `));
2117
+ const send = (m) => output.write(`${JSON.stringify(m)}
2118
+ `);
2119
+ if (!opts.output) {
2120
+ console.log = console.info = console.warn = console.debug = (...args) => {
2121
+ process.stderr.write(`${args.map((a) => typeof a === "string" ? a : JSON.stringify(a)).join(" ")}
2122
+ `);
2123
+ };
2124
+ }
2125
+ let initialize = null;
2126
+ let localCalls = 0;
2127
+ let ready = null;
2128
+ let active = null;
2129
+ let preparing = false;
2130
+ let notice = null;
2131
+ const forwarded = /* @__PURE__ */ new Set();
2132
+ let timer = null;
2133
+ const withNotice = (answer) => {
2134
+ if (!notice || !answer.result || typeof answer.result !== "object") return answer;
2135
+ const result = answer.result;
2136
+ if (!Array.isArray(result.content)) return answer;
2137
+ const text2 = notice;
2138
+ notice = null;
2139
+ return { ...answer, result: { ...result, content: [{ type: "text", text: text2 }, ...result.content] } };
2140
+ };
2141
+ const prepare = async (version) => {
2142
+ if (preparing || ready || active || !initialize) return;
2143
+ preparing = true;
2144
+ let child = null;
2145
+ try {
2146
+ child = (opts.start ?? startWithNpx)(version);
2147
+ const c = child;
2148
+ const lines2 = readline2.createInterface({ input: c.stdout, crlfDelay: Infinity });
2149
+ const waiting = /* @__PURE__ */ new Map();
2150
+ const exited = new Promise((_, reject) => c.once("exit", (code) => reject(new Error(`it stopped while starting (exit ${code})`))));
2151
+ exited.catch(() => void 0);
2152
+ c.once("error", (e) => waiting.forEach((resolve) => resolve({ jsonrpc: "2.0", error: { message: e.message } })));
2153
+ lines2.on("line", (line) => {
2154
+ let m;
2155
+ try {
2156
+ m = JSON.parse(line);
2157
+ } catch {
2158
+ return;
2159
+ }
2160
+ if (typeof m.id === "string" && m.id.startsWith(PRIVATE)) {
2161
+ waiting.get(m.id)?.(m);
2162
+ waiting.delete(m.id);
2163
+ return;
2164
+ }
2165
+ if (active?.child !== c) return;
2166
+ if (m.id !== void 0 && m.id !== null) forwarded.delete(m.id);
2167
+ send(m.id !== void 0 && m.id !== null && "result" in m ? withNotice(m) : m);
2168
+ });
2169
+ const ask = (method, params, id) => new Promise((resolve, reject) => {
2170
+ waiting.set(id, resolve);
2171
+ c.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", id, method, ...params ? { params } : {} })}
2172
+ `);
2173
+ setTimeout(() => reject(new Error("it did not answer in time")), opts.readyWithinMs ?? 18e4).unref();
2174
+ });
2175
+ const hello = await Promise.race([ask("initialize", initialize.params, `${PRIVATE}-init`), exited]);
2176
+ if (hello.error) throw new Error(JSON.stringify(hello.error));
2177
+ c.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" })}
2178
+ `);
2179
+ const listed = await Promise.race([ask("tools/list", void 0, `${PRIVATE}-tools`), exited]);
2180
+ const theirs = listed.result?.tools ?? [];
2181
+ ready = { child: c, version, lines: lines2, toolsChanged: toolShape(theirs) !== toolShape(tools) };
2182
+ c.once("exit", () => {
2183
+ if (ready?.child === c) ready = null;
2184
+ if (active?.child === c) {
2185
+ active = null;
2186
+ for (const id of forwarded) send({ jsonrpc: "2.0", id, error: { code: -32603, message: "the updated Flowriki stopped \u2014 ask again" } });
2187
+ forwarded.clear();
2188
+ notice = `The updated Flowriki (${version}) stopped, so this connection carries on with ${opts.current}.`;
2189
+ log(`updated server ${version} exited; back on ${opts.current}`);
2190
+ }
2191
+ });
2192
+ log(`${version} is ready to take over from ${opts.current}`);
2193
+ } catch (error) {
2194
+ child?.kill();
2195
+ log(`could not start ${version}: ${error.message}`);
2196
+ notice ??= `Flowriki ${version} is out (this connection runs ${opts.current}) and could not switch over by itself: ${error.message}. Reconnect Flowriki in Claude to start the new version.`;
2197
+ } finally {
2198
+ preparing = false;
2199
+ }
2200
+ };
2201
+ const maybeSwitch = async (incomingTool) => {
2202
+ if (!ready || active || localCalls > 0) return false;
2203
+ if (!opts.idle() && incomingTool !== "start_flow") return false;
2204
+ const next = ready;
2205
+ ready = null;
2206
+ await opts.handOff().catch(() => void 0);
2207
+ active = next;
2208
+ if (timer) clearInterval(timer);
2209
+ notice = `Flowriki updated itself from ${opts.current} to ${next.version} \u2014 the new version is answering now.` + (next.toolsChanged ? " It brings new or changed tools; if Claude does not offer them yet, reconnect Flowriki in Claude (in Claude Code: /mcp, then Reconnect)." : "");
2210
+ send({ jsonrpc: "2.0", method: "notifications/tools/list_changed" });
2211
+ log(`switched to ${next.version}`);
2212
+ return true;
2213
+ };
2214
+ const look = async () => {
2215
+ if (active || ready || preparing) return;
2216
+ const version = await opts.check().catch(() => null);
2217
+ if (version) {
2218
+ await prepare(version);
2219
+ await maybeSwitch();
2220
+ }
2221
+ };
2222
+ setTimeout(() => void look(), opts.firstCheckMs ?? 15e3).unref();
2223
+ timer = setInterval(() => void look(), opts.everyMs ?? 30 * 6e4);
2224
+ timer.unref();
2225
+ const lines = readline2.createInterface({ input, crlfDelay: Infinity });
2226
+ const pending = /* @__PURE__ */ new Set();
2227
+ lines.on("line", (line) => {
2228
+ if (!line.trim()) return;
2229
+ let message;
2230
+ try {
2231
+ message = JSON.parse(line);
2232
+ } catch {
2233
+ send({ jsonrpc: "2.0", id: null, error: { code: -32700, message: "parse error" } });
2234
+ return;
2235
+ }
2236
+ if (message.method === "initialize") initialize = message;
2237
+ const task = (async () => {
2238
+ const tool = message.method === "tools/call" ? String(message.params?.["name"] ?? "") : void 0;
2239
+ if (!active && tool) await maybeSwitch(tool);
2240
+ if (active) {
2241
+ if (message.id !== void 0 && message.id !== null) forwarded.add(message.id);
2242
+ active.child.stdin.write(`${line}
2243
+ `);
2244
+ return;
2245
+ }
2246
+ if (tool) localCalls++;
2247
+ try {
2248
+ const answer = await handle(message, tools, info);
2249
+ if (answer) send(tool ? withNotice(answer) : answer);
2250
+ if (message.method === "notifications/initialized") send({ jsonrpc: "2.0", method: "notifications/tools/list_changed" });
2251
+ } finally {
2252
+ if (tool) localCalls--;
2253
+ }
2254
+ if (tool && ready) await maybeSwitch();
2255
+ })();
2256
+ pending.add(task);
2257
+ void task.finally(() => pending.delete(task));
2258
+ });
2259
+ return new Promise((resolve) => lines.on("close", () => {
2260
+ void Promise.allSettled([...pending]).then(() => {
2261
+ if (timer) clearInterval(timer);
2262
+ const child = active?.child ?? ready?.child;
2263
+ if (!child) return resolve();
2264
+ child.stdin?.end();
2265
+ const done = () => resolve();
2266
+ child.once("exit", done);
2267
+ setTimeout(() => {
2268
+ child.kill();
2269
+ done();
2270
+ }, 5e3).unref();
2271
+ });
2272
+ }));
2273
+ }
2274
+
2275
+ // src/updates.ts
2276
+ import fs from "fs";
2277
+ import path from "path";
2278
+ var CACHE_MS = 30 * 60 * 1e3;
2279
+ async function latestPublished(opts = {}) {
2280
+ const file = path.join(dataDir(), "latest-agent.json");
2281
+ if (!opts.fresh) {
2282
+ try {
2283
+ const kept = JSON.parse(fs.readFileSync(file, "utf8"));
2284
+ if (kept.version && kept.at && Date.now() - kept.at < CACHE_MS) return kept.version;
2285
+ } catch {
2286
+ }
2287
+ }
2288
+ try {
2289
+ const res = await fetch(AGENT_LATEST_URL, { signal: AbortSignal.timeout(5e3), headers: { accept: "application/json" } });
2290
+ if (!res.ok) return null;
2291
+ const version = (await res.json()).version ?? null;
2292
+ if (version) {
2293
+ try {
2294
+ fs.mkdirSync(path.dirname(file), { recursive: true });
2295
+ fs.writeFileSync(file, JSON.stringify({ version, at: Date.now() }));
2296
+ } catch {
2297
+ }
2298
+ }
2299
+ return version;
2300
+ } catch {
2301
+ return null;
2302
+ }
2303
+ }
2304
+ async function updateFor(running) {
2305
+ const latest = await latestPublished();
2306
+ return agentUpdateFor(running, latest) ? latest : null;
2307
+ }
2308
+
2100
2309
  // src/mcp/index.ts
2101
2310
  async function runMcp(opts) {
2102
2311
  let service;
@@ -2120,7 +2329,20 @@ async function runMcp(opts) {
2120
2329
  };
2121
2330
  process.on("SIGINT", () => void closing());
2122
2331
  process.on("SIGTERM", () => void closing());
2123
- await serve(tools, { name: "flowriki", version: opts.version, instructions: INSTRUCTIONS });
2332
+ const info = { name: "flowriki", version: opts.version, instructions: INSTRUCTIONS };
2333
+ if (process.env["FLOWRIKI_MCP_CHILD"] === "1" || process.env["FLOWRIKI_NO_UPDATE"] === "1" || !/^\d+\.\d+\.\d+/.test(opts.version)) {
2334
+ await serve(tools, info);
2335
+ } else {
2336
+ await serveWithUpgrades(tools, info, {
2337
+ current: opts.version,
2338
+ check: () => updateFor(opts.version),
2339
+ idle: () => !session?.open,
2340
+ handOff: async () => {
2341
+ await session?.close();
2342
+ if (session) session.draft = null;
2343
+ }
2344
+ });
2345
+ }
2124
2346
  await session?.close();
2125
2347
  return 0;
2126
2348
  }
@@ -2270,6 +2492,17 @@ Run \`flowriki-agent --help\`.`);
2270
2492
  console.log(dim(` project ${project.name} (${project.root})`));
2271
2493
  }
2272
2494
  console.log(dim("\n Running in the foreground. Ctrl-C to stop.\n"));
2495
+ let told = null;
2496
+ const tellIfBehind = async () => {
2497
+ const newer = await updateFor(AGENT_VERSION).catch(() => null);
2498
+ if (!newer || newer === told) return;
2499
+ told = newer;
2500
+ console.log(yellow(` ! Flowriki agent ${newer} is out \u2014 this is ${AGENT_VERSION}.`));
2501
+ console.log(dim(` Update: npm install -g @flowriki/agent@latest then stop (Ctrl-C) and start again
2502
+ `));
2503
+ };
2504
+ void tellIfBehind();
2505
+ setInterval(() => void tellIfBehind(), 6 * 60 * 6e4).unref();
2273
2506
  let stopping = false;
2274
2507
  const shutdown = async (signal) => {
2275
2508
  if (stopping) return;
@@ -2349,7 +2582,7 @@ Run \`flowriki-agent --help\`.`);
2349
2582
  for (const [title, lines] of [["Appium", found.appium], ["Android", found.android], ["iOS", found.ios]]) {
2350
2583
  console.log(bold(title));
2351
2584
  for (const line of lines) {
2352
- console.log(` ${line.ok ? green("\u2713") : red("\u2717")} ${line.item.padEnd(15)} ${line.ok ? dim(line.detail) : line.detail}`);
2585
+ console.log(` ${line.ok ? green("\u2713") : line.optional ? yellow("\u2013") : red("\u2717")} ${line.item.padEnd(15)} ${line.ok ? dim(line.detail) : line.detail}`);
2353
2586
  if (!line.ok && line.fix) console.log(` ${yellow("\u2192")} ${line.fix}`);
2354
2587
  }
2355
2588
  }
package/dist/engine.js CHANGED
@@ -9,7 +9,7 @@ import {
9
9
  resolveCheckouts,
10
10
  start,
11
11
  status
12
- } from "./chunk-LIURHWRP.js";
12
+ } from "./chunk-AMXSHZYE.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.46",
3
+ "version": "0.1.0-beta.47",
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",