@flowriki/agent 0.1.0-beta.63 → 0.1.0-beta.65
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.
|
@@ -66,9 +66,10 @@ import {
|
|
|
66
66
|
stepRefusedOn,
|
|
67
67
|
targetFor,
|
|
68
68
|
textIndependent,
|
|
69
|
+
usageKindOf,
|
|
69
70
|
validateActors,
|
|
70
71
|
webCloudOf
|
|
71
|
-
} from "./chunk-
|
|
72
|
+
} from "./chunk-PDUMGI6V.js";
|
|
72
73
|
|
|
73
74
|
// src/engine/machine.ts
|
|
74
75
|
import fs from "fs";
|
|
@@ -76,7 +77,7 @@ import os from "os";
|
|
|
76
77
|
import path from "path";
|
|
77
78
|
import { createRequire } from "module";
|
|
78
79
|
import { spawnSync } from "child_process";
|
|
79
|
-
var AGENT_VERSION = "0.1.0-beta.
|
|
80
|
+
var AGENT_VERSION = "0.1.0-beta.65";
|
|
80
81
|
async function prepareBrowsers() {
|
|
81
82
|
if (browsersReady()) {
|
|
82
83
|
return { ok: true, already: true, message: "chromium is already installed" };
|
|
@@ -136,7 +137,7 @@ async function checkMachine() {
|
|
|
136
137
|
}
|
|
137
138
|
} catch {
|
|
138
139
|
}
|
|
139
|
-
const { mobileDoctor: mobileDoctor2 } = await import("./dist-
|
|
140
|
+
const { mobileDoctor: mobileDoctor2 } = await import("./dist-3PMNCKSW.js");
|
|
140
141
|
return {
|
|
141
142
|
agentVersion: AGENT_VERSION,
|
|
142
143
|
node: process.versions.node,
|
|
@@ -551,7 +552,9 @@ async function connect(options = {}) {
|
|
|
551
552
|
const res = await fetch(`${service}/api/pairing-requests`, {
|
|
552
553
|
method: "POST",
|
|
553
554
|
headers: { "content-type": "application/json" },
|
|
554
|
-
|
|
555
|
+
// Ask for a code to show here: the page then insists on it, so a link
|
|
556
|
+
// sent by somebody else's agent cannot be approved by mistake.
|
|
557
|
+
body: JSON.stringify({ machineName, agentVersion: AGENT_VERSION, wantsCode: true }),
|
|
555
558
|
signal: options.signal ?? null
|
|
556
559
|
});
|
|
557
560
|
if (!res.ok) {
|
|
@@ -565,7 +568,7 @@ async function connect(options = {}) {
|
|
|
565
568
|
options.onProgress?.({
|
|
566
569
|
kind: "approve",
|
|
567
570
|
message: "waiting for approval in your browser",
|
|
568
|
-
detail: { url: approve, machineName, canOpen: options.open !== false && canOpenBrowser() }
|
|
571
|
+
detail: { url: approve, machineName, canOpen: options.open !== false && canOpenBrowser(), ...asked.code ? { code: `${asked.code.slice(0, 3)}-${asked.code.slice(3)}` } : {} }
|
|
569
572
|
});
|
|
570
573
|
if (options.open !== false) openBrowser(approve);
|
|
571
574
|
const pollMs = options.pollMs ?? 1500;
|
|
@@ -2130,6 +2133,12 @@ async function performHelperStep(page, step, options) {
|
|
|
2130
2133
|
}
|
|
2131
2134
|
return got;
|
|
2132
2135
|
}
|
|
2136
|
+
var LOAD_CAP_MS = 3e3;
|
|
2137
|
+
var QUIET_CAP_MS = 1e3;
|
|
2138
|
+
async function settleAfterNavigation(page) {
|
|
2139
|
+
await page.waitForLoadState("load", { timeout: LOAD_CAP_MS }).catch(() => void 0);
|
|
2140
|
+
await page.waitForLoadState("networkidle", { timeout: QUIET_CAP_MS }).catch(() => void 0);
|
|
2141
|
+
}
|
|
2133
2142
|
var secretsBehind = (auth) => [.../* @__PURE__ */ new Set([
|
|
2134
2143
|
...Object.values(auth.secretRefs ?? {}),
|
|
2135
2144
|
...auth.steps.flatMap((step) => step.secretRef ? [step.secretRef] : [])
|
|
@@ -2333,7 +2342,10 @@ var RunActors = class {
|
|
|
2333
2342
|
if (switching && this.opts.headless === false) await session.page.bringToFront().catch(() => {
|
|
2334
2343
|
});
|
|
2335
2344
|
if (this.initialized.has(id)) return;
|
|
2336
|
-
if (startUrl)
|
|
2345
|
+
if (startUrl) {
|
|
2346
|
+
await session.page.goto(startUrl, { waitUntil: "domcontentloaded" });
|
|
2347
|
+
await settleAfterNavigation(session.page);
|
|
2348
|
+
}
|
|
2337
2349
|
if (actor?.startSignedIn) {
|
|
2338
2350
|
if (kept?.state) {
|
|
2339
2351
|
const confirmed = await confirmSignedIn(session.page, kept.auth, this.opts.reuseCheckMs ?? 3e3);
|
|
@@ -2345,7 +2357,10 @@ var RunActors = class {
|
|
|
2345
2357
|
this.opts.reuse?.cache.evict(kept.key);
|
|
2346
2358
|
this.preparations.set(id, "stale");
|
|
2347
2359
|
session = await this.replaceWithGuest(id, session);
|
|
2348
|
-
|
|
2360
|
+
{
|
|
2361
|
+
await session.page.goto(startUrl, { waitUntil: "domcontentloaded" });
|
|
2362
|
+
await settleAfterNavigation(session.page);
|
|
2363
|
+
}
|
|
2349
2364
|
}
|
|
2350
2365
|
await executeStep(
|
|
2351
2366
|
session.page,
|
|
@@ -2377,7 +2392,10 @@ var RunActors = class {
|
|
|
2377
2392
|
} catch {
|
|
2378
2393
|
}
|
|
2379
2394
|
}
|
|
2380
|
-
if (startUrl)
|
|
2395
|
+
if (startUrl) {
|
|
2396
|
+
await session.page.goto(startUrl, { waitUntil: "domcontentloaded" });
|
|
2397
|
+
await settleAfterNavigation(session.page);
|
|
2398
|
+
}
|
|
2381
2399
|
}
|
|
2382
2400
|
this.initialized.add(id);
|
|
2383
2401
|
}
|
|
@@ -2474,7 +2492,10 @@ var RunActors = class {
|
|
|
2474
2492
|
this.retired.add(previous);
|
|
2475
2493
|
}
|
|
2476
2494
|
await this.opts.onCreate?.(session);
|
|
2477
|
-
|
|
2495
|
+
{
|
|
2496
|
+
await session.page.goto(this.opts.startUrl, { waitUntil: "domcontentloaded" });
|
|
2497
|
+
await settleAfterNavigation(session.page);
|
|
2498
|
+
}
|
|
2478
2499
|
this.initialized.add(id);
|
|
2479
2500
|
}
|
|
2480
2501
|
/** Back to one participant, between debug replays.
|
|
@@ -2805,7 +2826,7 @@ var ExecutionPages = class {
|
|
|
2805
2826
|
await Promise.all(this.dialogTasks);
|
|
2806
2827
|
if (this.unexpected) throw new Error(this.unexpected);
|
|
2807
2828
|
if (this.expectedDialogs.length) throw new Error("The expected browser dialog did not appear");
|
|
2808
|
-
if (step.downloads &&
|
|
2829
|
+
if (step.downloads && !downloadsMatch(step.downloads, downloads)) throw new Error(`Expected downloads ${step.downloads.join(", ")}; received ${downloads.join(", ") || "none"}`);
|
|
2809
2830
|
} finally {
|
|
2810
2831
|
page.off("download", onDownload);
|
|
2811
2832
|
this.expectedDialogs = [];
|
|
@@ -2820,6 +2841,17 @@ var ExecutionPages = class {
|
|
|
2820
2841
|
this.unexpected = null;
|
|
2821
2842
|
}
|
|
2822
2843
|
};
|
|
2844
|
+
var shapeOf = (name) => name.toLowerCase().replace(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/g, "#").replace(/(?<![a-z0-9])[0-9a-f]{12,}(?![a-z0-9])/g, "#").replace(/\d+/g, "#").replace(/#([^a-z]*#)+/g, "#");
|
|
2845
|
+
function downloadsMatch(expected, received) {
|
|
2846
|
+
if (expected.length !== received.length) return false;
|
|
2847
|
+
const left = received.map(shapeOf);
|
|
2848
|
+
for (const name of expected) {
|
|
2849
|
+
const at = left.indexOf(shapeOf(name));
|
|
2850
|
+
if (at === -1) return false;
|
|
2851
|
+
left.splice(at, 1);
|
|
2852
|
+
}
|
|
2853
|
+
return true;
|
|
2854
|
+
}
|
|
2823
2855
|
function rootFor(page, step) {
|
|
2824
2856
|
let root = page;
|
|
2825
2857
|
for (const frame of step.frame ?? []) root = root.frameLocator(frameSelector(frame));
|
|
@@ -2899,17 +2931,22 @@ async function scrollUntilFound(page, loc, budgetMs = 3e3) {
|
|
|
2899
2931
|
for (let pass = 0; pass < PASSES && Date.now() < giveUpAt; pass++) {
|
|
2900
2932
|
if (await loc.count() > 0) return true;
|
|
2901
2933
|
const moved = await page.evaluate(() => {
|
|
2902
|
-
const
|
|
2903
|
-
document.scrollingElement,
|
|
2904
|
-
...Array.from(document.querySelectorAll("*"))
|
|
2905
|
-
].filter((el) => !!el && el.scrollHeight > el.clientHeight + 4);
|
|
2906
|
-
let any = false;
|
|
2907
|
-
for (const el of scrollable) {
|
|
2934
|
+
const nudge = (el) => {
|
|
2908
2935
|
const before = el.scrollTop;
|
|
2909
2936
|
el.scrollTop = before + Math.max(80, el.clientHeight * 0.8);
|
|
2910
|
-
|
|
2911
|
-
}
|
|
2912
|
-
|
|
2937
|
+
return el.scrollTop !== before;
|
|
2938
|
+
};
|
|
2939
|
+
const page2 = document.scrollingElement;
|
|
2940
|
+
if (page2 && page2.scrollHeight > page2.clientHeight + 4 && nudge(page2)) return true;
|
|
2941
|
+
const areas = Array.from(document.querySelectorAll("*")).filter((el) => el.scrollHeight > el.clientHeight + 4 && el.clientHeight > 40).map((el) => {
|
|
2942
|
+
const box = el.getBoundingClientRect();
|
|
2943
|
+
const style = getComputedStyle(el);
|
|
2944
|
+
const scrolls = /(auto|scroll|overlay)/.test(style.overflowY);
|
|
2945
|
+
const shown = box.width > 0 && box.height > 0 && box.bottom > 0 && box.top < innerHeight && style.visibility !== "hidden";
|
|
2946
|
+
return { el, area: scrolls && shown ? box.width * box.height : 0 };
|
|
2947
|
+
}).filter((each) => each.area > 0).sort((a, b2) => b2.area - a.area);
|
|
2948
|
+
for (const { el } of areas) if (nudge(el)) return true;
|
|
2949
|
+
return false;
|
|
2913
2950
|
}).catch(() => false);
|
|
2914
2951
|
if (!moved) break;
|
|
2915
2952
|
await page.waitForTimeout(120);
|
|
@@ -3422,6 +3459,7 @@ async function runStepsIn(session, steps, opts) {
|
|
|
3422
3459
|
try {
|
|
3423
3460
|
if (opts.startUrl && !opensItsOwnEntry(steps, opts)) {
|
|
3424
3461
|
await page.goto(opts.startUrl, { waitUntil: "domcontentloaded" });
|
|
3462
|
+
await settleAfterNavigation(page);
|
|
3425
3463
|
}
|
|
3426
3464
|
for (const recorded of steps) {
|
|
3427
3465
|
let step = recorded;
|
|
@@ -3625,8 +3663,9 @@ async function runStepsIn(session, steps, opts) {
|
|
|
3625
3663
|
// SAID OUT LOUD, on a step that passed. The element was found, but
|
|
3626
3664
|
// not the way this step says it should be — something has moved,
|
|
3627
3665
|
// and the run is the only place anybody would learn it.
|
|
3628
|
-
onFallback: (_step, foundBy) => {
|
|
3666
|
+
onFallback: (_step, foundBy, renamedTo) => {
|
|
3629
3667
|
result.foundBy = foundBy;
|
|
3668
|
+
if (renamedTo) result.renamedTo = renamedTo;
|
|
3630
3669
|
}
|
|
3631
3670
|
});
|
|
3632
3671
|
try {
|
|
@@ -3815,6 +3854,7 @@ async function executeStep(main, input, options = {}) {
|
|
|
3815
3854
|
const remaining = () => Math.max(1, deadline - Date.now());
|
|
3816
3855
|
const navigationBudget = () => step.navigationTimeoutMs ?? options.navigationTimeoutMs ?? 45e3;
|
|
3817
3856
|
page.setDefaultTimeout(remaining());
|
|
3857
|
+
page.setDefaultNavigationTimeout(navigationBudget());
|
|
3818
3858
|
const act = async () => {
|
|
3819
3859
|
if (step.type === "unsupported") throw new Error(step.warning || step.value || "This interaction requires a supported action before replay");
|
|
3820
3860
|
if (step.type === "assert") {
|
|
@@ -3823,6 +3863,7 @@ async function executeStep(main, input, options = {}) {
|
|
|
3823
3863
|
}
|
|
3824
3864
|
if (step.type === "navigate") {
|
|
3825
3865
|
await page.goto(new URL(step.value, step.navigationBase === "application" ? options.baseUrl ?? page.url() : page.url()).href, { waitUntil: "domcontentloaded", timeout: navigationBudget() });
|
|
3866
|
+
await settleAfterNavigation(page);
|
|
3826
3867
|
return;
|
|
3827
3868
|
}
|
|
3828
3869
|
if (step.type === "closePage") {
|
|
@@ -3852,7 +3893,7 @@ async function executeStep(main, input, options = {}) {
|
|
|
3852
3893
|
}
|
|
3853
3894
|
const target = await ensureTarget(page, step, remaining());
|
|
3854
3895
|
const loc = target.loc;
|
|
3855
|
-
if (target.foundBy) options.onFallback?.(step, target.foundBy);
|
|
3896
|
+
if (target.foundBy) options.onFallback?.(step, target.foundBy, target.renamedTo);
|
|
3856
3897
|
page.setDefaultTimeout(remaining());
|
|
3857
3898
|
await applyAction(loc, step, value, { resolveFile: options.resolveFile, dropTarget: await locateDrop(page, step) });
|
|
3858
3899
|
};
|
|
@@ -3904,6 +3945,26 @@ async function applyScroll(page, step, timeout) {
|
|
|
3904
3945
|
} catch {
|
|
3905
3946
|
}
|
|
3906
3947
|
}
|
|
3948
|
+
async function renamedFrom(page, step, found) {
|
|
3949
|
+
if (!step.role || step.locator) return void 0;
|
|
3950
|
+
try {
|
|
3951
|
+
const first = (await found.ariaSnapshot({ timeout: 1e3 })).split("\n")[0] ?? "";
|
|
3952
|
+
const parsed = /^- ([a-z]+) "((?:[^"\\]|\\.)*)"/.exec(first);
|
|
3953
|
+
if (!parsed || parsed[1] !== step.role) return void 0;
|
|
3954
|
+
const name = JSON.parse(`"${parsed[2]}"`).trim();
|
|
3955
|
+
if (!name || name === step.name || name.length > 200) return void 0;
|
|
3956
|
+
const { nameFrom: _from, candidates: _backups, ...rest } = step;
|
|
3957
|
+
const [again] = await locateCandidates(page, { ...rest, name });
|
|
3958
|
+
if (!again || await again.count() !== 1) return void 0;
|
|
3959
|
+
const handle = await found.elementHandle({ timeout: 1e3 });
|
|
3960
|
+
const same = handle ? await again.evaluate((el, other) => el === other, handle) : false;
|
|
3961
|
+
await handle?.dispose().catch(() => {
|
|
3962
|
+
});
|
|
3963
|
+
return same ? name : void 0;
|
|
3964
|
+
} catch {
|
|
3965
|
+
return void 0;
|
|
3966
|
+
}
|
|
3967
|
+
}
|
|
3907
3968
|
async function ensureTarget(page, step, timeout) {
|
|
3908
3969
|
const candidates = await locateCandidates(page, step);
|
|
3909
3970
|
const described = describeLocators(step);
|
|
@@ -3920,7 +3981,10 @@ async function ensureTarget(page, step, timeout) {
|
|
|
3920
3981
|
let crowded = null;
|
|
3921
3982
|
for (let at = 1; at < candidates.length; at += 1) {
|
|
3922
3983
|
const found = await candidates[at].count().catch(() => 0);
|
|
3923
|
-
if (found === 1)
|
|
3984
|
+
if (found === 1) {
|
|
3985
|
+
const renamedTo = await renamedFrom(page, step, candidates[at]);
|
|
3986
|
+
return { loc: candidates[at], foundBy: described[at] ?? "another locator", ...renamedTo ? { renamedTo } : {} };
|
|
3987
|
+
}
|
|
3924
3988
|
if (found > 1 && !crowded) crowded = { where: described[at] ?? "another locator", count: found };
|
|
3925
3989
|
}
|
|
3926
3990
|
if (several) throw Object.assign(several, { kind: "ambiguous" });
|
|
@@ -5261,6 +5325,45 @@ async function fieldNames(page) {
|
|
|
5261
5325
|
return found;
|
|
5262
5326
|
}).catch(() => []);
|
|
5263
5327
|
}
|
|
5328
|
+
async function backupsFor(page, step) {
|
|
5329
|
+
if (!step.role || !step.name || step.locator) return [];
|
|
5330
|
+
try {
|
|
5331
|
+
const [primary] = await locateCandidates(page, step);
|
|
5332
|
+
if (!primary || await primary.count() !== 1) return [];
|
|
5333
|
+
return await primary.evaluate((el) => {
|
|
5334
|
+
const clean = (s2) => (s2 ?? "").replace(/\s+/g, " ").trim();
|
|
5335
|
+
const root = el.getRootNode();
|
|
5336
|
+
const unique = (selector) => {
|
|
5337
|
+
try {
|
|
5338
|
+
return root.querySelectorAll(selector).length === 1;
|
|
5339
|
+
} catch {
|
|
5340
|
+
return false;
|
|
5341
|
+
}
|
|
5342
|
+
};
|
|
5343
|
+
const found = [];
|
|
5344
|
+
const testId = el.getAttribute("data-testid");
|
|
5345
|
+
if (testId && unique(`[data-testid=${JSON.stringify(testId)}]`)) found.push({ strategy: "testId", value: testId });
|
|
5346
|
+
const placeholder = el.getAttribute("placeholder");
|
|
5347
|
+
if (clean(placeholder) && unique(`[placeholder=${JSON.stringify(placeholder)}]`)) found.push({ strategy: "placeholder", value: clean(placeholder) });
|
|
5348
|
+
const alt = el.getAttribute("alt");
|
|
5349
|
+
if (clean(alt) && unique(`[alt=${JSON.stringify(alt)}]`)) found.push({ strategy: "altText", value: clean(alt) });
|
|
5350
|
+
const title = el.getAttribute("title");
|
|
5351
|
+
if (clean(title) && unique(`[title=${JSON.stringify(title)}]`)) found.push({ strategy: "title", value: clean(title) });
|
|
5352
|
+
if (el.id && !/:r|[a-f0-9]{12}|\d{6}/i.test(el.id) && unique(`#${CSS.escape(el.id)}`)) {
|
|
5353
|
+
found.push({ strategy: "css", value: `#${CSS.escape(el.id)}` });
|
|
5354
|
+
}
|
|
5355
|
+
return found.slice(0, 4);
|
|
5356
|
+
}, void 0, { timeout: 1e3 });
|
|
5357
|
+
} catch {
|
|
5358
|
+
return [];
|
|
5359
|
+
}
|
|
5360
|
+
}
|
|
5361
|
+
function withBackups(target, backups) {
|
|
5362
|
+
if (!backups.length) return target;
|
|
5363
|
+
const seen = new Set((target.candidates ?? []).map((c) => `${c.strategy}\0${c.value}`));
|
|
5364
|
+
const merged = [...target.candidates ?? [], ...backups.filter((b2) => !seen.has(`${b2.strategy}\0${b2.value}`))].slice(0, 4);
|
|
5365
|
+
return { ...target, candidates: merged };
|
|
5366
|
+
}
|
|
5264
5367
|
var playwrightVersion2 = () => {
|
|
5265
5368
|
try {
|
|
5266
5369
|
return createRequire22(import.meta.url)("playwright/package.json").version;
|
|
@@ -6106,8 +6209,18 @@ var SqliteStorage = class {
|
|
|
6106
6209
|
) AS rn FROM run
|
|
6107
6210
|
) WHERE rn > ?`
|
|
6108
6211
|
).all(keep).map((r) => String(r.id));
|
|
6212
|
+
const dirOf = this.handle.prepare("SELECT artifacts_dir FROM run WHERE id=?");
|
|
6109
6213
|
const stmt = this.handle.prepare("DELETE FROM run WHERE id=?");
|
|
6110
|
-
for (const id of doomed)
|
|
6214
|
+
for (const id of doomed) {
|
|
6215
|
+
const dir = dirOf.get(id)?.artifacts_dir;
|
|
6216
|
+
stmt.run(id);
|
|
6217
|
+
if (typeof dir === "string" && dir) {
|
|
6218
|
+
try {
|
|
6219
|
+
fs5.rmSync(dir, { recursive: true, force: true });
|
|
6220
|
+
} catch {
|
|
6221
|
+
}
|
|
6222
|
+
}
|
|
6223
|
+
}
|
|
6111
6224
|
return doomed;
|
|
6112
6225
|
});
|
|
6113
6226
|
}
|
|
@@ -6978,11 +7091,12 @@ function totp(secretBase32, at, options = {}) {
|
|
|
6978
7091
|
const remaining = period - Math.floor(at / 1e3 % period);
|
|
6979
7092
|
return { code, remaining };
|
|
6980
7093
|
}
|
|
6981
|
-
async function imapMailbox(config, password) {
|
|
7094
|
+
async function imapMailbox(config, password, target = { host: config.host }) {
|
|
6982
7095
|
const { ImapFlow } = await import("imapflow");
|
|
6983
7096
|
const { simpleParser } = await import("mailparser");
|
|
6984
7097
|
const client = new ImapFlow({
|
|
6985
|
-
host:
|
|
7098
|
+
host: target.host,
|
|
7099
|
+
...target.servername ? { servername: target.servername } : {},
|
|
6986
7100
|
port: config.port,
|
|
6987
7101
|
secure: config.secure,
|
|
6988
7102
|
auth: { user: config.username, pass: password },
|
|
@@ -7041,7 +7155,8 @@ async function runMailbox(config, ctx) {
|
|
|
7041
7155
|
subject: fill(config.match.subject, ctx.inputs),
|
|
7042
7156
|
since: ctx.since ?? null
|
|
7043
7157
|
};
|
|
7044
|
-
const
|
|
7158
|
+
const password = secret(ctx, config.passwordRef, "mailbox password");
|
|
7159
|
+
const mailbox = ctx.openMailbox ? await ctx.openMailbox(config, password) : await imapMailbox(config, password, ctx.connectTo ? await ctx.connectTo(config.host) : void 0);
|
|
7045
7160
|
const deadline = now() + config.waitSeconds * 1e3;
|
|
7046
7161
|
try {
|
|
7047
7162
|
for (; ; ) {
|
|
@@ -7195,6 +7310,7 @@ async function cloudBrowserFor(config, secrets, name) {
|
|
|
7195
7310
|
return connectCloudBrowser(account, key, choice, name);
|
|
7196
7311
|
}
|
|
7197
7312
|
var LOCAL_CHROMIUM = { id: "chromium", provider: "local", engine: "chromium" };
|
|
7313
|
+
var KEEP_RUNS_LOCALLY = 50;
|
|
7198
7314
|
async function executeFlow(opts) {
|
|
7199
7315
|
const { store, storage = null, flowId, trigger, watch = false, onEvent } = opts;
|
|
7200
7316
|
const headless = watch ? false : opts.headless ?? store.readConfig().headless;
|
|
@@ -7431,6 +7547,7 @@ async function executeFlow(opts) {
|
|
|
7431
7547
|
artifactsDir: runDir && fs7.existsSync(runDir) ? runDir : null
|
|
7432
7548
|
});
|
|
7433
7549
|
}
|
|
7550
|
+
if (storage) void storage.pruneRuns(KEEP_RUNS_LOCALLY).catch(() => void 0);
|
|
7434
7551
|
await emit({ type: "run.finished", runId: run?.id ?? "", flowId: flow.id, outcome: last });
|
|
7435
7552
|
return {
|
|
7436
7553
|
outcome: last,
|
|
@@ -7733,6 +7850,7 @@ async function executeCase(opts) {
|
|
|
7733
7850
|
masked: false
|
|
7734
7851
|
});
|
|
7735
7852
|
}
|
|
7853
|
+
void storage.pruneRuns(50).catch(() => void 0);
|
|
7736
7854
|
await storage.finishRun(run.id, {
|
|
7737
7855
|
status: outcome.status === "passed" ? "passed" : outcome.error === "the run was cancelled" ? "cancelled" : outcome.status === "failed" ? "failed" : "errored",
|
|
7738
7856
|
errorMessage: outcome.error,
|
|
@@ -7750,6 +7868,10 @@ function redactPanel(state) {
|
|
|
7750
7868
|
return { ...panel, steps: panel.steps.map((s2) => redact(s2)) };
|
|
7751
7869
|
}
|
|
7752
7870
|
var START_GRACE_MS = 3e3;
|
|
7871
|
+
var IDLE_MS = (() => {
|
|
7872
|
+
const minutes = Number(process.env["FLOWRIKI_RECORDING_IDLE_MINUTES"] ?? 30);
|
|
7873
|
+
return Number.isFinite(minutes) && minutes >= 0 ? minutes * 6e4 : 30 * 6e4;
|
|
7874
|
+
})();
|
|
7753
7875
|
var RecordingCoordinator = class {
|
|
7754
7876
|
constructor(deps) {
|
|
7755
7877
|
this.deps = deps;
|
|
@@ -7773,6 +7895,33 @@ var RecordingCoordinator = class {
|
|
|
7773
7895
|
* than rebuilding it from the event, which would either lose the values or
|
|
7774
7896
|
* publish them. */
|
|
7775
7897
|
lastResult = null;
|
|
7898
|
+
/** When somebody last did anything in the recording. */
|
|
7899
|
+
touchedAt = 0;
|
|
7900
|
+
panelSeen = "";
|
|
7901
|
+
idleTimer = null;
|
|
7902
|
+
stoppingForIdle = false;
|
|
7903
|
+
touch() {
|
|
7904
|
+
this.touchedAt = Date.now();
|
|
7905
|
+
}
|
|
7906
|
+
watchIdle() {
|
|
7907
|
+
const limit = this.deps.idleMs ?? IDLE_MS;
|
|
7908
|
+
this.stopWatchingIdle();
|
|
7909
|
+
if (limit <= 0) return;
|
|
7910
|
+
this.touch();
|
|
7911
|
+
this.idleTimer = setInterval(() => {
|
|
7912
|
+
if (!this.session || this.stopInFlight || Date.now() - this.touchedAt < limit) return;
|
|
7913
|
+
this.stopWatchingIdle();
|
|
7914
|
+
this.stoppingForIdle = true;
|
|
7915
|
+
void (this.draft?.steps.length ? this.stop() : this.discard()).finally(() => {
|
|
7916
|
+
this.stoppingForIdle = false;
|
|
7917
|
+
});
|
|
7918
|
+
}, Math.min(this.deps.idleCheckMs ?? 6e4, limit));
|
|
7919
|
+
this.idleTimer.unref?.();
|
|
7920
|
+
}
|
|
7921
|
+
stopWatchingIdle() {
|
|
7922
|
+
if (this.idleTimer) clearInterval(this.idleTimer);
|
|
7923
|
+
this.idleTimer = null;
|
|
7924
|
+
}
|
|
7776
7925
|
/** What the last completed stop answered, for a caller that missed it —
|
|
7777
7926
|
* a dropped socket, or a Stop pressed inside the recorded page. */
|
|
7778
7927
|
lastStop() {
|
|
@@ -7796,6 +7945,7 @@ var RecordingCoordinator = class {
|
|
|
7796
7945
|
}
|
|
7797
7946
|
async answerDialog(accept, promptText) {
|
|
7798
7947
|
if (!this.session) throw new Error("No recording is active");
|
|
7948
|
+
this.touch();
|
|
7799
7949
|
await this.session.answerDialog(accept, promptText);
|
|
7800
7950
|
}
|
|
7801
7951
|
/** Open a headed browser and start capturing.
|
|
@@ -7871,18 +8021,29 @@ var RecordingCoordinator = class {
|
|
|
7871
8021
|
// a file nothing ever read.
|
|
7872
8022
|
environment: ExecutionEnvironmentSchema.parse(opts.environment ?? config.environment),
|
|
7873
8023
|
headless: opts.headless ?? false,
|
|
7874
|
-
onStep: (step) =>
|
|
8024
|
+
onStep: (step) => {
|
|
8025
|
+
this.touch();
|
|
8026
|
+
this.upsert(step);
|
|
8027
|
+
},
|
|
7875
8028
|
// The panel carries its OWN copy of every step, and it used to be
|
|
7876
8029
|
// emitted exactly as the session built it — so while `recording.step`
|
|
7877
8030
|
// was carefully redacted, the very next frame published the same step
|
|
7878
8031
|
// with the password still in it. Found by a test that typed a real
|
|
7879
8032
|
// password into a real browser; nothing about the redacted step event
|
|
7880
8033
|
// hinted at it.
|
|
7881
|
-
onState: (state) =>
|
|
8034
|
+
onState: (state) => {
|
|
8035
|
+
const seen = JSON.stringify(state);
|
|
8036
|
+
if (seen !== this.panelSeen) {
|
|
8037
|
+
this.panelSeen = seen;
|
|
8038
|
+
this.touch();
|
|
8039
|
+
}
|
|
8040
|
+
this.deps.emit({ type: "recording.panel", state: redactPanel(state) });
|
|
8041
|
+
},
|
|
7882
8042
|
// Stop pressed on the floating control inside the recorded page.
|
|
7883
8043
|
onStopRequested: (commit) => void (commit ? this.stop() : this.discard()),
|
|
7884
8044
|
onDisconnect: () => {
|
|
7885
8045
|
this.session = null;
|
|
8046
|
+
this.stopWatchingIdle();
|
|
7886
8047
|
this.deps.lease?.release("recording");
|
|
7887
8048
|
this.deps.emit({ type: "recording.interrupted" });
|
|
7888
8049
|
}
|
|
@@ -7898,6 +8059,7 @@ var RecordingCoordinator = class {
|
|
|
7898
8059
|
const projectOrigin = configuredBase ? new URL(normalizeStartUrl(configuredBase)).origin : "";
|
|
7899
8060
|
this.draft.baseUrl = opened.origin === projectOrigin ? "" : opened.origin;
|
|
7900
8061
|
this.draft.startPath = opened.pathname + opened.search;
|
|
8062
|
+
this.watchIdle();
|
|
7901
8063
|
this.deps.emit({ type: "recording.started", name: this.draft.name, url: this.session.startUrl });
|
|
7902
8064
|
return null;
|
|
7903
8065
|
}
|
|
@@ -7921,6 +8083,7 @@ var RecordingCoordinator = class {
|
|
|
7921
8083
|
* followed by an await, so two stops arriving together could otherwise both
|
|
7922
8084
|
* pass it and save the flow twice. */
|
|
7923
8085
|
stop() {
|
|
8086
|
+
this.stopWatchingIdle();
|
|
7924
8087
|
if (!this.stopInFlight) {
|
|
7925
8088
|
this.deps.lease?.beginRelease("recording");
|
|
7926
8089
|
this.stopInFlight = this.doStop().finally(() => {
|
|
@@ -7969,7 +8132,7 @@ var RecordingCoordinator = class {
|
|
|
7969
8132
|
const file2 = this.deps.store.saveFlow(doc);
|
|
7970
8133
|
this.draft = null;
|
|
7971
8134
|
const saved = { ...this.deps.store.getFlow(doc.id) ?? doc, path: file2 };
|
|
7972
|
-
this.deps.emit({ type: "recording.stopped", flow: saved, file: file2 });
|
|
8135
|
+
this.deps.emit({ type: "recording.stopped", flow: saved, file: file2, ...this.stoppingForIdle ? { reason: "idle" } : {} });
|
|
7973
8136
|
return this.lastResult = { ok: true, flow: saved, secrets: captured };
|
|
7974
8137
|
}
|
|
7975
8138
|
/** Throw the recording away: close the browser, drop the draft, write
|
|
@@ -7980,6 +8143,7 @@ var RecordingCoordinator = class {
|
|
|
7980
8143
|
* time — a password briefly reaches disk for a recording nobody wanted. */
|
|
7981
8144
|
async discard() {
|
|
7982
8145
|
if (!this.draft && !this.session) return { ok: false, error: "no recording in progress" };
|
|
8146
|
+
this.stopWatchingIdle();
|
|
7983
8147
|
this.deps.lease?.beginRelease("recording");
|
|
7984
8148
|
const ending = this.session;
|
|
7985
8149
|
this.session = null;
|
|
@@ -8122,15 +8286,41 @@ var COMMANDS = /* @__PURE__ */ new Set([
|
|
|
8122
8286
|
]);
|
|
8123
8287
|
var UnknownDebugCommand = class extends Error {
|
|
8124
8288
|
};
|
|
8289
|
+
var DEBUG_IDLE_MS = (() => {
|
|
8290
|
+
const minutes = Number(process.env["FLOWRIKI_RECORDING_IDLE_MINUTES"] ?? 30);
|
|
8291
|
+
return Number.isFinite(minutes) && minutes >= 0 ? minutes * 6e4 : 30 * 6e4;
|
|
8292
|
+
})();
|
|
8125
8293
|
var DebugCoordinator = class {
|
|
8126
8294
|
constructor(deps) {
|
|
8127
8295
|
this.deps = deps;
|
|
8128
8296
|
}
|
|
8129
8297
|
deps;
|
|
8298
|
+
touchedAt = 0;
|
|
8299
|
+
stateSeen = "";
|
|
8300
|
+
/** The steps as the session opened them: unchanged means nothing to lose. */
|
|
8301
|
+
openedWith = "";
|
|
8302
|
+
idleTimer = null;
|
|
8130
8303
|
retainCompleted = false;
|
|
8131
8304
|
lastState = null;
|
|
8132
8305
|
lastEnd = null;
|
|
8133
8306
|
current = null;
|
|
8307
|
+
watchIdle() {
|
|
8308
|
+
this.stopWatchingIdle();
|
|
8309
|
+
const limit = this.deps.idleMs ?? DEBUG_IDLE_MS;
|
|
8310
|
+
if (limit <= 0) return;
|
|
8311
|
+
this.touchedAt = Date.now();
|
|
8312
|
+
this.idleTimer = setInterval(() => {
|
|
8313
|
+
if (!this.current || Date.now() - this.touchedAt < limit) return;
|
|
8314
|
+
if (JSON.stringify(this.current.session.state().steps) !== this.openedWith) return;
|
|
8315
|
+
this.stopWatchingIdle();
|
|
8316
|
+
void this.end(false).catch(() => void 0);
|
|
8317
|
+
}, Math.min(this.deps.idleCheckMs ?? 6e4, limit));
|
|
8318
|
+
this.idleTimer.unref?.();
|
|
8319
|
+
}
|
|
8320
|
+
stopWatchingIdle() {
|
|
8321
|
+
if (this.idleTimer) clearInterval(this.idleTimer);
|
|
8322
|
+
this.idleTimer = null;
|
|
8323
|
+
}
|
|
8134
8324
|
status() {
|
|
8135
8325
|
return this.current ? { flowId: this.current.flowId, state: this.current.session.state() } : this.lastEnd?.ok && this.lastEnd.flow && this.lastState ? { flowId: this.lastEnd.flow.id, state: this.lastState } : { flowId: null, state: null };
|
|
8136
8326
|
}
|
|
@@ -8169,11 +8359,19 @@ var DebugCoordinator = class {
|
|
|
8169
8359
|
// Secrets are read on THIS machine and handed to the browser here. A
|
|
8170
8360
|
// hosted dashboard drives the session and never learns a value.
|
|
8171
8361
|
resolveSecret: (ref) => secrets[ref],
|
|
8172
|
-
onState: (state2) =>
|
|
8362
|
+
onState: (state2) => {
|
|
8363
|
+
const seen = JSON.stringify(state2);
|
|
8364
|
+
if (seen !== this.stateSeen) {
|
|
8365
|
+
this.stateSeen = seen;
|
|
8366
|
+
this.touchedAt = Date.now();
|
|
8367
|
+
}
|
|
8368
|
+
this.deps.emit({ type: "debug.state", flowId: flow.id, state: state2 });
|
|
8369
|
+
},
|
|
8173
8370
|
// Save & close / Discard, pressed on the panel inside the page.
|
|
8174
8371
|
onEndRequested: (commit) => void this.end(commit),
|
|
8175
8372
|
onDisconnect: () => {
|
|
8176
8373
|
this.current = null;
|
|
8374
|
+
this.stopWatchingIdle();
|
|
8177
8375
|
this.deps.lease?.release("debug");
|
|
8178
8376
|
this.deps.emit({ type: "debug.ended", flowId: flow.id, committed: false });
|
|
8179
8377
|
}
|
|
@@ -8181,6 +8379,8 @@ var DebugCoordinator = class {
|
|
|
8181
8379
|
this.deps.lease?.activate("debug");
|
|
8182
8380
|
this.current = { flowId: flow.id, session };
|
|
8183
8381
|
const state = session.state();
|
|
8382
|
+
this.openedWith = JSON.stringify(state.steps);
|
|
8383
|
+
this.watchIdle();
|
|
8184
8384
|
this.deps.emit({ type: "debug.state", flowId: flow.id, state });
|
|
8185
8385
|
return { flowId: flow.id, state };
|
|
8186
8386
|
} catch (err) {
|
|
@@ -8191,6 +8391,7 @@ var DebugCoordinator = class {
|
|
|
8191
8391
|
async command(command, args = {}) {
|
|
8192
8392
|
if (!this.current) throw new Error("no debug session is running");
|
|
8193
8393
|
if (!COMMANDS.has(command)) throw new UnknownDebugCommand(`unknown command: ${command}`);
|
|
8394
|
+
this.touchedAt = Date.now();
|
|
8194
8395
|
const s2 = this.current.session;
|
|
8195
8396
|
switch (command) {
|
|
8196
8397
|
case "play":
|
|
@@ -8233,6 +8434,7 @@ var DebugCoordinator = class {
|
|
|
8233
8434
|
}
|
|
8234
8435
|
const { flowId, session } = this.current;
|
|
8235
8436
|
this.current = null;
|
|
8437
|
+
this.stopWatchingIdle();
|
|
8236
8438
|
this.deps.lease?.beginRelease("debug");
|
|
8237
8439
|
try {
|
|
8238
8440
|
if (!commit) {
|
|
@@ -8388,7 +8590,7 @@ function resolveCheckouts(dirs) {
|
|
|
8388
8590
|
// src/engine/session.ts
|
|
8389
8591
|
import fs12 from "fs";
|
|
8390
8592
|
|
|
8391
|
-
// ../agent-core/dist/chunk-
|
|
8593
|
+
// ../agent-core/dist/chunk-CRKFBFSC.js
|
|
8392
8594
|
import { WebSocket, WebSocketServer } from "ws";
|
|
8393
8595
|
var HEARTBEAT_MS = 3e4;
|
|
8394
8596
|
function adapt(socket) {
|
|
@@ -8565,6 +8767,27 @@ var EventJournal = class {
|
|
|
8565
8767
|
}
|
|
8566
8768
|
};
|
|
8567
8769
|
var { DatabaseSync } = createRequire4(import.meta.url)("node:sqlite");
|
|
8770
|
+
var LOCAL_LIMIT = 64 * 1024;
|
|
8771
|
+
function fitted(value, limit = LOCAL_LIMIT) {
|
|
8772
|
+
const text = JSON.stringify(value);
|
|
8773
|
+
if (Buffer.byteLength(text) <= limit) return text;
|
|
8774
|
+
for (const [maxText, maxItems] of [[2e3, 400], [500, 120], [200, 40], [80, 12]]) {
|
|
8775
|
+
const smaller = JSON.stringify(shrink(value, maxText, maxItems));
|
|
8776
|
+
if (Buffer.byteLength(smaller) <= limit) return smaller;
|
|
8777
|
+
}
|
|
8778
|
+
throw new Error("event exceeds local size limit even when shortened");
|
|
8779
|
+
}
|
|
8780
|
+
function shrink(value, maxText, maxItems) {
|
|
8781
|
+
if (typeof value === "string") return value.length > maxText ? `${value.slice(0, maxText)}\u2026 (cut)` : value;
|
|
8782
|
+
if (Array.isArray(value)) {
|
|
8783
|
+
const items = value.length > maxItems ? [...value.slice(0, Math.ceil(maxItems / 2)), `\u2026 ${value.length - maxItems} left out`, ...value.slice(-Math.floor(maxItems / 2))] : value;
|
|
8784
|
+
return items.map((v) => shrink(v, maxText, maxItems));
|
|
8785
|
+
}
|
|
8786
|
+
if (value && typeof value === "object") {
|
|
8787
|
+
return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, shrink(v, maxText, maxItems)]));
|
|
8788
|
+
}
|
|
8789
|
+
return value;
|
|
8790
|
+
}
|
|
8568
8791
|
var AgentState = class {
|
|
8569
8792
|
constructor(file2, deviceId, commandLimit = 1e5) {
|
|
8570
8793
|
this.commandLimit = commandLimit;
|
|
@@ -8635,6 +8858,10 @@ var AgentState = class {
|
|
|
8635
8858
|
begin(command, fingerprint2) {
|
|
8636
8859
|
return this.transaction(() => {
|
|
8637
8860
|
if (this.get(command.id)) return false;
|
|
8861
|
+
if (Number(this.db.prepare("SELECT count(*) AS n FROM commands").get()?.n) >= this.commandLimit) {
|
|
8862
|
+
const cutoff = (command.issuedAt ?? command.expiresAt) - 30 * 24 * 60 * 6e4;
|
|
8863
|
+
if (cutoff > 0) this.db.prepare("DELETE FROM commands WHERE state IN ('completed','retired') AND finished_at < ?").run(cutoff);
|
|
8864
|
+
}
|
|
8638
8865
|
if (Number(this.db.prepare("SELECT count(*) AS n FROM commands").get()?.n) >= this.commandLimit) {
|
|
8639
8866
|
throw new Error("command ledger capacity reached; retain state and reconcile before further work");
|
|
8640
8867
|
}
|
|
@@ -8643,8 +8870,7 @@ var AgentState = class {
|
|
|
8643
8870
|
});
|
|
8644
8871
|
}
|
|
8645
8872
|
finish(id, ack, now) {
|
|
8646
|
-
const text =
|
|
8647
|
-
if (Buffer.byteLength(text) > 64 * 1024) throw new Error("command result exceeds local size limit");
|
|
8873
|
+
const text = fitted(AckSchema.parse(ack));
|
|
8648
8874
|
const result = this.db.prepare("UPDATE commands SET state='completed', ack=?, finished_at=? WHERE id=? AND state='started'").run(text, now, id);
|
|
8649
8875
|
if (Number(result.changes) !== 1) throw new Error("command intent is unavailable");
|
|
8650
8876
|
}
|
|
@@ -8657,8 +8883,7 @@ var AgentState = class {
|
|
|
8657
8883
|
this.db.prepare("UPDATE commands SET state='retired', ack=NULL WHERE state='completed' AND finished_at < ? AND expires_at <= ?").run(cutoff, now);
|
|
8658
8884
|
}
|
|
8659
8885
|
append(event, limit) {
|
|
8660
|
-
const text =
|
|
8661
|
-
if (Buffer.byteLength(text) > 64 * 1024) throw new Error("event exceeds local size limit");
|
|
8886
|
+
const text = fitted(event);
|
|
8662
8887
|
return this.transaction(() => {
|
|
8663
8888
|
const row = this.db.prepare("INSERT INTO events(payload) VALUES (?)").run(text);
|
|
8664
8889
|
const seq = Number(row.lastInsertRowid) - 1;
|
|
@@ -8687,6 +8912,13 @@ var AgentState = class {
|
|
|
8687
8912
|
this.db.close();
|
|
8688
8913
|
}
|
|
8689
8914
|
};
|
|
8915
|
+
function withoutSecrets(value) {
|
|
8916
|
+
if (Array.isArray(value)) return value.map(withoutSecrets);
|
|
8917
|
+
if (value && typeof value === "object") {
|
|
8918
|
+
return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, k === "secrets" ? {} : withoutSecrets(v)]));
|
|
8919
|
+
}
|
|
8920
|
+
return value;
|
|
8921
|
+
}
|
|
8690
8922
|
var Agent = class {
|
|
8691
8923
|
constructor(opts) {
|
|
8692
8924
|
this.opts = opts;
|
|
@@ -8824,7 +9056,7 @@ var Agent = class {
|
|
|
8824
9056
|
ack = this.refuse(command.id, "failed", String(err instanceof Error ? err.message : err).split("\n")[0]);
|
|
8825
9057
|
}
|
|
8826
9058
|
if (EFFECTFUL.has(command.name)) {
|
|
8827
|
-
this.state.finish(command.id, ack, this.now());
|
|
9059
|
+
this.state.finish(command.id, withoutSecrets(ack), this.now());
|
|
8828
9060
|
}
|
|
8829
9061
|
return ack;
|
|
8830
9062
|
}
|
|
@@ -9859,6 +10091,12 @@ var runFile = (file2, args, timeoutMs) => new Promise((resolve2, reject) => {
|
|
|
9859
10091
|
});
|
|
9860
10092
|
var live = /* @__PURE__ */ new Map();
|
|
9861
10093
|
var liveById = (sessionId) => [...live.values()].find((l) => l.id === sessionId);
|
|
10094
|
+
var IDLE_MS2 = (() => {
|
|
10095
|
+
const minutes = Number(process.env["FLOWRIKI_RECORDING_IDLE_MINUTES"] ?? 30);
|
|
10096
|
+
return Number.isFinite(minutes) && minutes >= 0 ? minutes * 6e4 : 30 * 6e4;
|
|
10097
|
+
})();
|
|
10098
|
+
var parked = /* @__PURE__ */ new Map();
|
|
10099
|
+
var PARKED_FOR_MS = 24 * 60 * 6e4;
|
|
9862
10100
|
var ownerOf = (l) => l.draft ? "recording" : "debug";
|
|
9863
10101
|
var SECRET_WORDS = /password|passwd|pass[-_\s]?code|pwd|secret|otp|\bpin\b|cvv|cvc/i;
|
|
9864
10102
|
async function screenOf(l, withImage = false) {
|
|
@@ -9886,12 +10124,31 @@ async function screenOf(l, withImage = false) {
|
|
|
9886
10124
|
device: l.session.deviceName
|
|
9887
10125
|
};
|
|
9888
10126
|
}
|
|
10127
|
+
async function closeIdlePhoneSessions(release, limit = IDLE_MS2, now = Date.now()) {
|
|
10128
|
+
for (const [id, kept] of parked) if (now - kept.at > PARKED_FOR_MS) parked.delete(id);
|
|
10129
|
+
if (limit <= 0) return;
|
|
10130
|
+
for (const l of [...live.values()]) {
|
|
10131
|
+
if (now - l.touchedAt < limit) continue;
|
|
10132
|
+
live.delete(l.platform);
|
|
10133
|
+
release(ownerOf(l), l.platform);
|
|
10134
|
+
await l.session.close().catch(() => void 0);
|
|
10135
|
+
if (l.draft?.steps.length) parked.set(l.id, { l, at: now });
|
|
10136
|
+
}
|
|
10137
|
+
}
|
|
9889
10138
|
function mobileHandlers(ctx) {
|
|
9890
10139
|
const require2 = (command) => {
|
|
9891
10140
|
const l = liveById(command.args["sessionId"]);
|
|
9892
|
-
if (
|
|
9893
|
-
|
|
10141
|
+
if (l) {
|
|
10142
|
+
l.touchedAt = Date.now();
|
|
10143
|
+
return l;
|
|
10144
|
+
}
|
|
10145
|
+
if (parked.has(String(command.args["sessionId"]))) {
|
|
10146
|
+
throw new Error("this phone was let go after a long time with nothing done \u2014 press Stop to save what was recorded");
|
|
10147
|
+
}
|
|
10148
|
+
throw new Error("that phone session has ended \u2014 open it again");
|
|
9894
10149
|
};
|
|
10150
|
+
const sweeper = setInterval(() => void closeIdlePhoneSessions((owner, platform) => ctx.lease?.release(owner, platform)), 6e4);
|
|
10151
|
+
sweeper.unref?.();
|
|
9895
10152
|
return {
|
|
9896
10153
|
// Everything this computer has for testing — web and phones — for the Guide.
|
|
9897
10154
|
...ctx.deps.machineCheck ? {
|
|
@@ -9965,6 +10222,7 @@ function mobileHandlers(ctx) {
|
|
|
9965
10222
|
platform,
|
|
9966
10223
|
values: /* @__PURE__ */ new Map(),
|
|
9967
10224
|
typed,
|
|
10225
|
+
touchedAt: Date.now(),
|
|
9968
10226
|
resolveSecret: (ref) => typed.get(ref) ?? secrets[ref],
|
|
9969
10227
|
resolveAuth: authenticationResolver(command.args["authentications"]),
|
|
9970
10228
|
runHelper: helperRunner(command.args["helpers"], secrets)
|
|
@@ -9994,6 +10252,12 @@ function mobileHandlers(ctx) {
|
|
|
9994
10252
|
}
|
|
9995
10253
|
return { ok: true, result: { sessionId: l2.id, screen: await screenOf(l2) } };
|
|
9996
10254
|
}
|
|
10255
|
+
const resting = parked.get(String(command.args["sessionId"]));
|
|
10256
|
+
if (resting && (op === "stop" || op === "discard")) {
|
|
10257
|
+
parked.delete(String(command.args["sessionId"]));
|
|
10258
|
+
if (op === "discard") return { ok: true, result: { ended: true } };
|
|
10259
|
+
return { ok: true, result: finished(resting.l) };
|
|
10260
|
+
}
|
|
9997
10261
|
const l = require2(command);
|
|
9998
10262
|
if (op === "act" || op === "run") {
|
|
9999
10263
|
const raw = command.args["step"] ?? {};
|
|
@@ -10069,24 +10333,7 @@ function mobileHandlers(ctx) {
|
|
|
10069
10333
|
ctx.lease?.release(ownerOf(l), l.platform);
|
|
10070
10334
|
await l.session.close().catch(() => void 0);
|
|
10071
10335
|
if (op === "discard" || !l.draft) return { ok: true, result: { ended: true } };
|
|
10072
|
-
|
|
10073
|
-
const d = l.draft;
|
|
10074
|
-
const flow = {
|
|
10075
|
-
schemaVersion: CURRENT_SCHEMA_VERSION,
|
|
10076
|
-
id: `flw_${randomUUID2().replace(/-/g, "").slice(0, 12)}`,
|
|
10077
|
-
featureId: d.featureId,
|
|
10078
|
-
name: d.name,
|
|
10079
|
-
slug: slugify(d.name) || "flow",
|
|
10080
|
-
startPath: d.startPath,
|
|
10081
|
-
platform: l.platform,
|
|
10082
|
-
recordedAt: now,
|
|
10083
|
-
recordedBy: d.recordedBy,
|
|
10084
|
-
updatedAt: now,
|
|
10085
|
-
revision: 0,
|
|
10086
|
-
steps: d.steps,
|
|
10087
|
-
...d.signIn ? { signIn: d.signIn } : {}
|
|
10088
|
-
};
|
|
10089
|
-
return { ok: true, result: { flow, secrets: Object.fromEntries(l.typed) } };
|
|
10336
|
+
return { ok: true, result: finished(l) };
|
|
10090
10337
|
}
|
|
10091
10338
|
return { ok: false, reason: "failed", detail: `unknown phone session operation "${op}"` };
|
|
10092
10339
|
} catch (error) {
|
|
@@ -10096,6 +10343,26 @@ function mobileHandlers(ctx) {
|
|
|
10096
10343
|
}
|
|
10097
10344
|
};
|
|
10098
10345
|
}
|
|
10346
|
+
function finished(l) {
|
|
10347
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
10348
|
+
const d = l.draft;
|
|
10349
|
+
const flow = {
|
|
10350
|
+
schemaVersion: CURRENT_SCHEMA_VERSION,
|
|
10351
|
+
id: `flw_${randomUUID2().replace(/-/g, "").slice(0, 12)}`,
|
|
10352
|
+
featureId: d.featureId,
|
|
10353
|
+
name: d.name,
|
|
10354
|
+
slug: slugify(d.name) || "flow",
|
|
10355
|
+
startPath: d.startPath,
|
|
10356
|
+
platform: l.platform,
|
|
10357
|
+
recordedAt: now,
|
|
10358
|
+
recordedBy: d.recordedBy,
|
|
10359
|
+
updatedAt: now,
|
|
10360
|
+
revision: 0,
|
|
10361
|
+
steps: d.steps,
|
|
10362
|
+
...d.signIn ? { signIn: d.signIn } : {}
|
|
10363
|
+
};
|
|
10364
|
+
return { flow, secrets: Object.fromEntries(l.typed) };
|
|
10365
|
+
}
|
|
10099
10366
|
function buildHandlers(deps) {
|
|
10100
10367
|
const context = {
|
|
10101
10368
|
deps,
|
|
@@ -10154,6 +10421,20 @@ function toolArgs(tool, input) {
|
|
|
10154
10421
|
if (typeof value === "number" && Number.isFinite(value)) out[key] = value;
|
|
10155
10422
|
}
|
|
10156
10423
|
if (record["newBranch"] === true) out["newBranch"] = true;
|
|
10424
|
+
if (typeof record["case"] === "string" && record["case"]) out["case"] = clip(record["case"], 40);
|
|
10425
|
+
if (Array.isArray(record["features"])) {
|
|
10426
|
+
const cases = [];
|
|
10427
|
+
for (const feature of record["features"]) {
|
|
10428
|
+
if (!feature || typeof feature !== "object") continue;
|
|
10429
|
+
const f = feature;
|
|
10430
|
+
const folder = typeof f["feature"] === "string" ? f["feature"] : typeof f["name"] === "string" ? f["name"] : "";
|
|
10431
|
+
for (const c of Array.isArray(f["cases"]) ? f["cases"] : []) {
|
|
10432
|
+
const name = c && typeof c === "object" ? c["name"] : void 0;
|
|
10433
|
+
if (typeof name === "string" && name && cases.length < 40) cases.push({ folder: clip(folder), name: clip(name) });
|
|
10434
|
+
}
|
|
10435
|
+
}
|
|
10436
|
+
if (cases.length) out["cases"] = cases;
|
|
10437
|
+
}
|
|
10157
10438
|
if (tool === "add_check" && typeof record["value"] === "string") out["value"] = clip(record["value"], 120);
|
|
10158
10439
|
if (Array.isArray(record["after"])) {
|
|
10159
10440
|
out["after"] = record["after"].filter((v) => typeof v === "string").slice(0, 5).map((v) => clip(v));
|
|
@@ -10185,13 +10466,65 @@ var ASK_MODE_TOOLS = [
|
|
|
10185
10466
|
"list_my_branches",
|
|
10186
10467
|
"run_insights"
|
|
10187
10468
|
];
|
|
10469
|
+
var NOT_RECORDING = [
|
|
10470
|
+
"create_project",
|
|
10471
|
+
"list_projects",
|
|
10472
|
+
"read_import_file",
|
|
10473
|
+
"save_import_cases",
|
|
10474
|
+
"read_update_request",
|
|
10475
|
+
"propose_case_updates",
|
|
10476
|
+
"write_test_cases",
|
|
10477
|
+
"gather_branches",
|
|
10478
|
+
"discard_branch"
|
|
10479
|
+
];
|
|
10480
|
+
var NOT_SAVING_TESTS = [
|
|
10481
|
+
"save_flow",
|
|
10482
|
+
"save_case",
|
|
10483
|
+
"preview_case",
|
|
10484
|
+
"start_case",
|
|
10485
|
+
"continue_flow",
|
|
10486
|
+
"remove_step",
|
|
10487
|
+
"replay_flow",
|
|
10488
|
+
"add_check",
|
|
10489
|
+
"use_helper",
|
|
10490
|
+
"create_sign_in",
|
|
10491
|
+
"create_account",
|
|
10492
|
+
"create_helper",
|
|
10493
|
+
"report_blocker"
|
|
10494
|
+
];
|
|
10495
|
+
var JOB_TOOLS_EXCEPT = {
|
|
10496
|
+
heal: [...NOT_RECORDING, "create_sign_in", "create_account"],
|
|
10497
|
+
automate: NOT_RECORDING,
|
|
10498
|
+
reautomate: NOT_RECORDING,
|
|
10499
|
+
update: NOT_RECORDING.filter((t) => t !== "read_update_request" && t !== "propose_case_updates"),
|
|
10500
|
+
explore: [...NOT_RECORDING, ...NOT_SAVING_TESTS],
|
|
10501
|
+
// Writing cases may still set up a sign-in it finds missing.
|
|
10502
|
+
write: [
|
|
10503
|
+
"create_project",
|
|
10504
|
+
"list_projects",
|
|
10505
|
+
"gather_branches",
|
|
10506
|
+
"discard_branch",
|
|
10507
|
+
"read_update_request",
|
|
10508
|
+
"propose_case_updates",
|
|
10509
|
+
...NOT_SAVING_TESTS.filter((t) => !t.startsWith("create_"))
|
|
10510
|
+
]
|
|
10511
|
+
};
|
|
10512
|
+
function toolsExceptFor(prompt, briefGiven) {
|
|
10513
|
+
const except = [...JOB_TOOLS_EXCEPT[usageKindOf(prompt)] ?? []];
|
|
10514
|
+
if (briefGiven) except.push("project_brief");
|
|
10515
|
+
return except;
|
|
10516
|
+
}
|
|
10188
10517
|
function buildAskArgs(opts) {
|
|
10189
10518
|
const ask = opts.mode === "ask";
|
|
10190
10519
|
const folders = opts.folders ?? [];
|
|
10191
10520
|
const brief = opts.brief?.trim();
|
|
10192
10521
|
const guidance = [
|
|
10193
|
-
`You are the Flowriki assistant for the project "${opts.projectName}" (id ${opts.projectId}). Use the Flowriki tools, and pass projectId "${opts.projectId}" wherever a tool takes one. ` + (brief ? "The project brief below
|
|
10194
|
-
ask ? "ASK MODE: answer the question; change nothing. Cite what you rely on as [[testCase:<its id, tc_\u2026>]], [[knowledge:features/checkout.md]] or [[file:path/in/repo.ts]]." : "DO MODE: do what is asked, on Claude's working branch, for a person to review. Ask in the chat and wait before publishing, deleting, or sending anything to another system. Work quietly: the person sees what you build and your final answer, so keep words between tool calls to a minimum and end with a short result \u2014 what was saved, and anything that needs them.
|
|
10522
|
+
`You are the Flowriki assistant for the project "${opts.projectName}" (id ${opts.projectId}). Use the Flowriki tools, and pass projectId "${opts.projectId}" wherever a tool takes one. ` + (brief ? "The project brief below IS project_brief for this job \u2014 do not call project_brief." : "Start with project_brief: it is this project's memory."),
|
|
10523
|
+
ask ? "ASK MODE: answer the question; change nothing. Cite what you rely on as [[testCase:<its id, tc_\u2026>]], [[knowledge:features/checkout.md]] or [[file:path/in/repo.ts]]." : "DO MODE: do what is asked, on Claude's working branch, for a person to review. Ask in the chat and wait before publishing, deleting, or sending anything to another system. Work quietly: the person sees what you build and your final answer, so keep words between tool calls to a minimum and end with a short result \u2014 what was saved, and anything that needs them. Only when you learned something about the app that the brief does not already say and the next job needs (a screen, a rule, a quirk, what failed and why), write it with write_knowledge (memory/sessions/<date>-<topic>.md, append) \u2014 briefly.",
|
|
10524
|
+
// SPENDING. Every turn re-reads the whole conversation, so turns are the
|
|
10525
|
+
// cost: do several steps per act call, don't re-read a page a step already
|
|
10526
|
+
// returned, and stop instead of retrying the same failing step.
|
|
10527
|
+
ask ? "" : "Spend turns carefully: act takes several steps at once; a step's answer already shows the page \u2014 read_page only when it did not; after 3 failed tries at the same step, report_blocker and move on.",
|
|
10195
10528
|
"Use only the tools you have: a tool is used by calling it, not by writing the call out as text. If Flowriki's tools are missing, say so in one sentence and stop.",
|
|
10196
10529
|
folders.length ? `The project's code is in ${folders.join(" and ")}. Read it with Read, Grep and Glob when it helps; you cannot change it.` : "",
|
|
10197
10530
|
brief ? `
|
|
@@ -10221,9 +10554,25 @@ ${brief}` : ""
|
|
|
10221
10554
|
"dontAsk",
|
|
10222
10555
|
"--append-system-prompt",
|
|
10223
10556
|
guidance,
|
|
10557
|
+
// THE MODEL, SAID — not whatever the person's default is. Recording and
|
|
10558
|
+
// automating tests is what Sonnet does well; a default of Opus costs
|
|
10559
|
+
// several times as much for the same tests. FLOWRIKI_ASSISTANT_MODEL
|
|
10560
|
+
// overrides it.
|
|
10561
|
+
"--model",
|
|
10562
|
+
opts.model ?? (process.env["FLOWRIKI_ASSISTANT_MODEL"]?.trim() || "sonnet"),
|
|
10563
|
+
// A ceiling, not a target: a job that has taken this many turns is going
|
|
10564
|
+
// round in circles. What it saved stays; "continue" carries on.
|
|
10565
|
+
"--max-turns",
|
|
10566
|
+
String(opts.maxTurns ?? (ask ? 40 : 160)),
|
|
10224
10567
|
...opts.sessionId ? ["--resume", opts.sessionId] : []
|
|
10225
10568
|
];
|
|
10226
10569
|
}
|
|
10570
|
+
var tokensOf = (u) => ({
|
|
10571
|
+
input: u.input_tokens ?? 0,
|
|
10572
|
+
output: u.output_tokens ?? 0,
|
|
10573
|
+
cacheRead: u.cache_read_input_tokens ?? 0,
|
|
10574
|
+
cacheWrite: u.cache_creation_input_tokens ?? 0
|
|
10575
|
+
});
|
|
10227
10576
|
function clearOld(dir) {
|
|
10228
10577
|
try {
|
|
10229
10578
|
for (const name of fs43.readdirSync(dir)) {
|
|
@@ -10248,11 +10597,13 @@ function eventsFromLine(askId, line) {
|
|
|
10248
10597
|
if (parsed.session_id) out.push({ name: "assistant.session", data: { askId, sessionId: parsed.session_id } });
|
|
10249
10598
|
}
|
|
10250
10599
|
if (parsed.type === "assistant") {
|
|
10600
|
+
const tools = [];
|
|
10251
10601
|
for (const block of parsed.message?.content ?? []) {
|
|
10252
10602
|
if (block.type === "text" && block.text?.trim()) {
|
|
10253
10603
|
out.push({ name: "assistant.text", data: { askId, text: block.text } });
|
|
10254
10604
|
} else if (block.type === "tool_use" && block.name) {
|
|
10255
10605
|
const tool = block.name.replace(TOOL_PREFIX, "");
|
|
10606
|
+
tools.push(tool);
|
|
10256
10607
|
out.push({
|
|
10257
10608
|
name: "assistant.tool",
|
|
10258
10609
|
data: {
|
|
@@ -10265,6 +10616,12 @@ function eventsFromLine(askId, line) {
|
|
|
10265
10616
|
});
|
|
10266
10617
|
}
|
|
10267
10618
|
}
|
|
10619
|
+
if (parsed.message?.usage && parsed.message.id) {
|
|
10620
|
+
out.push({
|
|
10621
|
+
name: "assistant.usage",
|
|
10622
|
+
data: { askId, messageId: parsed.message.id, tools, ...tokensOf(parsed.message.usage), ...parsed.message.model ? { model: parsed.message.model } : {} }
|
|
10623
|
+
});
|
|
10624
|
+
}
|
|
10268
10625
|
}
|
|
10269
10626
|
if (parsed.type === "user") {
|
|
10270
10627
|
for (const block of parsed.message?.content ?? []) {
|
|
@@ -10278,6 +10635,20 @@ function eventsFromLine(askId, line) {
|
|
|
10278
10635
|
});
|
|
10279
10636
|
}
|
|
10280
10637
|
}
|
|
10638
|
+
if (parsed.type === "rate_limit_event" && parsed.rate_limit_info) {
|
|
10639
|
+
const info = parsed.rate_limit_info;
|
|
10640
|
+
out.push({
|
|
10641
|
+
name: "assistant.limits",
|
|
10642
|
+
data: {
|
|
10643
|
+
askId,
|
|
10644
|
+
...typeof info.status === "string" ? { status: info.status } : {},
|
|
10645
|
+
...typeof info.rateLimitType === "string" ? { window: info.rateLimitType } : {},
|
|
10646
|
+
...typeof info.utilization === "number" ? { utilization: info.utilization } : {},
|
|
10647
|
+
...typeof info.resetsAt === "number" ? { resetsAt: info.resetsAt } : {},
|
|
10648
|
+
...info.isUsingOverage === true ? { overage: true } : {}
|
|
10649
|
+
}
|
|
10650
|
+
});
|
|
10651
|
+
}
|
|
10281
10652
|
if (parsed.type === "result") {
|
|
10282
10653
|
out.push({
|
|
10283
10654
|
name: "assistant.done",
|
|
@@ -10285,12 +10656,24 @@ function eventsFromLine(askId, line) {
|
|
|
10285
10656
|
askId,
|
|
10286
10657
|
ok: parsed.is_error !== true,
|
|
10287
10658
|
result: typeof parsed.result === "string" ? parsed.result : "",
|
|
10288
|
-
...parsed.session_id ? { sessionId: parsed.session_id } : {}
|
|
10659
|
+
...parsed.session_id ? { sessionId: parsed.session_id } : {},
|
|
10660
|
+
// The whole job, as the model counted it.
|
|
10661
|
+
...parsed.usage ? { usage: tokensOf(parsed.usage) } : {},
|
|
10662
|
+
...typeof parsed.total_cost_usd === "number" ? { costUsd: parsed.total_cost_usd } : {},
|
|
10663
|
+
...typeof parsed.num_turns === "number" ? { turns: parsed.num_turns } : {},
|
|
10664
|
+
...typeof parsed.duration_ms === "number" ? { durationMs: parsed.duration_ms } : {}
|
|
10289
10665
|
}
|
|
10290
10666
|
});
|
|
10291
10667
|
}
|
|
10292
10668
|
return out;
|
|
10293
10669
|
}
|
|
10670
|
+
function stop(child) {
|
|
10671
|
+
child.kill("SIGTERM");
|
|
10672
|
+
const force = setTimeout(() => {
|
|
10673
|
+
if (child.exitCode === null && child.signalCode === null) child.kill("SIGKILL");
|
|
10674
|
+
}, 5e3);
|
|
10675
|
+
force.unref?.();
|
|
10676
|
+
}
|
|
10294
10677
|
function createClaudeCli(options) {
|
|
10295
10678
|
const spawn2 = options.spawn ?? nodeSpawn;
|
|
10296
10679
|
const binary = options.binary ?? process.env["FLOWRIKI_CLAUDE_BIN"] ?? "claude";
|
|
@@ -10298,7 +10681,18 @@ function createClaudeCli(options) {
|
|
|
10298
10681
|
const scratchRoot2 = options.scratchRoot ?? path42.join(os22.tmpdir(), "flowriki-claude");
|
|
10299
10682
|
const running = /* @__PURE__ */ new Map();
|
|
10300
10683
|
const MAX_AT_ONCE = 3;
|
|
10301
|
-
|
|
10684
|
+
process.once("exit", () => {
|
|
10685
|
+
for (const child of running.values()) {
|
|
10686
|
+
try {
|
|
10687
|
+
child.kill("SIGTERM");
|
|
10688
|
+
} catch {
|
|
10689
|
+
}
|
|
10690
|
+
}
|
|
10691
|
+
});
|
|
10692
|
+
const cancelled = /* @__PURE__ */ new Set();
|
|
10693
|
+
const retrying = /* @__PURE__ */ new Set();
|
|
10694
|
+
const sessionsInUse = /* @__PURE__ */ new Set();
|
|
10695
|
+
const occupied = () => running.size + retrying.size;
|
|
10302
10696
|
function capture(args, timeoutMs = 15e3) {
|
|
10303
10697
|
return new Promise((resolve2) => {
|
|
10304
10698
|
let out = "", err = "", settled = false;
|
|
@@ -10328,8 +10722,8 @@ function createClaudeCli(options) {
|
|
|
10328
10722
|
child.on("close", (code) => finish(code, false));
|
|
10329
10723
|
});
|
|
10330
10724
|
}
|
|
10331
|
-
const LANE_FRESH_MS =
|
|
10332
|
-
const LANE_MAX_TOKENS =
|
|
10725
|
+
const LANE_FRESH_MS = 5 * 6e4;
|
|
10726
|
+
const LANE_MAX_TOKENS = 4e4;
|
|
10333
10727
|
const laneFile = (dir) => path42.join(dir, "lane-sessions.json");
|
|
10334
10728
|
const readLanes = (dir) => {
|
|
10335
10729
|
try {
|
|
@@ -10355,6 +10749,8 @@ function createClaudeCli(options) {
|
|
|
10355
10749
|
}
|
|
10356
10750
|
const kept = readLanes(dir)[ask.platform];
|
|
10357
10751
|
if (!kept || Date.now() - kept.updatedAt > LANE_FRESH_MS || kept.tokens > LANE_MAX_TOKENS) return void 0;
|
|
10752
|
+
if (sessionsInUse.has(kept.sessionId)) return void 0;
|
|
10753
|
+
if ((kept.kind ?? "chat") !== usageKindOf(ask.prompt)) return void 0;
|
|
10358
10754
|
return kept.sessionId;
|
|
10359
10755
|
}
|
|
10360
10756
|
return {
|
|
@@ -10375,10 +10771,10 @@ function createClaudeCli(options) {
|
|
|
10375
10771
|
} catch {
|
|
10376
10772
|
return { started: false, detail: "The assistant\u2019s command-line tool could not be started." };
|
|
10377
10773
|
}
|
|
10378
|
-
let
|
|
10774
|
+
let finished2 = false;
|
|
10379
10775
|
const done = async (detail) => {
|
|
10380
|
-
if (
|
|
10381
|
-
|
|
10776
|
+
if (finished2) return;
|
|
10777
|
+
finished2 = true;
|
|
10382
10778
|
const status2 = await this.status();
|
|
10383
10779
|
emit("assistant.connect.finished", { signedIn: status2.signedIn, ...detail ? { detail } : {} });
|
|
10384
10780
|
};
|
|
@@ -10398,31 +10794,49 @@ function createClaudeCli(options) {
|
|
|
10398
10794
|
return { started: true };
|
|
10399
10795
|
},
|
|
10400
10796
|
ask(ask) {
|
|
10401
|
-
if (
|
|
10797
|
+
if (occupied() >= MAX_AT_ONCE) return { started: false, detail: `The assistant is already answering ${MAX_AT_ONCE} questions on this machine \u2014 wait for one to finish.` };
|
|
10402
10798
|
fs43.mkdirSync(scratchRoot2, { recursive: true, mode: 448 });
|
|
10403
10799
|
const dir = path42.join(scratchRoot2, `project-${ask.projectId.replace(/[^A-Za-z0-9_-]/g, "_")}`);
|
|
10404
10800
|
fs43.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
10405
10801
|
clearOld(dir);
|
|
10406
10802
|
const mcpConfigPath = path42.join(dir, `mcp-${ask.askId.replace(/[^A-Za-z0-9_-]/g, "_").slice(0, 80)}.json`);
|
|
10803
|
+
const session = laneSession(ask, dir);
|
|
10804
|
+
const except = ask.mode === "ask" ? [] : toolsExceptFor(ask.prompt, Boolean(ask.brief && !session));
|
|
10407
10805
|
fs43.writeFileSync(
|
|
10408
10806
|
mcpConfigPath,
|
|
10409
|
-
JSON.stringify({ mcpServers: { [MCP_SERVER_NAME]: { command: options.mcp.command, args: options.mcp.args, env: {
|
|
10807
|
+
JSON.stringify({ mcpServers: { [MCP_SERVER_NAME]: { command: options.mcp.command, args: options.mcp.args, env: {
|
|
10808
|
+
FLOWRIKI_ASK_ID: ask.askId,
|
|
10809
|
+
// The project, known from the start: no "which project?" turn.
|
|
10810
|
+
FLOWRIKI_PROJECT_ID: ask.projectId,
|
|
10811
|
+
// The agent that launched it decides its version: the tool server
|
|
10812
|
+
// does not update itself in the middle of somebody's job.
|
|
10813
|
+
FLOWRIKI_NO_UPDATE: "1",
|
|
10814
|
+
FLOWRIKI_MCP_CHILD: "1",
|
|
10815
|
+
// A question: only the reading tools are served at all.
|
|
10816
|
+
...ask.mode === "ask" ? { FLOWRIKI_TOOLS: ASK_MODE_TOOLS.join(",") } : {},
|
|
10817
|
+
// A job of a known kind: the tools it never uses are not served.
|
|
10818
|
+
...except.length ? { FLOWRIKI_TOOLS_EXCEPT: except.join(",") } : {}
|
|
10819
|
+
} } } }),
|
|
10410
10820
|
{ mode: 384 }
|
|
10411
10821
|
);
|
|
10412
|
-
return launch(ask, dir, mcpConfigPath,
|
|
10822
|
+
return launch(ask, dir, mcpConfigPath, session);
|
|
10413
10823
|
},
|
|
10414
10824
|
cancel(askId) {
|
|
10825
|
+
if (retrying.has(askId)) {
|
|
10826
|
+
cancelled.add(askId);
|
|
10827
|
+
return true;
|
|
10828
|
+
}
|
|
10415
10829
|
const child = running.get(askId);
|
|
10416
10830
|
if (!child) return false;
|
|
10417
|
-
cancelled
|
|
10418
|
-
child
|
|
10831
|
+
cancelled.add(askId);
|
|
10832
|
+
stop(child);
|
|
10419
10833
|
setTimeout(() => child.kill("SIGKILL"), 3e3).unref?.();
|
|
10420
10834
|
return true;
|
|
10421
10835
|
},
|
|
10422
10836
|
/** Full — no room for another question — rather than "something runs":
|
|
10423
10837
|
* a second conversation is welcome while the first works. */
|
|
10424
10838
|
busy() {
|
|
10425
|
-
return
|
|
10839
|
+
return occupied() >= MAX_AT_ONCE;
|
|
10426
10840
|
}
|
|
10427
10841
|
};
|
|
10428
10842
|
function launch(ask, dir, mcpConfigPath, sessionId, attempt = 1) {
|
|
@@ -10438,20 +10852,26 @@ function createClaudeCli(options) {
|
|
|
10438
10852
|
...sessionId ? { sessionId } : {},
|
|
10439
10853
|
...ask.folders?.length ? { folders: ask.folders } : {},
|
|
10440
10854
|
...ask.mode ? { mode: ask.mode } : {},
|
|
10441
|
-
|
|
10855
|
+
// Resumed, the conversation already holds its brief: a changed
|
|
10856
|
+
// one in the system prompt would throw away the whole cache.
|
|
10857
|
+
...ask.brief && !sessionId ? { brief: ask.brief } : {}
|
|
10442
10858
|
}),
|
|
10443
10859
|
// THE SCRATCH FOLDER, not the project: nothing here to read or write.
|
|
10444
|
-
|
|
10860
|
+
// Thinking is output, priced at five times input: capped per turn,
|
|
10861
|
+
// unless the person set their own.
|
|
10862
|
+
{ cwd: dir, stdio: ["ignore", "pipe", "pipe"], env: { ...process.env, MAX_THINKING_TOKENS: process.env["MAX_THINKING_TOKENS"] ?? "6000" } }
|
|
10445
10863
|
);
|
|
10446
10864
|
} catch {
|
|
10447
10865
|
return { started: false, detail: "The assistant\u2019s command-line tool could not be started." };
|
|
10448
10866
|
}
|
|
10449
10867
|
running.set(ask.askId, child);
|
|
10868
|
+
if (sessionId) sessionsInUse.add(sessionId);
|
|
10450
10869
|
let buffer = "", stderr = "", sawDone = false, ended = false;
|
|
10451
10870
|
let silentFailure = false;
|
|
10452
10871
|
let toolsMissing = false;
|
|
10453
10872
|
let session;
|
|
10454
10873
|
let spent = 0;
|
|
10874
|
+
let lastContext = 0;
|
|
10455
10875
|
const handleLine = (line) => {
|
|
10456
10876
|
if (toolsMissing) return;
|
|
10457
10877
|
for (const event of eventsFromLine(ask.askId, line)) {
|
|
@@ -10464,6 +10884,7 @@ function createClaudeCli(options) {
|
|
|
10464
10884
|
continue;
|
|
10465
10885
|
}
|
|
10466
10886
|
if (event.name === "assistant.session") session = String(event.data["sessionId"] ?? "") || session;
|
|
10887
|
+
if (event.name === "assistant.usage") lastContext = Number(event.data["input"] ?? 0) + Number(event.data["cacheRead"] ?? 0) + Number(event.data["cacheWrite"] ?? 0) + Number(event.data["output"] ?? 0);
|
|
10467
10888
|
if (event.name === "assistant.done" && event.data["ok"] === false && !String(event.data["result"] ?? "").trim()) {
|
|
10468
10889
|
silentFailure = true;
|
|
10469
10890
|
continue;
|
|
@@ -10486,9 +10907,17 @@ function createClaudeCli(options) {
|
|
|
10486
10907
|
clearTimeout(timer);
|
|
10487
10908
|
if (buffer.trim()) handleLine(buffer);
|
|
10488
10909
|
running.delete(ask.askId);
|
|
10489
|
-
if (
|
|
10910
|
+
if (sessionId) sessionsInUse.delete(sessionId);
|
|
10911
|
+
const stopped = cancelled.has(ask.askId);
|
|
10912
|
+
if (toolsMissing && !stopped) {
|
|
10490
10913
|
if (attempt < 3) {
|
|
10914
|
+
retrying.add(ask.askId);
|
|
10491
10915
|
setTimeout(() => {
|
|
10916
|
+
retrying.delete(ask.askId);
|
|
10917
|
+
if (cancelled.delete(ask.askId)) {
|
|
10918
|
+
ask.emit("assistant.done", { askId: ask.askId, ok: false, result: "Stopped." });
|
|
10919
|
+
return;
|
|
10920
|
+
}
|
|
10492
10921
|
const again = launch(ask, dir, mcpConfigPath, sessionId, attempt + 1);
|
|
10493
10922
|
if (!again.started) ask.emit("assistant.done", { askId: ask.askId, ok: false, result: again.detail ?? "The assistant could not be started." });
|
|
10494
10923
|
}, 1500 * attempt + Math.floor(Math.random() * 1e3)).unref?.();
|
|
@@ -10497,22 +10926,22 @@ function createClaudeCli(options) {
|
|
|
10497
10926
|
ask.emit("assistant.done", { askId: ask.askId, ok: false, result: "Flowriki's tools did not connect to the assistant on this computer after three tries, so nothing was done. Send it again in a moment; if it keeps happening, restart the agent." });
|
|
10498
10927
|
return;
|
|
10499
10928
|
}
|
|
10500
|
-
if (silentFailure && sessionId && /no conversation found/i.test(stderr) &&
|
|
10929
|
+
if (silentFailure && sessionId && /no conversation found/i.test(stderr) && !stopped) {
|
|
10501
10930
|
if (ask.platform) writeLane(dir, ask.platform, null);
|
|
10502
10931
|
const again = launch(ask, dir, mcpConfigPath, void 0);
|
|
10503
10932
|
if (again.started) return;
|
|
10504
10933
|
}
|
|
10505
10934
|
if (ask.platform && session && ok && !silentFailure) {
|
|
10506
|
-
|
|
10507
|
-
writeLane(dir, ask.platform, { sessionId: session, updatedAt: Date.now(), tokens: Math.max(before, spent) });
|
|
10935
|
+
writeLane(dir, ask.platform, { sessionId: session, updatedAt: Date.now(), tokens: lastContext || spent, kind: usageKindOf(ask.prompt) });
|
|
10508
10936
|
}
|
|
10509
10937
|
if (!sawDone) {
|
|
10510
10938
|
const why = detail || stderr.trim().split("\n").filter((l) => l && !/^Warning:/.test(l)).slice(-3).join(" ") || "The assistant stopped without saying why.";
|
|
10511
10939
|
ask.emit("assistant.done", { askId: ask.askId, ok: ok && !silentFailure, result: ok && !silentFailure ? "" : why });
|
|
10512
10940
|
}
|
|
10941
|
+
cancelled.delete(ask.askId);
|
|
10513
10942
|
};
|
|
10514
10943
|
const timer = setTimeout(() => {
|
|
10515
|
-
child
|
|
10944
|
+
stop(child);
|
|
10516
10945
|
end(false, `Stopped after ${Math.max(1, Math.round(maxAskMs / 6e4))} minutes. Whatever it saved so far is on the Branches page \u2014 send \u201Ccontinue\u201D to carry on.`);
|
|
10517
10946
|
}, maxAskMs);
|
|
10518
10947
|
timer.unref?.();
|
|
@@ -10520,7 +10949,12 @@ function createClaudeCli(options) {
|
|
|
10520
10949
|
buffer += chunk.toString();
|
|
10521
10950
|
let at;
|
|
10522
10951
|
while ((at = buffer.indexOf("\n")) >= 0) {
|
|
10523
|
-
|
|
10952
|
+
try {
|
|
10953
|
+
handleLine(buffer.slice(0, at));
|
|
10954
|
+
} catch (error) {
|
|
10955
|
+
stderr = `${stderr}
|
|
10956
|
+
${error.message}`.slice(-2e3);
|
|
10957
|
+
}
|
|
10524
10958
|
buffer = buffer.slice(at + 1);
|
|
10525
10959
|
}
|
|
10526
10960
|
});
|
|
@@ -10532,7 +10966,7 @@ function createClaudeCli(options) {
|
|
|
10532
10966
|
"close",
|
|
10533
10967
|
(code) => end(
|
|
10534
10968
|
code === 0,
|
|
10535
|
-
cancelled
|
|
10969
|
+
cancelled.has(ask.askId) ? "Stopped." : code === 0 ? "" : stderr.trim().split("\n").filter((l) => l && !/^Warning:/.test(l)).slice(-3).join(" ") || `The assistant stopped (exit ${code ?? "?"}).`
|
|
10536
10970
|
)
|
|
10537
10971
|
);
|
|
10538
10972
|
return { started: true };
|
|
@@ -10868,8 +11302,12 @@ async function start(options = {}) {
|
|
|
10868
11302
|
if (stopped) return;
|
|
10869
11303
|
stopped = true;
|
|
10870
11304
|
link.stop();
|
|
10871
|
-
|
|
10872
|
-
|
|
11305
|
+
try {
|
|
11306
|
+
agent.close();
|
|
11307
|
+
} catch {
|
|
11308
|
+
} finally {
|
|
11309
|
+
lock.lock.release();
|
|
11310
|
+
}
|
|
10873
11311
|
}
|
|
10874
11312
|
};
|
|
10875
11313
|
}
|
|
@@ -10968,6 +11406,8 @@ export {
|
|
|
10968
11406
|
executeStep,
|
|
10969
11407
|
replayCase,
|
|
10970
11408
|
fieldNames,
|
|
11409
|
+
backupsFor,
|
|
11410
|
+
withBackups,
|
|
10971
11411
|
flowrikiHome,
|
|
10972
11412
|
helperRunner,
|
|
10973
11413
|
resolveCheckouts,
|