@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 +8 -0
- package/dist/{chunk-LIURHWRP.js → chunk-AMXSHZYE.js} +155 -58
- package/dist/cli.js +243 -10
- package/dist/engine.js +1 -1
- package/package.json +1 -1
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.
|
|
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
|
|
508
|
+
import path12 from "path";
|
|
486
509
|
|
|
487
510
|
// ../execution/dist/index.js
|
|
488
511
|
import fs8 from "fs";
|
|
489
|
-
import
|
|
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
|
|
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
|
|
2651
|
-
const first =
|
|
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,
|
|
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(/\/+$/, "")}${
|
|
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,
|
|
7250
|
-
return this.req(method,
|
|
7276
|
+
raw(method, path82, body, timeoutMs) {
|
|
7277
|
+
return this.req(method, path82, body, timeoutMs);
|
|
7251
7278
|
}
|
|
7252
|
-
req(method,
|
|
7253
|
-
return call(this.endpoint, method, `/session/${this.id}${
|
|
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
|
-
|
|
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(`${
|
|
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(
|
|
8885
|
-
if (!fs52.existsSync(
|
|
8886
|
-
fs52.writeFileSync(
|
|
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:
|
|
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 =
|
|
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
|
-
|
|
8955
|
-
|
|
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
|
-
|
|
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
|
|
9281
|
+
import path8 from "path";
|
|
9191
9282
|
function flowrikiHome() {
|
|
9192
|
-
return process.env.FLOWRIKI_HOME ||
|
|
9283
|
+
return process.env.FLOWRIKI_HOME || path8.join(os5.homedir(), ".flowriki");
|
|
9193
9284
|
}
|
|
9194
9285
|
function defaultDbPath() {
|
|
9195
|
-
return
|
|
9286
|
+
return path8.join(flowrikiHome(), "flowriki.db");
|
|
9196
9287
|
}
|
|
9197
9288
|
function runArtifactsDir(runId, projectId = "default") {
|
|
9198
|
-
return
|
|
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 =
|
|
9208
|
-
const target =
|
|
9209
|
-
return target === base || target.startsWith(base +
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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 =
|
|
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(
|
|
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,
|
|
10898
|
+
function atPath(value, path15) {
|
|
10808
10899
|
let at = value;
|
|
10809
|
-
for (const key of
|
|
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 ?
|
|
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(
|
|
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
|
|
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 =
|
|
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
|
|
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(
|
|
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
|
|
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(
|
|
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-
|
|
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(
|
|
141
|
-
const res = await this.fetcher(`${this.connection.service}${
|
|
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(
|
|
286
|
+
scoped(path2, projectId, extra = {}) {
|
|
284
287
|
const query = new URLSearchParams({ projectId, ...extra });
|
|
285
|
-
return `/api/agent${
|
|
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,
|
|
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 (
|
|
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
|
-
|
|
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
package/package.json
CHANGED