@flowriki/agent 0.1.0-beta.64 → 0.1.0-beta.66
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.
|
@@ -28,6 +28,8 @@ import {
|
|
|
28
28
|
adjacentLabelPattern,
|
|
29
29
|
caseDebugPlan,
|
|
30
30
|
checkMobileAssertion,
|
|
31
|
+
commandFor,
|
|
32
|
+
commandSystemOf,
|
|
31
33
|
describeAssertion,
|
|
32
34
|
describeLocators,
|
|
33
35
|
expandForRun,
|
|
@@ -66,9 +68,10 @@ import {
|
|
|
66
68
|
stepRefusedOn,
|
|
67
69
|
targetFor,
|
|
68
70
|
textIndependent,
|
|
71
|
+
usageKindOf,
|
|
69
72
|
validateActors,
|
|
70
73
|
webCloudOf
|
|
71
|
-
} from "./chunk-
|
|
74
|
+
} from "./chunk-YD7F7JMA.js";
|
|
72
75
|
|
|
73
76
|
// src/engine/machine.ts
|
|
74
77
|
import fs from "fs";
|
|
@@ -76,7 +79,7 @@ import os from "os";
|
|
|
76
79
|
import path from "path";
|
|
77
80
|
import { createRequire } from "module";
|
|
78
81
|
import { spawnSync } from "child_process";
|
|
79
|
-
var AGENT_VERSION = "0.1.0-beta.
|
|
82
|
+
var AGENT_VERSION = "0.1.0-beta.66";
|
|
80
83
|
async function prepareBrowsers() {
|
|
81
84
|
if (browsersReady()) {
|
|
82
85
|
return { ok: true, already: true, message: "chromium is already installed" };
|
|
@@ -136,7 +139,7 @@ async function checkMachine() {
|
|
|
136
139
|
}
|
|
137
140
|
} catch {
|
|
138
141
|
}
|
|
139
|
-
const { mobileDoctor: mobileDoctor2 } = await import("./dist-
|
|
142
|
+
const { mobileDoctor: mobileDoctor2 } = await import("./dist-CUDUSTFO.js");
|
|
140
143
|
return {
|
|
141
144
|
agentVersion: AGENT_VERSION,
|
|
142
145
|
node: process.versions.node,
|
|
@@ -235,7 +238,7 @@ function forgetConnection(service, env = process.env) {
|
|
|
235
238
|
}
|
|
236
239
|
var fingerprint = (secret2) => crypto2.createHash("sha256").update(secret2).digest("hex").slice(0, 12);
|
|
237
240
|
|
|
238
|
-
// ../contracts/dist/chunk-
|
|
241
|
+
// ../contracts/dist/chunk-VTO4SX4J.js
|
|
239
242
|
import { z } from "zod";
|
|
240
243
|
var PROTOCOL_VERSION = 1;
|
|
241
244
|
var PAIRING_CODE_TTL_MS = 5 * 60 * 1e3;
|
|
@@ -288,7 +291,10 @@ var CommandNameSchema = z.enum([
|
|
|
288
291
|
// a recording's — as a picture, for the live mirror. Reading.
|
|
289
292
|
"mobile.peek",
|
|
290
293
|
// What this computer has installed for web and phone testing. Reading.
|
|
291
|
-
"machine.check"
|
|
294
|
+
"machine.check",
|
|
295
|
+
// A helper run HERE — "Try it" on a command helper, which only the
|
|
296
|
+
// tester's computer can run. NOT baseline.
|
|
297
|
+
"helper.run"
|
|
292
298
|
]);
|
|
293
299
|
var EFFECTFUL = /* @__PURE__ */ new Set([
|
|
294
300
|
"record.start",
|
|
@@ -332,7 +338,9 @@ var EFFECTFUL = /* @__PURE__ */ new Set([
|
|
|
332
338
|
"project.claim",
|
|
333
339
|
"run.cancel",
|
|
334
340
|
// A phone session taps and types on somebody's device.
|
|
335
|
-
"mobile.session"
|
|
341
|
+
"mobile.session",
|
|
342
|
+
// Runs a command on somebody's computer.
|
|
343
|
+
"helper.run"
|
|
336
344
|
]);
|
|
337
345
|
var CommandSchema = z.object({
|
|
338
346
|
/** Unique per command. The agent stores it, so a redelivery after a
|
|
@@ -483,7 +491,7 @@ function compareAgentVersions(a, b2) {
|
|
|
483
491
|
}
|
|
484
492
|
return 0;
|
|
485
493
|
}
|
|
486
|
-
var agentUpdateFor = (
|
|
494
|
+
var agentUpdateFor = (running2, latest) => Boolean(running2 && latest && running2 !== "test" && compareAgentVersions(latest, running2) > 0);
|
|
487
495
|
|
|
488
496
|
// src/service.ts
|
|
489
497
|
var BadServiceUrl = class extends Error {
|
|
@@ -551,7 +559,9 @@ async function connect(options = {}) {
|
|
|
551
559
|
const res = await fetch(`${service}/api/pairing-requests`, {
|
|
552
560
|
method: "POST",
|
|
553
561
|
headers: { "content-type": "application/json" },
|
|
554
|
-
|
|
562
|
+
// Ask for a code to show here: the page then insists on it, so a link
|
|
563
|
+
// sent by somebody else's agent cannot be approved by mistake.
|
|
564
|
+
body: JSON.stringify({ machineName, agentVersion: AGENT_VERSION, wantsCode: true }),
|
|
555
565
|
signal: options.signal ?? null
|
|
556
566
|
});
|
|
557
567
|
if (!res.ok) {
|
|
@@ -565,7 +575,7 @@ async function connect(options = {}) {
|
|
|
565
575
|
options.onProgress?.({
|
|
566
576
|
kind: "approve",
|
|
567
577
|
message: "waiting for approval in your browser",
|
|
568
|
-
detail: { url: approve, machineName, canOpen: options.open !== false && canOpenBrowser() }
|
|
578
|
+
detail: { url: approve, machineName, canOpen: options.open !== false && canOpenBrowser(), ...asked.code ? { code: `${asked.code.slice(0, 3)}-${asked.code.slice(3)}` } : {} }
|
|
569
579
|
});
|
|
570
580
|
if (options.open !== false) openBrowser(approve);
|
|
571
581
|
const pollMs = options.pollMs ?? 1500;
|
|
@@ -1840,7 +1850,7 @@ function installDebugPanel() {
|
|
|
1840
1850
|
pendingRender = false;
|
|
1841
1851
|
const p = root.getElementById("p");
|
|
1842
1852
|
p.className = "p" + (vertical ? " v" : "");
|
|
1843
|
-
const
|
|
1853
|
+
const running2 = state.status === "running";
|
|
1844
1854
|
const done = state.status === "finished";
|
|
1845
1855
|
const stCls = state.failure ? "failed" : state.recording ? "running" : state.status;
|
|
1846
1856
|
const stLabel = state.recording ? "recording" : state.failure ? "paused at a failure" : state.status === "idle" ? "not started" : state.status;
|
|
@@ -1933,7 +1943,7 @@ function installDebugPanel() {
|
|
|
1933
1943
|
});
|
|
1934
1944
|
return;
|
|
1935
1945
|
}
|
|
1936
|
-
p.innerHTML = '<div class="bar"><span class="grip" title="Drag to move"><i></i><i></i><i></i></span><span class="st ' + esc(stCls) + '"><span class="d"></span><span>' + esc(stLabel) + '</span><span class="prog"> · ' + ran + "/" + state.steps.length + '</span></span><span class="dv"></span><button id="play" class="primary" title="' + (state.failure ? "Give up on this step and carry on" : "Run until the next breakpoint or failure") + '" ' + (
|
|
1946
|
+
p.innerHTML = '<div class="bar"><span class="grip" title="Drag to move"><i></i><i></i><i></i></span><span class="st ' + esc(stCls) + '"><span class="d"></span><span>' + esc(stLabel) + '</span><span class="prog"> · ' + ran + "/" + state.steps.length + '</span></span><span class="dv"></span><button id="play" class="primary" title="' + (state.failure ? "Give up on this step and carry on" : "Run until the next breakpoint or failure") + '" ' + (running2 || done ? "disabled" : "") + ">" + (state.failure ? "Skip & continue" : "Play") + "</button>" + (state.failure ? '<button id="retry" title="Run this step again">Retry</button>' : "") + '<button id="step" title="Run exactly one step" ' + (running2 || done ? "disabled" : "") + '>Step</button><button id="pause" title="Stop after the step in flight" ' + (running2 ? "" : "disabled") + '>Pause</button><button id="replay" title="Start over from the first step" ' + (running2 ? "disabled" : "") + '>Replay</button><span class="dv"></span>' + // While armed, this is the only way to stop it — so it says so, rather
|
|
1937
1947
|
// than reading as a status label nobody realises is a button.
|
|
1938
1948
|
(state.recording ? '<button id="rec" class="danger" title="Stop capturing"><span class="rec"><span class="dot"></span>Stop recording</span></button>' : '<button id="rec" title="Capture new steps at the cursor">Record</button>') + // AUTHORING A CHECK BELONGS HERE MOST OF ALL. Recording had it and
|
|
1939
1949
|
// debugging did not, which is the wrong way round: the moment worth
|
|
@@ -2130,6 +2140,12 @@ async function performHelperStep(page, step, options) {
|
|
|
2130
2140
|
}
|
|
2131
2141
|
return got;
|
|
2132
2142
|
}
|
|
2143
|
+
var LOAD_CAP_MS = 3e3;
|
|
2144
|
+
var QUIET_CAP_MS = 1e3;
|
|
2145
|
+
async function settleAfterNavigation(page) {
|
|
2146
|
+
await page.waitForLoadState("load", { timeout: LOAD_CAP_MS }).catch(() => void 0);
|
|
2147
|
+
await page.waitForLoadState("networkidle", { timeout: QUIET_CAP_MS }).catch(() => void 0);
|
|
2148
|
+
}
|
|
2133
2149
|
var secretsBehind = (auth) => [.../* @__PURE__ */ new Set([
|
|
2134
2150
|
...Object.values(auth.secretRefs ?? {}),
|
|
2135
2151
|
...auth.steps.flatMap((step) => step.secretRef ? [step.secretRef] : [])
|
|
@@ -2333,7 +2349,10 @@ var RunActors = class {
|
|
|
2333
2349
|
if (switching && this.opts.headless === false) await session.page.bringToFront().catch(() => {
|
|
2334
2350
|
});
|
|
2335
2351
|
if (this.initialized.has(id)) return;
|
|
2336
|
-
if (startUrl)
|
|
2352
|
+
if (startUrl) {
|
|
2353
|
+
await session.page.goto(startUrl, { waitUntil: "domcontentloaded" });
|
|
2354
|
+
await settleAfterNavigation(session.page);
|
|
2355
|
+
}
|
|
2337
2356
|
if (actor?.startSignedIn) {
|
|
2338
2357
|
if (kept?.state) {
|
|
2339
2358
|
const confirmed = await confirmSignedIn(session.page, kept.auth, this.opts.reuseCheckMs ?? 3e3);
|
|
@@ -2345,7 +2364,10 @@ var RunActors = class {
|
|
|
2345
2364
|
this.opts.reuse?.cache.evict(kept.key);
|
|
2346
2365
|
this.preparations.set(id, "stale");
|
|
2347
2366
|
session = await this.replaceWithGuest(id, session);
|
|
2348
|
-
|
|
2367
|
+
{
|
|
2368
|
+
await session.page.goto(startUrl, { waitUntil: "domcontentloaded" });
|
|
2369
|
+
await settleAfterNavigation(session.page);
|
|
2370
|
+
}
|
|
2349
2371
|
}
|
|
2350
2372
|
await executeStep(
|
|
2351
2373
|
session.page,
|
|
@@ -2377,7 +2399,10 @@ var RunActors = class {
|
|
|
2377
2399
|
} catch {
|
|
2378
2400
|
}
|
|
2379
2401
|
}
|
|
2380
|
-
if (startUrl)
|
|
2402
|
+
if (startUrl) {
|
|
2403
|
+
await session.page.goto(startUrl, { waitUntil: "domcontentloaded" });
|
|
2404
|
+
await settleAfterNavigation(session.page);
|
|
2405
|
+
}
|
|
2381
2406
|
}
|
|
2382
2407
|
this.initialized.add(id);
|
|
2383
2408
|
}
|
|
@@ -2456,9 +2481,9 @@ var RunActors = class {
|
|
|
2456
2481
|
const previous = this.sessions.get(id);
|
|
2457
2482
|
const round = (this.resets.get(id) ?? 0) + 1;
|
|
2458
2483
|
this.resets.set(id, round);
|
|
2459
|
-
const
|
|
2460
|
-
const session = await openRunSession({ ...this.opts, browser: this.main.browser, ...this.evidenceIn(
|
|
2461
|
-
this.homes.set(session,
|
|
2484
|
+
const home2 = `actor-${id}-${round}`;
|
|
2485
|
+
const session = await openRunSession({ ...this.opts, browser: this.main.browser, ...this.evidenceIn(home2) });
|
|
2486
|
+
this.homes.set(session, home2);
|
|
2462
2487
|
this.sessions.set(id, session);
|
|
2463
2488
|
this.current = session;
|
|
2464
2489
|
this.currentId = id;
|
|
@@ -2474,7 +2499,10 @@ var RunActors = class {
|
|
|
2474
2499
|
this.retired.add(previous);
|
|
2475
2500
|
}
|
|
2476
2501
|
await this.opts.onCreate?.(session);
|
|
2477
|
-
|
|
2502
|
+
{
|
|
2503
|
+
await session.page.goto(this.opts.startUrl, { waitUntil: "domcontentloaded" });
|
|
2504
|
+
await settleAfterNavigation(session.page);
|
|
2505
|
+
}
|
|
2478
2506
|
this.initialized.add(id);
|
|
2479
2507
|
}
|
|
2480
2508
|
/** Back to one participant, between debug replays.
|
|
@@ -2805,7 +2833,7 @@ var ExecutionPages = class {
|
|
|
2805
2833
|
await Promise.all(this.dialogTasks);
|
|
2806
2834
|
if (this.unexpected) throw new Error(this.unexpected);
|
|
2807
2835
|
if (this.expectedDialogs.length) throw new Error("The expected browser dialog did not appear");
|
|
2808
|
-
if (step.downloads &&
|
|
2836
|
+
if (step.downloads && !downloadsMatch(step.downloads, downloads)) throw new Error(`Expected downloads ${step.downloads.join(", ")}; received ${downloads.join(", ") || "none"}`);
|
|
2809
2837
|
} finally {
|
|
2810
2838
|
page.off("download", onDownload);
|
|
2811
2839
|
this.expectedDialogs = [];
|
|
@@ -2820,6 +2848,17 @@ var ExecutionPages = class {
|
|
|
2820
2848
|
this.unexpected = null;
|
|
2821
2849
|
}
|
|
2822
2850
|
};
|
|
2851
|
+
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, "#");
|
|
2852
|
+
function downloadsMatch(expected, received) {
|
|
2853
|
+
if (expected.length !== received.length) return false;
|
|
2854
|
+
const left = received.map(shapeOf);
|
|
2855
|
+
for (const name of expected) {
|
|
2856
|
+
const at = left.indexOf(shapeOf(name));
|
|
2857
|
+
if (at === -1) return false;
|
|
2858
|
+
left.splice(at, 1);
|
|
2859
|
+
}
|
|
2860
|
+
return true;
|
|
2861
|
+
}
|
|
2823
2862
|
function rootFor(page, step) {
|
|
2824
2863
|
let root = page;
|
|
2825
2864
|
for (const frame of step.frame ?? []) root = root.frameLocator(frameSelector(frame));
|
|
@@ -2899,17 +2938,22 @@ async function scrollUntilFound(page, loc, budgetMs = 3e3) {
|
|
|
2899
2938
|
for (let pass = 0; pass < PASSES && Date.now() < giveUpAt; pass++) {
|
|
2900
2939
|
if (await loc.count() > 0) return true;
|
|
2901
2940
|
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) {
|
|
2941
|
+
const nudge = (el) => {
|
|
2908
2942
|
const before = el.scrollTop;
|
|
2909
2943
|
el.scrollTop = before + Math.max(80, el.clientHeight * 0.8);
|
|
2910
|
-
|
|
2911
|
-
}
|
|
2912
|
-
|
|
2944
|
+
return el.scrollTop !== before;
|
|
2945
|
+
};
|
|
2946
|
+
const page2 = document.scrollingElement;
|
|
2947
|
+
if (page2 && page2.scrollHeight > page2.clientHeight + 4 && nudge(page2)) return true;
|
|
2948
|
+
const areas = Array.from(document.querySelectorAll("*")).filter((el) => el.scrollHeight > el.clientHeight + 4 && el.clientHeight > 40).map((el) => {
|
|
2949
|
+
const box = el.getBoundingClientRect();
|
|
2950
|
+
const style = getComputedStyle(el);
|
|
2951
|
+
const scrolls = /(auto|scroll|overlay)/.test(style.overflowY);
|
|
2952
|
+
const shown = box.width > 0 && box.height > 0 && box.bottom > 0 && box.top < innerHeight && style.visibility !== "hidden";
|
|
2953
|
+
return { el, area: scrolls && shown ? box.width * box.height : 0 };
|
|
2954
|
+
}).filter((each) => each.area > 0).sort((a, b2) => b2.area - a.area);
|
|
2955
|
+
for (const { el } of areas) if (nudge(el)) return true;
|
|
2956
|
+
return false;
|
|
2913
2957
|
}).catch(() => false);
|
|
2914
2958
|
if (!moved) break;
|
|
2915
2959
|
await page.waitForTimeout(120);
|
|
@@ -3422,6 +3466,7 @@ async function runStepsIn(session, steps, opts) {
|
|
|
3422
3466
|
try {
|
|
3423
3467
|
if (opts.startUrl && !opensItsOwnEntry(steps, opts)) {
|
|
3424
3468
|
await page.goto(opts.startUrl, { waitUntil: "domcontentloaded" });
|
|
3469
|
+
await settleAfterNavigation(page);
|
|
3425
3470
|
}
|
|
3426
3471
|
for (const recorded of steps) {
|
|
3427
3472
|
let step = recorded;
|
|
@@ -3625,8 +3670,9 @@ async function runStepsIn(session, steps, opts) {
|
|
|
3625
3670
|
// SAID OUT LOUD, on a step that passed. The element was found, but
|
|
3626
3671
|
// not the way this step says it should be — something has moved,
|
|
3627
3672
|
// and the run is the only place anybody would learn it.
|
|
3628
|
-
onFallback: (_step, foundBy) => {
|
|
3673
|
+
onFallback: (_step, foundBy, renamedTo) => {
|
|
3629
3674
|
result.foundBy = foundBy;
|
|
3675
|
+
if (renamedTo) result.renamedTo = renamedTo;
|
|
3630
3676
|
}
|
|
3631
3677
|
});
|
|
3632
3678
|
try {
|
|
@@ -3815,6 +3861,7 @@ async function executeStep(main, input, options = {}) {
|
|
|
3815
3861
|
const remaining = () => Math.max(1, deadline - Date.now());
|
|
3816
3862
|
const navigationBudget = () => step.navigationTimeoutMs ?? options.navigationTimeoutMs ?? 45e3;
|
|
3817
3863
|
page.setDefaultTimeout(remaining());
|
|
3864
|
+
page.setDefaultNavigationTimeout(navigationBudget());
|
|
3818
3865
|
const act = async () => {
|
|
3819
3866
|
if (step.type === "unsupported") throw new Error(step.warning || step.value || "This interaction requires a supported action before replay");
|
|
3820
3867
|
if (step.type === "assert") {
|
|
@@ -3823,6 +3870,7 @@ async function executeStep(main, input, options = {}) {
|
|
|
3823
3870
|
}
|
|
3824
3871
|
if (step.type === "navigate") {
|
|
3825
3872
|
await page.goto(new URL(step.value, step.navigationBase === "application" ? options.baseUrl ?? page.url() : page.url()).href, { waitUntil: "domcontentloaded", timeout: navigationBudget() });
|
|
3873
|
+
await settleAfterNavigation(page);
|
|
3826
3874
|
return;
|
|
3827
3875
|
}
|
|
3828
3876
|
if (step.type === "closePage") {
|
|
@@ -3852,7 +3900,7 @@ async function executeStep(main, input, options = {}) {
|
|
|
3852
3900
|
}
|
|
3853
3901
|
const target = await ensureTarget(page, step, remaining());
|
|
3854
3902
|
const loc = target.loc;
|
|
3855
|
-
if (target.foundBy) options.onFallback?.(step, target.foundBy);
|
|
3903
|
+
if (target.foundBy) options.onFallback?.(step, target.foundBy, target.renamedTo);
|
|
3856
3904
|
page.setDefaultTimeout(remaining());
|
|
3857
3905
|
await applyAction(loc, step, value, { resolveFile: options.resolveFile, dropTarget: await locateDrop(page, step) });
|
|
3858
3906
|
};
|
|
@@ -3904,6 +3952,26 @@ async function applyScroll(page, step, timeout) {
|
|
|
3904
3952
|
} catch {
|
|
3905
3953
|
}
|
|
3906
3954
|
}
|
|
3955
|
+
async function renamedFrom(page, step, found) {
|
|
3956
|
+
if (!step.role || step.locator) return void 0;
|
|
3957
|
+
try {
|
|
3958
|
+
const first = (await found.ariaSnapshot({ timeout: 1e3 })).split("\n")[0] ?? "";
|
|
3959
|
+
const parsed = /^- ([a-z]+) "((?:[^"\\]|\\.)*)"/.exec(first);
|
|
3960
|
+
if (!parsed || parsed[1] !== step.role) return void 0;
|
|
3961
|
+
const name = JSON.parse(`"${parsed[2]}"`).trim();
|
|
3962
|
+
if (!name || name === step.name || name.length > 200) return void 0;
|
|
3963
|
+
const { nameFrom: _from, candidates: _backups, ...rest } = step;
|
|
3964
|
+
const [again] = await locateCandidates(page, { ...rest, name });
|
|
3965
|
+
if (!again || await again.count() !== 1) return void 0;
|
|
3966
|
+
const handle = await found.elementHandle({ timeout: 1e3 });
|
|
3967
|
+
const same = handle ? await again.evaluate((el, other) => el === other, handle) : false;
|
|
3968
|
+
await handle?.dispose().catch(() => {
|
|
3969
|
+
});
|
|
3970
|
+
return same ? name : void 0;
|
|
3971
|
+
} catch {
|
|
3972
|
+
return void 0;
|
|
3973
|
+
}
|
|
3974
|
+
}
|
|
3907
3975
|
async function ensureTarget(page, step, timeout) {
|
|
3908
3976
|
const candidates = await locateCandidates(page, step);
|
|
3909
3977
|
const described = describeLocators(step);
|
|
@@ -3920,7 +3988,10 @@ async function ensureTarget(page, step, timeout) {
|
|
|
3920
3988
|
let crowded = null;
|
|
3921
3989
|
for (let at = 1; at < candidates.length; at += 1) {
|
|
3922
3990
|
const found = await candidates[at].count().catch(() => 0);
|
|
3923
|
-
if (found === 1)
|
|
3991
|
+
if (found === 1) {
|
|
3992
|
+
const renamedTo = await renamedFrom(page, step, candidates[at]);
|
|
3993
|
+
return { loc: candidates[at], foundBy: described[at] ?? "another locator", ...renamedTo ? { renamedTo } : {} };
|
|
3994
|
+
}
|
|
3924
3995
|
if (found > 1 && !crowded) crowded = { where: described[at] ?? "another locator", count: found };
|
|
3925
3996
|
}
|
|
3926
3997
|
if (several) throw Object.assign(several, { kind: "ambiguous" });
|
|
@@ -5261,6 +5332,45 @@ async function fieldNames(page) {
|
|
|
5261
5332
|
return found;
|
|
5262
5333
|
}).catch(() => []);
|
|
5263
5334
|
}
|
|
5335
|
+
async function backupsFor(page, step) {
|
|
5336
|
+
if (!step.role || !step.name || step.locator) return [];
|
|
5337
|
+
try {
|
|
5338
|
+
const [primary] = await locateCandidates(page, step);
|
|
5339
|
+
if (!primary || await primary.count() !== 1) return [];
|
|
5340
|
+
return await primary.evaluate((el) => {
|
|
5341
|
+
const clean = (s2) => (s2 ?? "").replace(/\s+/g, " ").trim();
|
|
5342
|
+
const root = el.getRootNode();
|
|
5343
|
+
const unique = (selector) => {
|
|
5344
|
+
try {
|
|
5345
|
+
return root.querySelectorAll(selector).length === 1;
|
|
5346
|
+
} catch {
|
|
5347
|
+
return false;
|
|
5348
|
+
}
|
|
5349
|
+
};
|
|
5350
|
+
const found = [];
|
|
5351
|
+
const testId = el.getAttribute("data-testid");
|
|
5352
|
+
if (testId && unique(`[data-testid=${JSON.stringify(testId)}]`)) found.push({ strategy: "testId", value: testId });
|
|
5353
|
+
const placeholder = el.getAttribute("placeholder");
|
|
5354
|
+
if (clean(placeholder) && unique(`[placeholder=${JSON.stringify(placeholder)}]`)) found.push({ strategy: "placeholder", value: clean(placeholder) });
|
|
5355
|
+
const alt = el.getAttribute("alt");
|
|
5356
|
+
if (clean(alt) && unique(`[alt=${JSON.stringify(alt)}]`)) found.push({ strategy: "altText", value: clean(alt) });
|
|
5357
|
+
const title = el.getAttribute("title");
|
|
5358
|
+
if (clean(title) && unique(`[title=${JSON.stringify(title)}]`)) found.push({ strategy: "title", value: clean(title) });
|
|
5359
|
+
if (el.id && !/:r|[a-f0-9]{12}|\d{6}/i.test(el.id) && unique(`#${CSS.escape(el.id)}`)) {
|
|
5360
|
+
found.push({ strategy: "css", value: `#${CSS.escape(el.id)}` });
|
|
5361
|
+
}
|
|
5362
|
+
return found.slice(0, 4);
|
|
5363
|
+
}, void 0, { timeout: 1e3 });
|
|
5364
|
+
} catch {
|
|
5365
|
+
return [];
|
|
5366
|
+
}
|
|
5367
|
+
}
|
|
5368
|
+
function withBackups(target, backups) {
|
|
5369
|
+
if (!backups.length) return target;
|
|
5370
|
+
const seen = new Set((target.candidates ?? []).map((c) => `${c.strategy}\0${c.value}`));
|
|
5371
|
+
const merged = [...target.candidates ?? [], ...backups.filter((b2) => !seen.has(`${b2.strategy}\0${b2.value}`))].slice(0, 4);
|
|
5372
|
+
return { ...target, candidates: merged };
|
|
5373
|
+
}
|
|
5264
5374
|
var playwrightVersion2 = () => {
|
|
5265
5375
|
try {
|
|
5266
5376
|
return createRequire22(import.meta.url)("playwright/package.json").version;
|
|
@@ -6106,8 +6216,18 @@ var SqliteStorage = class {
|
|
|
6106
6216
|
) AS rn FROM run
|
|
6107
6217
|
) WHERE rn > ?`
|
|
6108
6218
|
).all(keep).map((r) => String(r.id));
|
|
6219
|
+
const dirOf = this.handle.prepare("SELECT artifacts_dir FROM run WHERE id=?");
|
|
6109
6220
|
const stmt = this.handle.prepare("DELETE FROM run WHERE id=?");
|
|
6110
|
-
for (const id of doomed)
|
|
6221
|
+
for (const id of doomed) {
|
|
6222
|
+
const dir = dirOf.get(id)?.artifacts_dir;
|
|
6223
|
+
stmt.run(id);
|
|
6224
|
+
if (typeof dir === "string" && dir) {
|
|
6225
|
+
try {
|
|
6226
|
+
fs5.rmSync(dir, { recursive: true, force: true });
|
|
6227
|
+
} catch {
|
|
6228
|
+
}
|
|
6229
|
+
}
|
|
6230
|
+
}
|
|
6111
6231
|
return doomed;
|
|
6112
6232
|
});
|
|
6113
6233
|
}
|
|
@@ -6912,7 +7032,7 @@ function ensureRootCommit(root) {
|
|
|
6912
7032
|
}
|
|
6913
7033
|
|
|
6914
7034
|
// ../helpers/dist/index.js
|
|
6915
|
-
import { createHmac as createHmac2 } from "crypto";
|
|
7035
|
+
import { createHash, createHmac as createHmac2 } from "crypto";
|
|
6916
7036
|
var HelperFailure = class extends Error {
|
|
6917
7037
|
constructor(message) {
|
|
6918
7038
|
super(message);
|
|
@@ -6978,11 +7098,12 @@ function totp(secretBase32, at, options = {}) {
|
|
|
6978
7098
|
const remaining = period - Math.floor(at / 1e3 % period);
|
|
6979
7099
|
return { code, remaining };
|
|
6980
7100
|
}
|
|
6981
|
-
async function imapMailbox(config, password) {
|
|
7101
|
+
async function imapMailbox(config, password, target = { host: config.host }) {
|
|
6982
7102
|
const { ImapFlow } = await import("imapflow");
|
|
6983
7103
|
const { simpleParser } = await import("mailparser");
|
|
6984
7104
|
const client = new ImapFlow({
|
|
6985
|
-
host:
|
|
7105
|
+
host: target.host,
|
|
7106
|
+
...target.servername ? { servername: target.servername } : {},
|
|
6986
7107
|
port: config.port,
|
|
6987
7108
|
secure: config.secure,
|
|
6988
7109
|
auth: { user: config.username, pass: password },
|
|
@@ -7041,7 +7162,8 @@ async function runMailbox(config, ctx) {
|
|
|
7041
7162
|
subject: fill(config.match.subject, ctx.inputs),
|
|
7042
7163
|
since: ctx.since ?? null
|
|
7043
7164
|
};
|
|
7044
|
-
const
|
|
7165
|
+
const password = secret(ctx, config.passwordRef, "mailbox password");
|
|
7166
|
+
const mailbox = ctx.openMailbox ? await ctx.openMailbox(config, password) : await imapMailbox(config, password, ctx.connectTo ? await ctx.connectTo(config.host) : void 0);
|
|
7045
7167
|
const deadline = now() + config.waitSeconds * 1e3;
|
|
7046
7168
|
try {
|
|
7047
7169
|
for (; ; ) {
|
|
@@ -7164,6 +7286,82 @@ async function runHttp(config, ctx) {
|
|
|
7164
7286
|
}
|
|
7165
7287
|
}
|
|
7166
7288
|
}
|
|
7289
|
+
function commandFingerprint(config, system) {
|
|
7290
|
+
const line = commandFor(config, system);
|
|
7291
|
+
const written = JSON.stringify({
|
|
7292
|
+
program: line.program,
|
|
7293
|
+
args: line.args,
|
|
7294
|
+
folder: config.folder,
|
|
7295
|
+
env: config.env,
|
|
7296
|
+
secretEnv: Object.keys(config.secretEnv).sort(),
|
|
7297
|
+
mode: config.mode,
|
|
7298
|
+
readyPattern: config.readyPattern
|
|
7299
|
+
});
|
|
7300
|
+
return createHash("sha256").update(written).digest("hex").slice(0, 16);
|
|
7301
|
+
}
|
|
7302
|
+
var lastLine = (text) => text.trim().split(/\r?\n/).filter(Boolean).pop() ?? "";
|
|
7303
|
+
async function runCommand(config, ctx, name) {
|
|
7304
|
+
if (!ctx.commands) {
|
|
7305
|
+
throw new HelperFailure("this helper runs a command on a tester's computer \u2014 try it, and run tests that use it, from a computer running the Flowriki agent");
|
|
7306
|
+
}
|
|
7307
|
+
const system = ctx.system ?? commandSystemOf(process.platform);
|
|
7308
|
+
const line = commandFor(config, system);
|
|
7309
|
+
const env = {};
|
|
7310
|
+
for (const [key, value] of Object.entries(config.env)) env[key] = fill(value, ctx.inputs);
|
|
7311
|
+
const redact2 = [];
|
|
7312
|
+
for (const [key, ref] of Object.entries(config.secretEnv)) {
|
|
7313
|
+
const value = secret(ctx, ref, `${key} secret`);
|
|
7314
|
+
env[key] = value;
|
|
7315
|
+
redact2.push(value);
|
|
7316
|
+
}
|
|
7317
|
+
const outcome = await ctx.commands.run({
|
|
7318
|
+
name,
|
|
7319
|
+
fingerprint: commandFingerprint(config, system),
|
|
7320
|
+
program: fill(line.program, ctx.inputs),
|
|
7321
|
+
// EACH ARGUMENT FILLED ON ITS OWN: a value is never split into two, and
|
|
7322
|
+
// never read by a shell.
|
|
7323
|
+
args: line.args.map((arg) => fill(arg, ctx.inputs)),
|
|
7324
|
+
folder: fill(config.folder, ctx.inputs),
|
|
7325
|
+
env,
|
|
7326
|
+
redact: redact2,
|
|
7327
|
+
mode: config.mode,
|
|
7328
|
+
readyPattern: config.readyPattern,
|
|
7329
|
+
timeoutMs: config.timeoutSeconds * 1e3
|
|
7330
|
+
});
|
|
7331
|
+
const evidence = {
|
|
7332
|
+
...outcome.exitCode === null ? { state: config.mode === "background" ? "running until the test ends" : "stopped" } : { "exit code": String(outcome.exitCode) },
|
|
7333
|
+
...lastLine(outcome.stdout) ? { "last line": lastLine(outcome.stdout).slice(0, 200) } : {}
|
|
7334
|
+
};
|
|
7335
|
+
if (config.mode === "run" && outcome.exitCode !== config.expectExit) {
|
|
7336
|
+
const why = lastLine(outcome.stderr) || lastLine(outcome.stdout);
|
|
7337
|
+
throw new HelperFailure(`the command ended with exit code ${outcome.exitCode ?? "?"}${config.expectExit ? ` (expected ${config.expectExit})` : ""}${why ? `: ${why.slice(0, 300)}` : ""}`);
|
|
7338
|
+
}
|
|
7339
|
+
const text = outcome.stdout;
|
|
7340
|
+
switch (config.extract.take) {
|
|
7341
|
+
case "nothing":
|
|
7342
|
+
return { value: "", detail: config.mode === "background" ? "started, and runs until the test ends" : "it ran", evidence };
|
|
7343
|
+
case "output": {
|
|
7344
|
+
const value = text.trim();
|
|
7345
|
+
return { value: value.length > 2e3 ? lastLine(value) : value, detail: "what the command printed", evidence };
|
|
7346
|
+
}
|
|
7347
|
+
case "pattern": {
|
|
7348
|
+
const value = takeCode(text, config.extract.pattern);
|
|
7349
|
+
if (value === null) throw new HelperFailure(`the command printed nothing matching ${config.extract.pattern}`);
|
|
7350
|
+
return { value, detail: "the match in what the command printed", evidence };
|
|
7351
|
+
}
|
|
7352
|
+
case "json": {
|
|
7353
|
+
let parsed;
|
|
7354
|
+
try {
|
|
7355
|
+
parsed = JSON.parse(text);
|
|
7356
|
+
} catch {
|
|
7357
|
+
throw new HelperFailure("the command did not print JSON");
|
|
7358
|
+
}
|
|
7359
|
+
const value = atPath(parsed, config.extract.path);
|
|
7360
|
+
if (value === void 0 || value === null) throw new HelperFailure(`what the command printed has no "${config.extract.path}"`);
|
|
7361
|
+
return { value: typeof value === "string" ? value : JSON.stringify(value), detail: `"${config.extract.path}" from what the command printed`, evidence };
|
|
7362
|
+
}
|
|
7363
|
+
}
|
|
7364
|
+
}
|
|
7167
7365
|
async function runHelper(helper, ctx) {
|
|
7168
7366
|
const config = "config" in helper ? helper.config : helper;
|
|
7169
7367
|
switch (config.kind) {
|
|
@@ -7177,11 +7375,17 @@ async function runHelper(helper, ctx) {
|
|
|
7177
7375
|
return runSms(config, ctx);
|
|
7178
7376
|
case "http":
|
|
7179
7377
|
return runHttp(config, ctx);
|
|
7378
|
+
case "command":
|
|
7379
|
+
return runCommand(config, ctx, "name" in helper ? helper.name : "this command");
|
|
7180
7380
|
}
|
|
7181
7381
|
}
|
|
7182
7382
|
|
|
7183
7383
|
// ../execution/dist/index.js
|
|
7384
|
+
import { spawn as spawn2 } from "child_process";
|
|
7184
7385
|
import fs24 from "fs";
|
|
7386
|
+
import os5 from "os";
|
|
7387
|
+
import path24 from "path";
|
|
7388
|
+
import fs34 from "fs";
|
|
7185
7389
|
import crypto3 from "crypto";
|
|
7186
7390
|
async function cloudBrowserFor(config, secrets, name) {
|
|
7187
7391
|
const settings = config ?? {};
|
|
@@ -7195,6 +7399,7 @@ async function cloudBrowserFor(config, secrets, name) {
|
|
|
7195
7399
|
return connectCloudBrowser(account, key, choice, name);
|
|
7196
7400
|
}
|
|
7197
7401
|
var LOCAL_CHROMIUM = { id: "chromium", provider: "local", engine: "chromium" };
|
|
7402
|
+
var KEEP_RUNS_LOCALLY = 50;
|
|
7198
7403
|
async function executeFlow(opts) {
|
|
7199
7404
|
const { store, storage = null, flowId, trigger, watch = false, onEvent } = opts;
|
|
7200
7405
|
const headless = watch ? false : opts.headless ?? store.readConfig().headless;
|
|
@@ -7354,6 +7559,7 @@ async function executeFlow(opts) {
|
|
|
7354
7559
|
writes.push(emit({ type: "run.step", runId: run?.id ?? "", flowId: flow.id, result }));
|
|
7355
7560
|
}
|
|
7356
7561
|
});
|
|
7562
|
+
store.runHelper?.close?.();
|
|
7357
7563
|
if (storage && taskId && run) {
|
|
7358
7564
|
const reached = new Set(outcome.steps.map((r) => r.stepId));
|
|
7359
7565
|
for (const step of flow.steps) {
|
|
@@ -7431,6 +7637,7 @@ async function executeFlow(opts) {
|
|
|
7431
7637
|
artifactsDir: runDir && fs7.existsSync(runDir) ? runDir : null
|
|
7432
7638
|
});
|
|
7433
7639
|
}
|
|
7640
|
+
if (storage) void storage.pruneRuns(KEEP_RUNS_LOCALLY).catch(() => void 0);
|
|
7434
7641
|
await emit({ type: "run.finished", runId: run?.id ?? "", flowId: flow.id, outcome: last });
|
|
7435
7642
|
return {
|
|
7436
7643
|
outcome: last,
|
|
@@ -7518,27 +7725,203 @@ function toStepRow(ordinal, result, step) {
|
|
|
7518
7725
|
checks
|
|
7519
7726
|
};
|
|
7520
7727
|
}
|
|
7728
|
+
var home = () => process.env["FLOWRIKI_HOME"] || path24.join(os5.homedir(), ".flowriki");
|
|
7729
|
+
var approvalsFile = () => path24.join(home(), "allowed-commands.json");
|
|
7730
|
+
function readApprovals() {
|
|
7731
|
+
try {
|
|
7732
|
+
const parsed = JSON.parse(fs24.readFileSync(approvalsFile(), "utf8"));
|
|
7733
|
+
return { allowed: parsed.allowed ?? {}, waiting: parsed.waiting ?? {} };
|
|
7734
|
+
} catch {
|
|
7735
|
+
return { allowed: {}, waiting: {} };
|
|
7736
|
+
}
|
|
7737
|
+
}
|
|
7738
|
+
function writeApprovals(approvals) {
|
|
7739
|
+
fs24.mkdirSync(home(), { recursive: true, mode: 448 });
|
|
7740
|
+
const temporary = `${approvalsFile()}.${process.pid}.tmp`;
|
|
7741
|
+
fs24.writeFileSync(temporary, JSON.stringify(approvals, null, 2), { mode: 384 });
|
|
7742
|
+
fs24.renameSync(temporary, approvalsFile());
|
|
7743
|
+
}
|
|
7744
|
+
function allowCommands(which) {
|
|
7745
|
+
const approvals = readApprovals();
|
|
7746
|
+
const wanted = which.trim().toLowerCase();
|
|
7747
|
+
const chosen = Object.values(approvals.waiting).filter((c) => wanted === "all" || c.fingerprint === wanted || c.name.toLowerCase() === wanted);
|
|
7748
|
+
for (const command of chosen) {
|
|
7749
|
+
approvals.allowed[command.fingerprint] = { ...command, at: (/* @__PURE__ */ new Date()).toISOString() };
|
|
7750
|
+
delete approvals.waiting[command.fingerprint];
|
|
7751
|
+
}
|
|
7752
|
+
if (chosen.length) writeApprovals(approvals);
|
|
7753
|
+
return chosen;
|
|
7754
|
+
}
|
|
7755
|
+
function revokeCommands(which) {
|
|
7756
|
+
const approvals = readApprovals();
|
|
7757
|
+
const wanted = which.trim().toLowerCase();
|
|
7758
|
+
const chosen = Object.values(approvals.allowed).filter((c) => wanted === "all" || c.fingerprint === wanted || c.name.toLowerCase() === wanted);
|
|
7759
|
+
for (const command of chosen) delete approvals.allowed[command.fingerprint];
|
|
7760
|
+
if (chosen.length) writeApprovals(approvals);
|
|
7761
|
+
return chosen;
|
|
7762
|
+
}
|
|
7763
|
+
var CommandNotAllowed = class extends HelperFailure {
|
|
7764
|
+
constructor(command) {
|
|
7765
|
+
super(`"${command.name}" has not been allowed on this computer yet. In a terminal here, run: flowriki-agent allow "${command.name}" \u2014 it shows the command before allowing it.`);
|
|
7766
|
+
this.command = command;
|
|
7767
|
+
this.name = "CommandNotAllowed";
|
|
7768
|
+
}
|
|
7769
|
+
command;
|
|
7770
|
+
code = "command_not_allowed";
|
|
7771
|
+
};
|
|
7772
|
+
var KEEP_BYTES = 64 * 1024;
|
|
7773
|
+
var BACKGROUND_LIMIT_MS = 30 * 6e4;
|
|
7774
|
+
var SETTLE_MS = 2e3;
|
|
7775
|
+
var running = /* @__PURE__ */ new Set();
|
|
7776
|
+
process.once("exit", () => {
|
|
7777
|
+
for (const child of running) {
|
|
7778
|
+
try {
|
|
7779
|
+
child.kill("SIGKILL");
|
|
7780
|
+
} catch {
|
|
7781
|
+
}
|
|
7782
|
+
}
|
|
7783
|
+
});
|
|
7784
|
+
function stop(child) {
|
|
7785
|
+
if (child.exitCode !== null || child.signalCode !== null) return;
|
|
7786
|
+
try {
|
|
7787
|
+
child.kill("SIGTERM");
|
|
7788
|
+
} catch {
|
|
7789
|
+
}
|
|
7790
|
+
const hard = setTimeout(() => {
|
|
7791
|
+
try {
|
|
7792
|
+
child.kill("SIGKILL");
|
|
7793
|
+
} catch {
|
|
7794
|
+
}
|
|
7795
|
+
}, 2e3);
|
|
7796
|
+
hard.unref();
|
|
7797
|
+
}
|
|
7798
|
+
function launchLine(program, args) {
|
|
7799
|
+
if (process.platform === "win32" && /\.(cmd|bat)$/i.test(program)) {
|
|
7800
|
+
const unsafe = [program, ...args].find((part) => /[&|<>^%!"]/.test(part));
|
|
7801
|
+
if (unsafe !== void 0) throw new HelperFailure(`on Windows a .cmd or .bat cannot take "${unsafe}" \u2014 those characters mean something to cmd.exe`);
|
|
7802
|
+
return { file: process.env["ComSpec"] || "cmd.exe", args: ["/d", "/c", program, ...args] };
|
|
7803
|
+
}
|
|
7804
|
+
return { file: program, args };
|
|
7805
|
+
}
|
|
7806
|
+
function localCommandRunner(opts) {
|
|
7807
|
+
const mine = /* @__PURE__ */ new Set();
|
|
7808
|
+
const trustAll = opts.trustAll ?? process.env["FLOWRIKI_TRUST_COMMANDS"] === "1";
|
|
7809
|
+
const run = async (request) => {
|
|
7810
|
+
const record = {
|
|
7811
|
+
fingerprint: request.fingerprint,
|
|
7812
|
+
name: request.name,
|
|
7813
|
+
program: request.program,
|
|
7814
|
+
args: request.args,
|
|
7815
|
+
folder: request.folder,
|
|
7816
|
+
env: Object.keys(request.env),
|
|
7817
|
+
mode: request.mode,
|
|
7818
|
+
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
7819
|
+
};
|
|
7820
|
+
if (!trustAll) {
|
|
7821
|
+
const approvals = readApprovals();
|
|
7822
|
+
if (!approvals.allowed[request.fingerprint]) {
|
|
7823
|
+
approvals.waiting[request.fingerprint] = record;
|
|
7824
|
+
try {
|
|
7825
|
+
writeApprovals(approvals);
|
|
7826
|
+
} catch {
|
|
7827
|
+
}
|
|
7828
|
+
throw new CommandNotAllowed(record);
|
|
7829
|
+
}
|
|
7830
|
+
}
|
|
7831
|
+
const cwd = request.folder ? path24.resolve(opts.root, request.folder) : opts.root;
|
|
7832
|
+
if (!fs24.existsSync(cwd)) throw new HelperFailure(`the folder ${cwd} does not exist on this computer`);
|
|
7833
|
+
const blank = (text) => request.redact.reduce((out, secret2) => secret2 ? out.split(secret2).join("\u2022\u2022\u2022") : out, text);
|
|
7834
|
+
const line = launchLine(request.program, request.args);
|
|
7835
|
+
return await new Promise((resolve2, reject) => {
|
|
7836
|
+
let stdout = "", stderr = "", settled = false;
|
|
7837
|
+
let child;
|
|
7838
|
+
try {
|
|
7839
|
+
child = spawn2(line.file, line.args, { cwd, env: { ...process.env, ...request.env }, shell: false, windowsHide: true, stdio: ["ignore", "pipe", "pipe"] });
|
|
7840
|
+
} catch (error) {
|
|
7841
|
+
reject(new HelperFailure(`${request.program} could not be started: ${error.message}`));
|
|
7842
|
+
return;
|
|
7843
|
+
}
|
|
7844
|
+
running.add(child);
|
|
7845
|
+
mine.add(child);
|
|
7846
|
+
const forget = () => {
|
|
7847
|
+
running.delete(child);
|
|
7848
|
+
mine.delete(child);
|
|
7849
|
+
};
|
|
7850
|
+
const finish = (outcome) => {
|
|
7851
|
+
if (settled) return;
|
|
7852
|
+
settled = true;
|
|
7853
|
+
clearTimeout(timer);
|
|
7854
|
+
if (outcome instanceof Error) reject(outcome);
|
|
7855
|
+
else resolve2(outcome);
|
|
7856
|
+
};
|
|
7857
|
+
const ready = request.readyPattern ? new RegExp(request.readyPattern) : null;
|
|
7858
|
+
const take = (into) => (chunk) => {
|
|
7859
|
+
const text = chunk.toString();
|
|
7860
|
+
if (into === "out") stdout = (stdout + text).slice(-KEEP_BYTES);
|
|
7861
|
+
else stderr = (stderr + text).slice(-KEEP_BYTES);
|
|
7862
|
+
if (request.mode === "background" && ready && ready.test(into === "out" ? stdout : stderr)) {
|
|
7863
|
+
finish({ exitCode: null, stdout: blank(stdout), stderr: blank(stderr) });
|
|
7864
|
+
}
|
|
7865
|
+
};
|
|
7866
|
+
child.stdout?.on("data", take("out"));
|
|
7867
|
+
child.stderr?.on("data", take("err"));
|
|
7868
|
+
child.on("error", (error) => {
|
|
7869
|
+
forget();
|
|
7870
|
+
finish(new HelperFailure(error.code === "ENOENT" ? `${request.program} was not found on this computer \u2014 install it, or give its full path` : `${request.program} could not be started: ${error.message}`));
|
|
7871
|
+
});
|
|
7872
|
+
child.on("close", (code) => {
|
|
7873
|
+
forget();
|
|
7874
|
+
if (request.mode === "background" && !settled) {
|
|
7875
|
+
const why = (stderr || stdout).trim().split(/\r?\n/).pop() ?? "";
|
|
7876
|
+
finish(new HelperFailure(`the command stopped before it was ready (exit ${code ?? "?"})${why ? `: ${blank(why).slice(0, 300)}` : ""}`));
|
|
7877
|
+
return;
|
|
7878
|
+
}
|
|
7879
|
+
finish({ exitCode: code, stdout: blank(stdout), stderr: blank(stderr) });
|
|
7880
|
+
});
|
|
7881
|
+
const timer = request.mode === "background" && !ready ? setTimeout(() => finish({ exitCode: null, stdout: blank(stdout), stderr: blank(stderr) }), Math.min(SETTLE_MS, request.timeoutMs)) : setTimeout(() => {
|
|
7882
|
+
stop(child);
|
|
7883
|
+
finish(new HelperFailure(request.mode === "background" ? `the command did not say it was ready within ${Math.round(request.timeoutMs / 1e3)}s` : `the command did not finish within ${Math.round(request.timeoutMs / 1e3)}s, so it was stopped`));
|
|
7884
|
+
}, request.timeoutMs);
|
|
7885
|
+
if (request.mode === "background") {
|
|
7886
|
+
const limit = setTimeout(() => stop(child), BACKGROUND_LIMIT_MS);
|
|
7887
|
+
limit.unref();
|
|
7888
|
+
}
|
|
7889
|
+
});
|
|
7890
|
+
};
|
|
7891
|
+
return {
|
|
7892
|
+
run,
|
|
7893
|
+
/** Stop every background command this runner started — the test is over. */
|
|
7894
|
+
close: () => {
|
|
7895
|
+
for (const child of mine) stop(child);
|
|
7896
|
+
mine.clear();
|
|
7897
|
+
}
|
|
7898
|
+
};
|
|
7899
|
+
}
|
|
7521
7900
|
var CLOCK_SLACK_MS = 1e4;
|
|
7522
|
-
function helperRunner(delivered, secrets, startedAt = /* @__PURE__ */ new Date()) {
|
|
7901
|
+
function helperRunner(delivered, secrets, startedAt = /* @__PURE__ */ new Date(), root = process.cwd()) {
|
|
7523
7902
|
if (!Array.isArray(delivered) || delivered.length === 0) return void 0;
|
|
7524
7903
|
const helpers = new Map(
|
|
7525
7904
|
DeliveredHelperSchema.array().parse(delivered).map((helper) => [helper.id, helper])
|
|
7526
7905
|
);
|
|
7527
7906
|
const since = new Date(startedAt.getTime() - CLOCK_SLACK_MS);
|
|
7528
|
-
|
|
7907
|
+
const commands = [...helpers.values()].some((h) => h.config.kind === "command") ? localCommandRunner({ root }) : void 0;
|
|
7908
|
+
const run = (async (helperId, inputs) => {
|
|
7529
7909
|
const helper = helpers.get(helperId);
|
|
7530
7910
|
if (!helper) throw new Error("that helper was deleted, or was not sent with this run \u2014 choose another");
|
|
7531
7911
|
try {
|
|
7532
|
-
const { value, detail } = await runHelper(helper
|
|
7912
|
+
const { value, detail } = await runHelper(helper, {
|
|
7533
7913
|
resolveSecret: (ref) => secrets?.[ref],
|
|
7534
7914
|
inputs,
|
|
7535
|
-
since
|
|
7915
|
+
since,
|
|
7916
|
+
...commands ? { commands } : {}
|
|
7536
7917
|
});
|
|
7537
7918
|
return { value, detail: `${helper.name}: ${detail}` };
|
|
7538
7919
|
} catch (error) {
|
|
7539
7920
|
throw new Error(`${helper.name}: ${error.message}`);
|
|
7540
7921
|
}
|
|
7541
|
-
};
|
|
7922
|
+
});
|
|
7923
|
+
run.close = () => commands?.close();
|
|
7924
|
+
return run;
|
|
7542
7925
|
}
|
|
7543
7926
|
var LOCAL_CHROMIUM2 = { id: "chromium", provider: "local", engine: "chromium" };
|
|
7544
7927
|
async function summaryOf2(storage, runId) {
|
|
@@ -7712,6 +8095,7 @@ async function executeCase(opts) {
|
|
|
7712
8095
|
};
|
|
7713
8096
|
}
|
|
7714
8097
|
await cloud?.close().catch(() => void 0);
|
|
8098
|
+
helpers?.close();
|
|
7715
8099
|
if (lane) outcome = { ...outcome, artifacts: [...outcome.artifacts ?? [], ...await lane.close(outcome.status === "passed")] };
|
|
7716
8100
|
await Promise.all(writes);
|
|
7717
8101
|
if (storage && run) {
|
|
@@ -7733,10 +8117,11 @@ async function executeCase(opts) {
|
|
|
7733
8117
|
masked: false
|
|
7734
8118
|
});
|
|
7735
8119
|
}
|
|
8120
|
+
void storage.pruneRuns(50).catch(() => void 0);
|
|
7736
8121
|
await storage.finishRun(run.id, {
|
|
7737
8122
|
status: outcome.status === "passed" ? "passed" : outcome.error === "the run was cancelled" ? "cancelled" : outcome.status === "failed" ? "failed" : "errored",
|
|
7738
8123
|
errorMessage: outcome.error,
|
|
7739
|
-
artifactsDir: runDir &&
|
|
8124
|
+
artifactsDir: runDir && fs34.existsSync(runDir) ? runDir : null
|
|
7740
8125
|
});
|
|
7741
8126
|
}
|
|
7742
8127
|
await emit({ type: "case.finished", runId: run?.id ?? "", caseId: testCase.id, outcome });
|
|
@@ -7750,6 +8135,10 @@ function redactPanel(state) {
|
|
|
7750
8135
|
return { ...panel, steps: panel.steps.map((s2) => redact(s2)) };
|
|
7751
8136
|
}
|
|
7752
8137
|
var START_GRACE_MS = 3e3;
|
|
8138
|
+
var IDLE_MS = (() => {
|
|
8139
|
+
const minutes = Number(process.env["FLOWRIKI_RECORDING_IDLE_MINUTES"] ?? 30);
|
|
8140
|
+
return Number.isFinite(minutes) && minutes >= 0 ? minutes * 6e4 : 30 * 6e4;
|
|
8141
|
+
})();
|
|
7753
8142
|
var RecordingCoordinator = class {
|
|
7754
8143
|
constructor(deps) {
|
|
7755
8144
|
this.deps = deps;
|
|
@@ -7773,6 +8162,33 @@ var RecordingCoordinator = class {
|
|
|
7773
8162
|
* than rebuilding it from the event, which would either lose the values or
|
|
7774
8163
|
* publish them. */
|
|
7775
8164
|
lastResult = null;
|
|
8165
|
+
/** When somebody last did anything in the recording. */
|
|
8166
|
+
touchedAt = 0;
|
|
8167
|
+
panelSeen = "";
|
|
8168
|
+
idleTimer = null;
|
|
8169
|
+
stoppingForIdle = false;
|
|
8170
|
+
touch() {
|
|
8171
|
+
this.touchedAt = Date.now();
|
|
8172
|
+
}
|
|
8173
|
+
watchIdle() {
|
|
8174
|
+
const limit = this.deps.idleMs ?? IDLE_MS;
|
|
8175
|
+
this.stopWatchingIdle();
|
|
8176
|
+
if (limit <= 0) return;
|
|
8177
|
+
this.touch();
|
|
8178
|
+
this.idleTimer = setInterval(() => {
|
|
8179
|
+
if (!this.session || this.stopInFlight || Date.now() - this.touchedAt < limit) return;
|
|
8180
|
+
this.stopWatchingIdle();
|
|
8181
|
+
this.stoppingForIdle = true;
|
|
8182
|
+
void (this.draft?.steps.length ? this.stop() : this.discard()).finally(() => {
|
|
8183
|
+
this.stoppingForIdle = false;
|
|
8184
|
+
});
|
|
8185
|
+
}, Math.min(this.deps.idleCheckMs ?? 6e4, limit));
|
|
8186
|
+
this.idleTimer.unref?.();
|
|
8187
|
+
}
|
|
8188
|
+
stopWatchingIdle() {
|
|
8189
|
+
if (this.idleTimer) clearInterval(this.idleTimer);
|
|
8190
|
+
this.idleTimer = null;
|
|
8191
|
+
}
|
|
7776
8192
|
/** What the last completed stop answered, for a caller that missed it —
|
|
7777
8193
|
* a dropped socket, or a Stop pressed inside the recorded page. */
|
|
7778
8194
|
lastStop() {
|
|
@@ -7796,6 +8212,7 @@ var RecordingCoordinator = class {
|
|
|
7796
8212
|
}
|
|
7797
8213
|
async answerDialog(accept, promptText) {
|
|
7798
8214
|
if (!this.session) throw new Error("No recording is active");
|
|
8215
|
+
this.touch();
|
|
7799
8216
|
await this.session.answerDialog(accept, promptText);
|
|
7800
8217
|
}
|
|
7801
8218
|
/** Open a headed browser and start capturing.
|
|
@@ -7871,18 +8288,29 @@ var RecordingCoordinator = class {
|
|
|
7871
8288
|
// a file nothing ever read.
|
|
7872
8289
|
environment: ExecutionEnvironmentSchema.parse(opts.environment ?? config.environment),
|
|
7873
8290
|
headless: opts.headless ?? false,
|
|
7874
|
-
onStep: (step) =>
|
|
8291
|
+
onStep: (step) => {
|
|
8292
|
+
this.touch();
|
|
8293
|
+
this.upsert(step);
|
|
8294
|
+
},
|
|
7875
8295
|
// The panel carries its OWN copy of every step, and it used to be
|
|
7876
8296
|
// emitted exactly as the session built it — so while `recording.step`
|
|
7877
8297
|
// was carefully redacted, the very next frame published the same step
|
|
7878
8298
|
// with the password still in it. Found by a test that typed a real
|
|
7879
8299
|
// password into a real browser; nothing about the redacted step event
|
|
7880
8300
|
// hinted at it.
|
|
7881
|
-
onState: (state) =>
|
|
8301
|
+
onState: (state) => {
|
|
8302
|
+
const seen = JSON.stringify(state);
|
|
8303
|
+
if (seen !== this.panelSeen) {
|
|
8304
|
+
this.panelSeen = seen;
|
|
8305
|
+
this.touch();
|
|
8306
|
+
}
|
|
8307
|
+
this.deps.emit({ type: "recording.panel", state: redactPanel(state) });
|
|
8308
|
+
},
|
|
7882
8309
|
// Stop pressed on the floating control inside the recorded page.
|
|
7883
8310
|
onStopRequested: (commit) => void (commit ? this.stop() : this.discard()),
|
|
7884
8311
|
onDisconnect: () => {
|
|
7885
8312
|
this.session = null;
|
|
8313
|
+
this.stopWatchingIdle();
|
|
7886
8314
|
this.deps.lease?.release("recording");
|
|
7887
8315
|
this.deps.emit({ type: "recording.interrupted" });
|
|
7888
8316
|
}
|
|
@@ -7898,6 +8326,7 @@ var RecordingCoordinator = class {
|
|
|
7898
8326
|
const projectOrigin = configuredBase ? new URL(normalizeStartUrl(configuredBase)).origin : "";
|
|
7899
8327
|
this.draft.baseUrl = opened.origin === projectOrigin ? "" : opened.origin;
|
|
7900
8328
|
this.draft.startPath = opened.pathname + opened.search;
|
|
8329
|
+
this.watchIdle();
|
|
7901
8330
|
this.deps.emit({ type: "recording.started", name: this.draft.name, url: this.session.startUrl });
|
|
7902
8331
|
return null;
|
|
7903
8332
|
}
|
|
@@ -7921,6 +8350,7 @@ var RecordingCoordinator = class {
|
|
|
7921
8350
|
* followed by an await, so two stops arriving together could otherwise both
|
|
7922
8351
|
* pass it and save the flow twice. */
|
|
7923
8352
|
stop() {
|
|
8353
|
+
this.stopWatchingIdle();
|
|
7924
8354
|
if (!this.stopInFlight) {
|
|
7925
8355
|
this.deps.lease?.beginRelease("recording");
|
|
7926
8356
|
this.stopInFlight = this.doStop().finally(() => {
|
|
@@ -7969,7 +8399,7 @@ var RecordingCoordinator = class {
|
|
|
7969
8399
|
const file2 = this.deps.store.saveFlow(doc);
|
|
7970
8400
|
this.draft = null;
|
|
7971
8401
|
const saved = { ...this.deps.store.getFlow(doc.id) ?? doc, path: file2 };
|
|
7972
|
-
this.deps.emit({ type: "recording.stopped", flow: saved, file: file2 });
|
|
8402
|
+
this.deps.emit({ type: "recording.stopped", flow: saved, file: file2, ...this.stoppingForIdle ? { reason: "idle" } : {} });
|
|
7973
8403
|
return this.lastResult = { ok: true, flow: saved, secrets: captured };
|
|
7974
8404
|
}
|
|
7975
8405
|
/** Throw the recording away: close the browser, drop the draft, write
|
|
@@ -7980,6 +8410,7 @@ var RecordingCoordinator = class {
|
|
|
7980
8410
|
* time — a password briefly reaches disk for a recording nobody wanted. */
|
|
7981
8411
|
async discard() {
|
|
7982
8412
|
if (!this.draft && !this.session) return { ok: false, error: "no recording in progress" };
|
|
8413
|
+
this.stopWatchingIdle();
|
|
7983
8414
|
this.deps.lease?.beginRelease("recording");
|
|
7984
8415
|
const ending = this.session;
|
|
7985
8416
|
this.session = null;
|
|
@@ -8122,15 +8553,41 @@ var COMMANDS = /* @__PURE__ */ new Set([
|
|
|
8122
8553
|
]);
|
|
8123
8554
|
var UnknownDebugCommand = class extends Error {
|
|
8124
8555
|
};
|
|
8556
|
+
var DEBUG_IDLE_MS = (() => {
|
|
8557
|
+
const minutes = Number(process.env["FLOWRIKI_RECORDING_IDLE_MINUTES"] ?? 30);
|
|
8558
|
+
return Number.isFinite(minutes) && minutes >= 0 ? minutes * 6e4 : 30 * 6e4;
|
|
8559
|
+
})();
|
|
8125
8560
|
var DebugCoordinator = class {
|
|
8126
8561
|
constructor(deps) {
|
|
8127
8562
|
this.deps = deps;
|
|
8128
8563
|
}
|
|
8129
8564
|
deps;
|
|
8565
|
+
touchedAt = 0;
|
|
8566
|
+
stateSeen = "";
|
|
8567
|
+
/** The steps as the session opened them: unchanged means nothing to lose. */
|
|
8568
|
+
openedWith = "";
|
|
8569
|
+
idleTimer = null;
|
|
8130
8570
|
retainCompleted = false;
|
|
8131
8571
|
lastState = null;
|
|
8132
8572
|
lastEnd = null;
|
|
8133
8573
|
current = null;
|
|
8574
|
+
watchIdle() {
|
|
8575
|
+
this.stopWatchingIdle();
|
|
8576
|
+
const limit = this.deps.idleMs ?? DEBUG_IDLE_MS;
|
|
8577
|
+
if (limit <= 0) return;
|
|
8578
|
+
this.touchedAt = Date.now();
|
|
8579
|
+
this.idleTimer = setInterval(() => {
|
|
8580
|
+
if (!this.current || Date.now() - this.touchedAt < limit) return;
|
|
8581
|
+
if (JSON.stringify(this.current.session.state().steps) !== this.openedWith) return;
|
|
8582
|
+
this.stopWatchingIdle();
|
|
8583
|
+
void this.end(false).catch(() => void 0);
|
|
8584
|
+
}, Math.min(this.deps.idleCheckMs ?? 6e4, limit));
|
|
8585
|
+
this.idleTimer.unref?.();
|
|
8586
|
+
}
|
|
8587
|
+
stopWatchingIdle() {
|
|
8588
|
+
if (this.idleTimer) clearInterval(this.idleTimer);
|
|
8589
|
+
this.idleTimer = null;
|
|
8590
|
+
}
|
|
8134
8591
|
status() {
|
|
8135
8592
|
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
8593
|
}
|
|
@@ -8169,11 +8626,19 @@ var DebugCoordinator = class {
|
|
|
8169
8626
|
// Secrets are read on THIS machine and handed to the browser here. A
|
|
8170
8627
|
// hosted dashboard drives the session and never learns a value.
|
|
8171
8628
|
resolveSecret: (ref) => secrets[ref],
|
|
8172
|
-
onState: (state2) =>
|
|
8629
|
+
onState: (state2) => {
|
|
8630
|
+
const seen = JSON.stringify(state2);
|
|
8631
|
+
if (seen !== this.stateSeen) {
|
|
8632
|
+
this.stateSeen = seen;
|
|
8633
|
+
this.touchedAt = Date.now();
|
|
8634
|
+
}
|
|
8635
|
+
this.deps.emit({ type: "debug.state", flowId: flow.id, state: state2 });
|
|
8636
|
+
},
|
|
8173
8637
|
// Save & close / Discard, pressed on the panel inside the page.
|
|
8174
8638
|
onEndRequested: (commit) => void this.end(commit),
|
|
8175
8639
|
onDisconnect: () => {
|
|
8176
8640
|
this.current = null;
|
|
8641
|
+
this.stopWatchingIdle();
|
|
8177
8642
|
this.deps.lease?.release("debug");
|
|
8178
8643
|
this.deps.emit({ type: "debug.ended", flowId: flow.id, committed: false });
|
|
8179
8644
|
}
|
|
@@ -8181,6 +8646,8 @@ var DebugCoordinator = class {
|
|
|
8181
8646
|
this.deps.lease?.activate("debug");
|
|
8182
8647
|
this.current = { flowId: flow.id, session };
|
|
8183
8648
|
const state = session.state();
|
|
8649
|
+
this.openedWith = JSON.stringify(state.steps);
|
|
8650
|
+
this.watchIdle();
|
|
8184
8651
|
this.deps.emit({ type: "debug.state", flowId: flow.id, state });
|
|
8185
8652
|
return { flowId: flow.id, state };
|
|
8186
8653
|
} catch (err) {
|
|
@@ -8191,6 +8658,7 @@ var DebugCoordinator = class {
|
|
|
8191
8658
|
async command(command, args = {}) {
|
|
8192
8659
|
if (!this.current) throw new Error("no debug session is running");
|
|
8193
8660
|
if (!COMMANDS.has(command)) throw new UnknownDebugCommand(`unknown command: ${command}`);
|
|
8661
|
+
this.touchedAt = Date.now();
|
|
8194
8662
|
const s2 = this.current.session;
|
|
8195
8663
|
switch (command) {
|
|
8196
8664
|
case "play":
|
|
@@ -8233,6 +8701,7 @@ var DebugCoordinator = class {
|
|
|
8233
8701
|
}
|
|
8234
8702
|
const { flowId, session } = this.current;
|
|
8235
8703
|
this.current = null;
|
|
8704
|
+
this.stopWatchingIdle();
|
|
8236
8705
|
this.deps.lease?.beginRelease("debug");
|
|
8237
8706
|
try {
|
|
8238
8707
|
if (!commit) {
|
|
@@ -8337,10 +8806,10 @@ async function verifyFlowSteps(deps, opts) {
|
|
|
8337
8806
|
}
|
|
8338
8807
|
function deliveredRun(delivered) {
|
|
8339
8808
|
const { flow, secrets, config } = delivered;
|
|
8340
|
-
const
|
|
8809
|
+
const runHelper3 = helperRunner(delivered.helpers, secrets);
|
|
8341
8810
|
return {
|
|
8342
8811
|
resolveAuth: authenticationResolver(delivered.authentications),
|
|
8343
|
-
...
|
|
8812
|
+
...runHelper3 ? { runHelper: runHelper3 } : {},
|
|
8344
8813
|
readConfig: () => config,
|
|
8345
8814
|
// By id, and only the one that was sent. A machine serving no checkout has
|
|
8346
8815
|
// no other flow to confuse it with.
|
|
@@ -8388,7 +8857,7 @@ function resolveCheckouts(dirs) {
|
|
|
8388
8857
|
// src/engine/session.ts
|
|
8389
8858
|
import fs12 from "fs";
|
|
8390
8859
|
|
|
8391
|
-
// ../agent-core/dist/chunk-
|
|
8860
|
+
// ../agent-core/dist/chunk-CRKFBFSC.js
|
|
8392
8861
|
import { WebSocket, WebSocketServer } from "ws";
|
|
8393
8862
|
var HEARTBEAT_MS = 3e4;
|
|
8394
8863
|
function adapt(socket) {
|
|
@@ -8565,6 +9034,27 @@ var EventJournal = class {
|
|
|
8565
9034
|
}
|
|
8566
9035
|
};
|
|
8567
9036
|
var { DatabaseSync } = createRequire4(import.meta.url)("node:sqlite");
|
|
9037
|
+
var LOCAL_LIMIT = 64 * 1024;
|
|
9038
|
+
function fitted(value, limit = LOCAL_LIMIT) {
|
|
9039
|
+
const text = JSON.stringify(value);
|
|
9040
|
+
if (Buffer.byteLength(text) <= limit) return text;
|
|
9041
|
+
for (const [maxText, maxItems] of [[2e3, 400], [500, 120], [200, 40], [80, 12]]) {
|
|
9042
|
+
const smaller = JSON.stringify(shrink(value, maxText, maxItems));
|
|
9043
|
+
if (Buffer.byteLength(smaller) <= limit) return smaller;
|
|
9044
|
+
}
|
|
9045
|
+
throw new Error("event exceeds local size limit even when shortened");
|
|
9046
|
+
}
|
|
9047
|
+
function shrink(value, maxText, maxItems) {
|
|
9048
|
+
if (typeof value === "string") return value.length > maxText ? `${value.slice(0, maxText)}\u2026 (cut)` : value;
|
|
9049
|
+
if (Array.isArray(value)) {
|
|
9050
|
+
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;
|
|
9051
|
+
return items.map((v) => shrink(v, maxText, maxItems));
|
|
9052
|
+
}
|
|
9053
|
+
if (value && typeof value === "object") {
|
|
9054
|
+
return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, shrink(v, maxText, maxItems)]));
|
|
9055
|
+
}
|
|
9056
|
+
return value;
|
|
9057
|
+
}
|
|
8568
9058
|
var AgentState = class {
|
|
8569
9059
|
constructor(file2, deviceId, commandLimit = 1e5) {
|
|
8570
9060
|
this.commandLimit = commandLimit;
|
|
@@ -8635,6 +9125,10 @@ var AgentState = class {
|
|
|
8635
9125
|
begin(command, fingerprint2) {
|
|
8636
9126
|
return this.transaction(() => {
|
|
8637
9127
|
if (this.get(command.id)) return false;
|
|
9128
|
+
if (Number(this.db.prepare("SELECT count(*) AS n FROM commands").get()?.n) >= this.commandLimit) {
|
|
9129
|
+
const cutoff = (command.issuedAt ?? command.expiresAt) - 30 * 24 * 60 * 6e4;
|
|
9130
|
+
if (cutoff > 0) this.db.prepare("DELETE FROM commands WHERE state IN ('completed','retired') AND finished_at < ?").run(cutoff);
|
|
9131
|
+
}
|
|
8638
9132
|
if (Number(this.db.prepare("SELECT count(*) AS n FROM commands").get()?.n) >= this.commandLimit) {
|
|
8639
9133
|
throw new Error("command ledger capacity reached; retain state and reconcile before further work");
|
|
8640
9134
|
}
|
|
@@ -8643,8 +9137,7 @@ var AgentState = class {
|
|
|
8643
9137
|
});
|
|
8644
9138
|
}
|
|
8645
9139
|
finish(id, ack, now) {
|
|
8646
|
-
const text =
|
|
8647
|
-
if (Buffer.byteLength(text) > 64 * 1024) throw new Error("command result exceeds local size limit");
|
|
9140
|
+
const text = fitted(AckSchema.parse(ack));
|
|
8648
9141
|
const result = this.db.prepare("UPDATE commands SET state='completed', ack=?, finished_at=? WHERE id=? AND state='started'").run(text, now, id);
|
|
8649
9142
|
if (Number(result.changes) !== 1) throw new Error("command intent is unavailable");
|
|
8650
9143
|
}
|
|
@@ -8657,8 +9150,7 @@ var AgentState = class {
|
|
|
8657
9150
|
this.db.prepare("UPDATE commands SET state='retired', ack=NULL WHERE state='completed' AND finished_at < ? AND expires_at <= ?").run(cutoff, now);
|
|
8658
9151
|
}
|
|
8659
9152
|
append(event, limit) {
|
|
8660
|
-
const text =
|
|
8661
|
-
if (Buffer.byteLength(text) > 64 * 1024) throw new Error("event exceeds local size limit");
|
|
9153
|
+
const text = fitted(event);
|
|
8662
9154
|
return this.transaction(() => {
|
|
8663
9155
|
const row = this.db.prepare("INSERT INTO events(payload) VALUES (?)").run(text);
|
|
8664
9156
|
const seq = Number(row.lastInsertRowid) - 1;
|
|
@@ -8687,6 +9179,13 @@ var AgentState = class {
|
|
|
8687
9179
|
this.db.close();
|
|
8688
9180
|
}
|
|
8689
9181
|
};
|
|
9182
|
+
function withoutSecrets(value) {
|
|
9183
|
+
if (Array.isArray(value)) return value.map(withoutSecrets);
|
|
9184
|
+
if (value && typeof value === "object") {
|
|
9185
|
+
return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, k === "secrets" ? {} : withoutSecrets(v)]));
|
|
9186
|
+
}
|
|
9187
|
+
return value;
|
|
9188
|
+
}
|
|
8690
9189
|
var Agent = class {
|
|
8691
9190
|
constructor(opts) {
|
|
8692
9191
|
this.opts = opts;
|
|
@@ -8785,10 +9284,10 @@ var Agent = class {
|
|
|
8785
9284
|
if (!allowed.ok) return this.refuse(command.id, allowed.reason);
|
|
8786
9285
|
if (this.closed) return this.refuse(command.id, "storage_unavailable", "the agent is closed");
|
|
8787
9286
|
const fingerprint2 = AgentState.fingerprint(command);
|
|
8788
|
-
const
|
|
8789
|
-
if (
|
|
8790
|
-
if (
|
|
8791
|
-
return { ...await
|
|
9287
|
+
const running2 = this.inFlight.get(command.id);
|
|
9288
|
+
if (running2) {
|
|
9289
|
+
if (running2.fingerprint !== fingerprint2) return this.refuse(command.id, "command_conflict", "this command ID names different work");
|
|
9290
|
+
return { ...await running2.promise, status: "duplicate" };
|
|
8792
9291
|
}
|
|
8793
9292
|
const previous = this.previous(command.id, fingerprint2);
|
|
8794
9293
|
if (previous) return previous;
|
|
@@ -8824,7 +9323,7 @@ var Agent = class {
|
|
|
8824
9323
|
ack = this.refuse(command.id, "failed", String(err instanceof Error ? err.message : err).split("\n")[0]);
|
|
8825
9324
|
}
|
|
8826
9325
|
if (EFFECTFUL.has(command.name)) {
|
|
8827
|
-
this.state.finish(command.id, ack, this.now());
|
|
9326
|
+
this.state.finish(command.id, withoutSecrets(ack), this.now());
|
|
8828
9327
|
}
|
|
8829
9328
|
return ack;
|
|
8830
9329
|
}
|
|
@@ -8989,18 +9488,19 @@ var AgentLink = class {
|
|
|
8989
9488
|
|
|
8990
9489
|
// ../provider-local/dist/index.js
|
|
8991
9490
|
import fs10 from "fs";
|
|
8992
|
-
import
|
|
9491
|
+
import os6 from "os";
|
|
8993
9492
|
import path10 from "path";
|
|
8994
9493
|
import fs25 from "fs";
|
|
8995
|
-
import
|
|
9494
|
+
import path25 from "path";
|
|
8996
9495
|
import { randomUUID } from "crypto";
|
|
8997
9496
|
import { execFile } from "child_process";
|
|
8998
|
-
import
|
|
9497
|
+
import fs35 from "fs";
|
|
8999
9498
|
import path34 from "path";
|
|
9000
9499
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
9500
|
+
import os22 from "os";
|
|
9001
9501
|
import { spawn as nodeSpawn } from "child_process";
|
|
9002
9502
|
import fs43 from "fs";
|
|
9003
|
-
import
|
|
9503
|
+
import os32 from "os";
|
|
9004
9504
|
import path42 from "path";
|
|
9005
9505
|
var NO_CHECKOUT = {
|
|
9006
9506
|
ok: false,
|
|
@@ -9011,7 +9511,7 @@ var scratchRoots = /* @__PURE__ */ new Map();
|
|
|
9011
9511
|
function scratchRoot(projectId) {
|
|
9012
9512
|
let root = scratchRoots.get(projectId);
|
|
9013
9513
|
if (!root) {
|
|
9014
|
-
root = fs10.mkdtempSync(path10.join(
|
|
9514
|
+
root = fs10.mkdtempSync(path10.join(os6.tmpdir(), `flowriki-record-${projectId.replace(/[^\w-]/g, "")}-`));
|
|
9015
9515
|
scratchRoots.set(projectId, root);
|
|
9016
9516
|
}
|
|
9017
9517
|
return root;
|
|
@@ -9245,7 +9745,7 @@ function historyHandlers({ storage, artifactLimit }) {
|
|
|
9245
9745
|
const artifact = run.artifacts.find((a) => a.id === artifactId);
|
|
9246
9746
|
if (!artifact) return { ok: false, reason: "failed", detail: "no such artifact on that run" };
|
|
9247
9747
|
const dir = run.artifactsDir ?? runArtifactsDir(runId, run.projectId);
|
|
9248
|
-
const file2 =
|
|
9748
|
+
const file2 = path25.join(dir, artifact.relPath);
|
|
9249
9749
|
if (!isInside(dir, file2) || !fs25.existsSync(file2) || !isInside(fs25.realpathSync(dir), fs25.realpathSync(file2))) {
|
|
9250
9750
|
return { ok: false, reason: "failed", detail: "the artifact file is not on this machine" };
|
|
9251
9751
|
}
|
|
@@ -9676,7 +10176,7 @@ function debuggingHandlers(ctx) {
|
|
|
9676
10176
|
var file = () => path34.join(flowrikiHome(), "assistant-folders.json");
|
|
9677
10177
|
function readAll() {
|
|
9678
10178
|
try {
|
|
9679
|
-
const parsed = JSON.parse(
|
|
10179
|
+
const parsed = JSON.parse(fs35.readFileSync(file(), "utf8"));
|
|
9680
10180
|
if (!parsed || typeof parsed !== "object") return {};
|
|
9681
10181
|
const out = {};
|
|
9682
10182
|
for (const [id, value] of Object.entries(parsed)) {
|
|
@@ -9689,18 +10189,18 @@ function readAll() {
|
|
|
9689
10189
|
}
|
|
9690
10190
|
}
|
|
9691
10191
|
function writeAll(all) {
|
|
9692
|
-
|
|
9693
|
-
|
|
10192
|
+
fs35.mkdirSync(path34.dirname(file()), { recursive: true, mode: 448 });
|
|
10193
|
+
fs35.writeFileSync(file(), JSON.stringify(all, null, 2), { mode: 384 });
|
|
9694
10194
|
}
|
|
9695
10195
|
function gitHead(folder) {
|
|
9696
10196
|
try {
|
|
9697
|
-
const head =
|
|
10197
|
+
const head = fs35.readFileSync(path34.join(folder, ".git", "HEAD"), "utf8").trim();
|
|
9698
10198
|
const ref = /^ref: (.+)$/.exec(head)?.[1];
|
|
9699
10199
|
if (!ref) return { commit: /^[0-9a-f]{40}$/.test(head) ? head : null, branch: null };
|
|
9700
10200
|
const branch = ref.replace(/^refs\/heads\//, "");
|
|
9701
10201
|
const loose = path34.join(folder, ".git", ref);
|
|
9702
|
-
if (
|
|
9703
|
-
const packed =
|
|
10202
|
+
if (fs35.existsSync(loose)) return { commit: fs35.readFileSync(loose, "utf8").trim(), branch };
|
|
10203
|
+
const packed = fs35.readFileSync(path34.join(folder, ".git", "packed-refs"), "utf8");
|
|
9704
10204
|
const line = packed.split("\n").find((l) => l.endsWith(` ${ref}`));
|
|
9705
10205
|
return { commit: line?.split(" ")[0] ?? null, branch };
|
|
9706
10206
|
} catch {
|
|
@@ -9710,7 +10210,7 @@ function gitHead(folder) {
|
|
|
9710
10210
|
function describeFolder(folder, name = path34.basename(folder)) {
|
|
9711
10211
|
const exists = (() => {
|
|
9712
10212
|
try {
|
|
9713
|
-
return
|
|
10213
|
+
return fs35.statSync(folder).isDirectory();
|
|
9714
10214
|
} catch {
|
|
9715
10215
|
return false;
|
|
9716
10216
|
}
|
|
@@ -9859,6 +10359,12 @@ var runFile = (file2, args, timeoutMs) => new Promise((resolve2, reject) => {
|
|
|
9859
10359
|
});
|
|
9860
10360
|
var live = /* @__PURE__ */ new Map();
|
|
9861
10361
|
var liveById = (sessionId) => [...live.values()].find((l) => l.id === sessionId);
|
|
10362
|
+
var IDLE_MS2 = (() => {
|
|
10363
|
+
const minutes = Number(process.env["FLOWRIKI_RECORDING_IDLE_MINUTES"] ?? 30);
|
|
10364
|
+
return Number.isFinite(minutes) && minutes >= 0 ? minutes * 6e4 : 30 * 6e4;
|
|
10365
|
+
})();
|
|
10366
|
+
var parked = /* @__PURE__ */ new Map();
|
|
10367
|
+
var PARKED_FOR_MS = 24 * 60 * 6e4;
|
|
9862
10368
|
var ownerOf = (l) => l.draft ? "recording" : "debug";
|
|
9863
10369
|
var SECRET_WORDS = /password|passwd|pass[-_\s]?code|pwd|secret|otp|\bpin\b|cvv|cvc/i;
|
|
9864
10370
|
async function screenOf(l, withImage = false) {
|
|
@@ -9886,12 +10392,31 @@ async function screenOf(l, withImage = false) {
|
|
|
9886
10392
|
device: l.session.deviceName
|
|
9887
10393
|
};
|
|
9888
10394
|
}
|
|
10395
|
+
async function closeIdlePhoneSessions(release, limit = IDLE_MS2, now = Date.now()) {
|
|
10396
|
+
for (const [id, kept] of parked) if (now - kept.at > PARKED_FOR_MS) parked.delete(id);
|
|
10397
|
+
if (limit <= 0) return;
|
|
10398
|
+
for (const l of [...live.values()]) {
|
|
10399
|
+
if (now - l.touchedAt < limit) continue;
|
|
10400
|
+
live.delete(l.platform);
|
|
10401
|
+
release(ownerOf(l), l.platform);
|
|
10402
|
+
await l.session.close().catch(() => void 0);
|
|
10403
|
+
if (l.draft?.steps.length) parked.set(l.id, { l, at: now });
|
|
10404
|
+
}
|
|
10405
|
+
}
|
|
9889
10406
|
function mobileHandlers(ctx) {
|
|
9890
10407
|
const require2 = (command) => {
|
|
9891
10408
|
const l = liveById(command.args["sessionId"]);
|
|
9892
|
-
if (
|
|
9893
|
-
|
|
10409
|
+
if (l) {
|
|
10410
|
+
l.touchedAt = Date.now();
|
|
10411
|
+
return l;
|
|
10412
|
+
}
|
|
10413
|
+
if (parked.has(String(command.args["sessionId"]))) {
|
|
10414
|
+
throw new Error("this phone was let go after a long time with nothing done \u2014 press Stop to save what was recorded");
|
|
10415
|
+
}
|
|
10416
|
+
throw new Error("that phone session has ended \u2014 open it again");
|
|
9894
10417
|
};
|
|
10418
|
+
const sweeper = setInterval(() => void closeIdlePhoneSessions((owner, platform) => ctx.lease?.release(owner, platform)), 6e4);
|
|
10419
|
+
sweeper.unref?.();
|
|
9895
10420
|
return {
|
|
9896
10421
|
// Everything this computer has for testing — web and phones — for the Guide.
|
|
9897
10422
|
...ctx.deps.machineCheck ? {
|
|
@@ -9965,6 +10490,7 @@ function mobileHandlers(ctx) {
|
|
|
9965
10490
|
platform,
|
|
9966
10491
|
values: /* @__PURE__ */ new Map(),
|
|
9967
10492
|
typed,
|
|
10493
|
+
touchedAt: Date.now(),
|
|
9968
10494
|
resolveSecret: (ref) => typed.get(ref) ?? secrets[ref],
|
|
9969
10495
|
resolveAuth: authenticationResolver(command.args["authentications"]),
|
|
9970
10496
|
runHelper: helperRunner(command.args["helpers"], secrets)
|
|
@@ -9994,6 +10520,12 @@ function mobileHandlers(ctx) {
|
|
|
9994
10520
|
}
|
|
9995
10521
|
return { ok: true, result: { sessionId: l2.id, screen: await screenOf(l2) } };
|
|
9996
10522
|
}
|
|
10523
|
+
const resting = parked.get(String(command.args["sessionId"]));
|
|
10524
|
+
if (resting && (op === "stop" || op === "discard")) {
|
|
10525
|
+
parked.delete(String(command.args["sessionId"]));
|
|
10526
|
+
if (op === "discard") return { ok: true, result: { ended: true } };
|
|
10527
|
+
return { ok: true, result: finished(resting.l) };
|
|
10528
|
+
}
|
|
9997
10529
|
const l = require2(command);
|
|
9998
10530
|
if (op === "act" || op === "run") {
|
|
9999
10531
|
const raw = command.args["step"] ?? {};
|
|
@@ -10069,24 +10601,7 @@ function mobileHandlers(ctx) {
|
|
|
10069
10601
|
ctx.lease?.release(ownerOf(l), l.platform);
|
|
10070
10602
|
await l.session.close().catch(() => void 0);
|
|
10071
10603
|
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) } };
|
|
10604
|
+
return { ok: true, result: finished(l) };
|
|
10090
10605
|
}
|
|
10091
10606
|
return { ok: false, reason: "failed", detail: `unknown phone session operation "${op}"` };
|
|
10092
10607
|
} catch (error) {
|
|
@@ -10096,6 +10611,52 @@ function mobileHandlers(ctx) {
|
|
|
10096
10611
|
}
|
|
10097
10612
|
};
|
|
10098
10613
|
}
|
|
10614
|
+
function finished(l) {
|
|
10615
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
10616
|
+
const d = l.draft;
|
|
10617
|
+
const flow = {
|
|
10618
|
+
schemaVersion: CURRENT_SCHEMA_VERSION,
|
|
10619
|
+
id: `flw_${randomUUID2().replace(/-/g, "").slice(0, 12)}`,
|
|
10620
|
+
featureId: d.featureId,
|
|
10621
|
+
name: d.name,
|
|
10622
|
+
slug: slugify(d.name) || "flow",
|
|
10623
|
+
startPath: d.startPath,
|
|
10624
|
+
platform: l.platform,
|
|
10625
|
+
recordedAt: now,
|
|
10626
|
+
recordedBy: d.recordedBy,
|
|
10627
|
+
updatedAt: now,
|
|
10628
|
+
revision: 0,
|
|
10629
|
+
steps: d.steps,
|
|
10630
|
+
...d.signIn ? { signIn: d.signIn } : {}
|
|
10631
|
+
};
|
|
10632
|
+
return { flow, secrets: Object.fromEntries(l.typed) };
|
|
10633
|
+
}
|
|
10634
|
+
function helperHandlers(_ctx) {
|
|
10635
|
+
return {
|
|
10636
|
+
"helper.run": async (command, project) => {
|
|
10637
|
+
const parsed = DeliveredHelperSchema.safeParse(command.args["helper"]);
|
|
10638
|
+
if (!parsed.success) return { ok: false, reason: "failed", detail: "that helper could not be read" };
|
|
10639
|
+
const secrets = command.args["secrets"] ?? {};
|
|
10640
|
+
const inputs = command.args["inputs"] ?? {};
|
|
10641
|
+
const commands = localCommandRunner({ root: project.root || os22.homedir() });
|
|
10642
|
+
const started = Date.now();
|
|
10643
|
+
try {
|
|
10644
|
+
const result = await runHelper(parsed.data, { resolveSecret: (ref) => secrets[ref], inputs, commands });
|
|
10645
|
+
return { ok: true, result: { ...result, tookMs: Date.now() - started } };
|
|
10646
|
+
} catch (error) {
|
|
10647
|
+
const notAllowed = error instanceof CommandNotAllowed;
|
|
10648
|
+
return {
|
|
10649
|
+
ok: false,
|
|
10650
|
+
reason: "failed",
|
|
10651
|
+
detail: error instanceof HelperFailure ? error.message : `the helper failed: ${error.message}`,
|
|
10652
|
+
...notAllowed ? { code: "command_not_allowed" } : {}
|
|
10653
|
+
};
|
|
10654
|
+
} finally {
|
|
10655
|
+
commands.close();
|
|
10656
|
+
}
|
|
10657
|
+
}
|
|
10658
|
+
};
|
|
10659
|
+
}
|
|
10099
10660
|
function buildHandlers(deps) {
|
|
10100
10661
|
const context = {
|
|
10101
10662
|
deps,
|
|
@@ -10124,6 +10685,7 @@ function buildHandlers(deps) {
|
|
|
10124
10685
|
...recordingHandlers(context),
|
|
10125
10686
|
...assistantHandlers(context),
|
|
10126
10687
|
...mobileHandlers(context),
|
|
10688
|
+
...helperHandlers(context),
|
|
10127
10689
|
...debug
|
|
10128
10690
|
};
|
|
10129
10691
|
}
|
|
@@ -10154,6 +10716,20 @@ function toolArgs(tool, input) {
|
|
|
10154
10716
|
if (typeof value === "number" && Number.isFinite(value)) out[key] = value;
|
|
10155
10717
|
}
|
|
10156
10718
|
if (record["newBranch"] === true) out["newBranch"] = true;
|
|
10719
|
+
if (typeof record["case"] === "string" && record["case"]) out["case"] = clip(record["case"], 40);
|
|
10720
|
+
if (Array.isArray(record["features"])) {
|
|
10721
|
+
const cases = [];
|
|
10722
|
+
for (const feature of record["features"]) {
|
|
10723
|
+
if (!feature || typeof feature !== "object") continue;
|
|
10724
|
+
const f = feature;
|
|
10725
|
+
const folder = typeof f["feature"] === "string" ? f["feature"] : typeof f["name"] === "string" ? f["name"] : "";
|
|
10726
|
+
for (const c of Array.isArray(f["cases"]) ? f["cases"] : []) {
|
|
10727
|
+
const name = c && typeof c === "object" ? c["name"] : void 0;
|
|
10728
|
+
if (typeof name === "string" && name && cases.length < 40) cases.push({ folder: clip(folder), name: clip(name) });
|
|
10729
|
+
}
|
|
10730
|
+
}
|
|
10731
|
+
if (cases.length) out["cases"] = cases;
|
|
10732
|
+
}
|
|
10157
10733
|
if (tool === "add_check" && typeof record["value"] === "string") out["value"] = clip(record["value"], 120);
|
|
10158
10734
|
if (Array.isArray(record["after"])) {
|
|
10159
10735
|
out["after"] = record["after"].filter((v) => typeof v === "string").slice(0, 5).map((v) => clip(v));
|
|
@@ -10185,13 +10761,65 @@ var ASK_MODE_TOOLS = [
|
|
|
10185
10761
|
"list_my_branches",
|
|
10186
10762
|
"run_insights"
|
|
10187
10763
|
];
|
|
10764
|
+
var NOT_RECORDING = [
|
|
10765
|
+
"create_project",
|
|
10766
|
+
"list_projects",
|
|
10767
|
+
"read_import_file",
|
|
10768
|
+
"save_import_cases",
|
|
10769
|
+
"read_update_request",
|
|
10770
|
+
"propose_case_updates",
|
|
10771
|
+
"write_test_cases",
|
|
10772
|
+
"gather_branches",
|
|
10773
|
+
"discard_branch"
|
|
10774
|
+
];
|
|
10775
|
+
var NOT_SAVING_TESTS = [
|
|
10776
|
+
"save_flow",
|
|
10777
|
+
"save_case",
|
|
10778
|
+
"preview_case",
|
|
10779
|
+
"start_case",
|
|
10780
|
+
"continue_flow",
|
|
10781
|
+
"remove_step",
|
|
10782
|
+
"replay_flow",
|
|
10783
|
+
"add_check",
|
|
10784
|
+
"use_helper",
|
|
10785
|
+
"create_sign_in",
|
|
10786
|
+
"create_account",
|
|
10787
|
+
"create_helper",
|
|
10788
|
+
"report_blocker"
|
|
10789
|
+
];
|
|
10790
|
+
var JOB_TOOLS_EXCEPT = {
|
|
10791
|
+
heal: [...NOT_RECORDING, "create_sign_in", "create_account"],
|
|
10792
|
+
automate: NOT_RECORDING,
|
|
10793
|
+
reautomate: NOT_RECORDING,
|
|
10794
|
+
update: NOT_RECORDING.filter((t) => t !== "read_update_request" && t !== "propose_case_updates"),
|
|
10795
|
+
explore: [...NOT_RECORDING, ...NOT_SAVING_TESTS],
|
|
10796
|
+
// Writing cases may still set up a sign-in it finds missing.
|
|
10797
|
+
write: [
|
|
10798
|
+
"create_project",
|
|
10799
|
+
"list_projects",
|
|
10800
|
+
"gather_branches",
|
|
10801
|
+
"discard_branch",
|
|
10802
|
+
"read_update_request",
|
|
10803
|
+
"propose_case_updates",
|
|
10804
|
+
...NOT_SAVING_TESTS.filter((t) => !t.startsWith("create_"))
|
|
10805
|
+
]
|
|
10806
|
+
};
|
|
10807
|
+
function toolsExceptFor(prompt, briefGiven) {
|
|
10808
|
+
const except = [...JOB_TOOLS_EXCEPT[usageKindOf(prompt)] ?? []];
|
|
10809
|
+
if (briefGiven) except.push("project_brief");
|
|
10810
|
+
return except;
|
|
10811
|
+
}
|
|
10188
10812
|
function buildAskArgs(opts) {
|
|
10189
10813
|
const ask = opts.mode === "ask";
|
|
10190
10814
|
const folders = opts.folders ?? [];
|
|
10191
10815
|
const brief = opts.brief?.trim();
|
|
10192
10816
|
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.
|
|
10817
|
+
`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."),
|
|
10818
|
+
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.",
|
|
10819
|
+
// SPENDING. Every turn re-reads the whole conversation, so turns are the
|
|
10820
|
+
// cost: do several steps per act call, don't re-read a page a step already
|
|
10821
|
+
// returned, and stop instead of retrying the same failing step.
|
|
10822
|
+
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
10823
|
"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
10824
|
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
10825
|
brief ? `
|
|
@@ -10221,9 +10849,25 @@ ${brief}` : ""
|
|
|
10221
10849
|
"dontAsk",
|
|
10222
10850
|
"--append-system-prompt",
|
|
10223
10851
|
guidance,
|
|
10852
|
+
// THE MODEL, SAID — not whatever the person's default is. Recording and
|
|
10853
|
+
// automating tests is what Sonnet does well; a default of Opus costs
|
|
10854
|
+
// several times as much for the same tests. FLOWRIKI_ASSISTANT_MODEL
|
|
10855
|
+
// overrides it.
|
|
10856
|
+
"--model",
|
|
10857
|
+
opts.model ?? (process.env["FLOWRIKI_ASSISTANT_MODEL"]?.trim() || "sonnet"),
|
|
10858
|
+
// A ceiling, not a target: a job that has taken this many turns is going
|
|
10859
|
+
// round in circles. What it saved stays; "continue" carries on.
|
|
10860
|
+
"--max-turns",
|
|
10861
|
+
String(opts.maxTurns ?? (ask ? 40 : 160)),
|
|
10224
10862
|
...opts.sessionId ? ["--resume", opts.sessionId] : []
|
|
10225
10863
|
];
|
|
10226
10864
|
}
|
|
10865
|
+
var tokensOf = (u) => ({
|
|
10866
|
+
input: u.input_tokens ?? 0,
|
|
10867
|
+
output: u.output_tokens ?? 0,
|
|
10868
|
+
cacheRead: u.cache_read_input_tokens ?? 0,
|
|
10869
|
+
cacheWrite: u.cache_creation_input_tokens ?? 0
|
|
10870
|
+
});
|
|
10227
10871
|
function clearOld(dir) {
|
|
10228
10872
|
try {
|
|
10229
10873
|
for (const name of fs43.readdirSync(dir)) {
|
|
@@ -10248,11 +10892,13 @@ function eventsFromLine(askId, line) {
|
|
|
10248
10892
|
if (parsed.session_id) out.push({ name: "assistant.session", data: { askId, sessionId: parsed.session_id } });
|
|
10249
10893
|
}
|
|
10250
10894
|
if (parsed.type === "assistant") {
|
|
10895
|
+
const tools = [];
|
|
10251
10896
|
for (const block of parsed.message?.content ?? []) {
|
|
10252
10897
|
if (block.type === "text" && block.text?.trim()) {
|
|
10253
10898
|
out.push({ name: "assistant.text", data: { askId, text: block.text } });
|
|
10254
10899
|
} else if (block.type === "tool_use" && block.name) {
|
|
10255
10900
|
const tool = block.name.replace(TOOL_PREFIX, "");
|
|
10901
|
+
tools.push(tool);
|
|
10256
10902
|
out.push({
|
|
10257
10903
|
name: "assistant.tool",
|
|
10258
10904
|
data: {
|
|
@@ -10265,6 +10911,12 @@ function eventsFromLine(askId, line) {
|
|
|
10265
10911
|
});
|
|
10266
10912
|
}
|
|
10267
10913
|
}
|
|
10914
|
+
if (parsed.message?.usage && parsed.message.id) {
|
|
10915
|
+
out.push({
|
|
10916
|
+
name: "assistant.usage",
|
|
10917
|
+
data: { askId, messageId: parsed.message.id, tools, ...tokensOf(parsed.message.usage), ...parsed.message.model ? { model: parsed.message.model } : {} }
|
|
10918
|
+
});
|
|
10919
|
+
}
|
|
10268
10920
|
}
|
|
10269
10921
|
if (parsed.type === "user") {
|
|
10270
10922
|
for (const block of parsed.message?.content ?? []) {
|
|
@@ -10278,6 +10930,20 @@ function eventsFromLine(askId, line) {
|
|
|
10278
10930
|
});
|
|
10279
10931
|
}
|
|
10280
10932
|
}
|
|
10933
|
+
if (parsed.type === "rate_limit_event" && parsed.rate_limit_info) {
|
|
10934
|
+
const info = parsed.rate_limit_info;
|
|
10935
|
+
out.push({
|
|
10936
|
+
name: "assistant.limits",
|
|
10937
|
+
data: {
|
|
10938
|
+
askId,
|
|
10939
|
+
...typeof info.status === "string" ? { status: info.status } : {},
|
|
10940
|
+
...typeof info.rateLimitType === "string" ? { window: info.rateLimitType } : {},
|
|
10941
|
+
...typeof info.utilization === "number" ? { utilization: info.utilization } : {},
|
|
10942
|
+
...typeof info.resetsAt === "number" ? { resetsAt: info.resetsAt } : {},
|
|
10943
|
+
...info.isUsingOverage === true ? { overage: true } : {}
|
|
10944
|
+
}
|
|
10945
|
+
});
|
|
10946
|
+
}
|
|
10281
10947
|
if (parsed.type === "result") {
|
|
10282
10948
|
out.push({
|
|
10283
10949
|
name: "assistant.done",
|
|
@@ -10285,20 +10951,43 @@ function eventsFromLine(askId, line) {
|
|
|
10285
10951
|
askId,
|
|
10286
10952
|
ok: parsed.is_error !== true,
|
|
10287
10953
|
result: typeof parsed.result === "string" ? parsed.result : "",
|
|
10288
|
-
...parsed.session_id ? { sessionId: parsed.session_id } : {}
|
|
10954
|
+
...parsed.session_id ? { sessionId: parsed.session_id } : {},
|
|
10955
|
+
// The whole job, as the model counted it.
|
|
10956
|
+
...parsed.usage ? { usage: tokensOf(parsed.usage) } : {},
|
|
10957
|
+
...typeof parsed.total_cost_usd === "number" ? { costUsd: parsed.total_cost_usd } : {},
|
|
10958
|
+
...typeof parsed.num_turns === "number" ? { turns: parsed.num_turns } : {},
|
|
10959
|
+
...typeof parsed.duration_ms === "number" ? { durationMs: parsed.duration_ms } : {}
|
|
10289
10960
|
}
|
|
10290
10961
|
});
|
|
10291
10962
|
}
|
|
10292
10963
|
return out;
|
|
10293
10964
|
}
|
|
10965
|
+
function stop2(child) {
|
|
10966
|
+
child.kill("SIGTERM");
|
|
10967
|
+
const force = setTimeout(() => {
|
|
10968
|
+
if (child.exitCode === null && child.signalCode === null) child.kill("SIGKILL");
|
|
10969
|
+
}, 5e3);
|
|
10970
|
+
force.unref?.();
|
|
10971
|
+
}
|
|
10294
10972
|
function createClaudeCli(options) {
|
|
10295
|
-
const
|
|
10973
|
+
const spawn3 = options.spawn ?? nodeSpawn;
|
|
10296
10974
|
const binary = options.binary ?? process.env["FLOWRIKI_CLAUDE_BIN"] ?? "claude";
|
|
10297
10975
|
const maxAskMs = options.maxAskMs ?? 60 * 6e4;
|
|
10298
|
-
const scratchRoot2 = options.scratchRoot ?? path42.join(
|
|
10299
|
-
const
|
|
10976
|
+
const scratchRoot2 = options.scratchRoot ?? path42.join(os32.tmpdir(), "flowriki-claude");
|
|
10977
|
+
const running2 = /* @__PURE__ */ new Map();
|
|
10300
10978
|
const MAX_AT_ONCE = 3;
|
|
10301
|
-
|
|
10979
|
+
process.once("exit", () => {
|
|
10980
|
+
for (const child of running2.values()) {
|
|
10981
|
+
try {
|
|
10982
|
+
child.kill("SIGTERM");
|
|
10983
|
+
} catch {
|
|
10984
|
+
}
|
|
10985
|
+
}
|
|
10986
|
+
});
|
|
10987
|
+
const cancelled = /* @__PURE__ */ new Set();
|
|
10988
|
+
const retrying = /* @__PURE__ */ new Set();
|
|
10989
|
+
const sessionsInUse = /* @__PURE__ */ new Set();
|
|
10990
|
+
const occupied = () => running2.size + retrying.size;
|
|
10302
10991
|
function capture(args, timeoutMs = 15e3) {
|
|
10303
10992
|
return new Promise((resolve2) => {
|
|
10304
10993
|
let out = "", err = "", settled = false;
|
|
@@ -10310,7 +10999,7 @@ function createClaudeCli(options) {
|
|
|
10310
10999
|
};
|
|
10311
11000
|
let child;
|
|
10312
11001
|
try {
|
|
10313
|
-
child =
|
|
11002
|
+
child = spawn3(binary, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
10314
11003
|
} catch {
|
|
10315
11004
|
return resolve2({ code: null, out: "", err: "", failed: true });
|
|
10316
11005
|
}
|
|
@@ -10328,8 +11017,8 @@ function createClaudeCli(options) {
|
|
|
10328
11017
|
child.on("close", (code) => finish(code, false));
|
|
10329
11018
|
});
|
|
10330
11019
|
}
|
|
10331
|
-
const LANE_FRESH_MS =
|
|
10332
|
-
const LANE_MAX_TOKENS =
|
|
11020
|
+
const LANE_FRESH_MS = 5 * 6e4;
|
|
11021
|
+
const LANE_MAX_TOKENS = 4e4;
|
|
10333
11022
|
const laneFile = (dir) => path42.join(dir, "lane-sessions.json");
|
|
10334
11023
|
const readLanes = (dir) => {
|
|
10335
11024
|
try {
|
|
@@ -10355,6 +11044,8 @@ function createClaudeCli(options) {
|
|
|
10355
11044
|
}
|
|
10356
11045
|
const kept = readLanes(dir)[ask.platform];
|
|
10357
11046
|
if (!kept || Date.now() - kept.updatedAt > LANE_FRESH_MS || kept.tokens > LANE_MAX_TOKENS) return void 0;
|
|
11047
|
+
if (sessionsInUse.has(kept.sessionId)) return void 0;
|
|
11048
|
+
if ((kept.kind ?? "chat") !== usageKindOf(ask.prompt)) return void 0;
|
|
10358
11049
|
return kept.sessionId;
|
|
10359
11050
|
}
|
|
10360
11051
|
return {
|
|
@@ -10368,17 +11059,17 @@ function createClaudeCli(options) {
|
|
|
10368
11059
|
return { installed: true, version: version.out.trim().split(/\s+/)[0] ?? null, ...parsed };
|
|
10369
11060
|
},
|
|
10370
11061
|
connect(emit) {
|
|
10371
|
-
if (
|
|
11062
|
+
if (running2.size > 0) return { started: false, detail: "The assistant is busy answering a question." };
|
|
10372
11063
|
let child;
|
|
10373
11064
|
try {
|
|
10374
|
-
child =
|
|
11065
|
+
child = spawn3(binary, ["auth", "login"], { stdio: ["ignore", "pipe", "pipe"] });
|
|
10375
11066
|
} catch {
|
|
10376
11067
|
return { started: false, detail: "The assistant\u2019s command-line tool could not be started." };
|
|
10377
11068
|
}
|
|
10378
|
-
let
|
|
11069
|
+
let finished2 = false;
|
|
10379
11070
|
const done = async (detail) => {
|
|
10380
|
-
if (
|
|
10381
|
-
|
|
11071
|
+
if (finished2) return;
|
|
11072
|
+
finished2 = true;
|
|
10382
11073
|
const status2 = await this.status();
|
|
10383
11074
|
emit("assistant.connect.finished", { signedIn: status2.signedIn, ...detail ? { detail } : {} });
|
|
10384
11075
|
};
|
|
@@ -10398,37 +11089,55 @@ function createClaudeCli(options) {
|
|
|
10398
11089
|
return { started: true };
|
|
10399
11090
|
},
|
|
10400
11091
|
ask(ask) {
|
|
10401
|
-
if (
|
|
11092
|
+
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
11093
|
fs43.mkdirSync(scratchRoot2, { recursive: true, mode: 448 });
|
|
10403
11094
|
const dir = path42.join(scratchRoot2, `project-${ask.projectId.replace(/[^A-Za-z0-9_-]/g, "_")}`);
|
|
10404
11095
|
fs43.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
10405
11096
|
clearOld(dir);
|
|
10406
11097
|
const mcpConfigPath = path42.join(dir, `mcp-${ask.askId.replace(/[^A-Za-z0-9_-]/g, "_").slice(0, 80)}.json`);
|
|
11098
|
+
const session = laneSession(ask, dir);
|
|
11099
|
+
const except = ask.mode === "ask" ? [] : toolsExceptFor(ask.prompt, Boolean(ask.brief && !session));
|
|
10407
11100
|
fs43.writeFileSync(
|
|
10408
11101
|
mcpConfigPath,
|
|
10409
|
-
JSON.stringify({ mcpServers: { [MCP_SERVER_NAME]: { command: options.mcp.command, args: options.mcp.args, env: {
|
|
11102
|
+
JSON.stringify({ mcpServers: { [MCP_SERVER_NAME]: { command: options.mcp.command, args: options.mcp.args, env: {
|
|
11103
|
+
FLOWRIKI_ASK_ID: ask.askId,
|
|
11104
|
+
// The project, known from the start: no "which project?" turn.
|
|
11105
|
+
FLOWRIKI_PROJECT_ID: ask.projectId,
|
|
11106
|
+
// The agent that launched it decides its version: the tool server
|
|
11107
|
+
// does not update itself in the middle of somebody's job.
|
|
11108
|
+
FLOWRIKI_NO_UPDATE: "1",
|
|
11109
|
+
FLOWRIKI_MCP_CHILD: "1",
|
|
11110
|
+
// A question: only the reading tools are served at all.
|
|
11111
|
+
...ask.mode === "ask" ? { FLOWRIKI_TOOLS: ASK_MODE_TOOLS.join(",") } : {},
|
|
11112
|
+
// A job of a known kind: the tools it never uses are not served.
|
|
11113
|
+
...except.length ? { FLOWRIKI_TOOLS_EXCEPT: except.join(",") } : {}
|
|
11114
|
+
} } } }),
|
|
10410
11115
|
{ mode: 384 }
|
|
10411
11116
|
);
|
|
10412
|
-
return launch(ask, dir, mcpConfigPath,
|
|
11117
|
+
return launch(ask, dir, mcpConfigPath, session);
|
|
10413
11118
|
},
|
|
10414
11119
|
cancel(askId) {
|
|
10415
|
-
|
|
11120
|
+
if (retrying.has(askId)) {
|
|
11121
|
+
cancelled.add(askId);
|
|
11122
|
+
return true;
|
|
11123
|
+
}
|
|
11124
|
+
const child = running2.get(askId);
|
|
10416
11125
|
if (!child) return false;
|
|
10417
|
-
cancelled
|
|
10418
|
-
child
|
|
11126
|
+
cancelled.add(askId);
|
|
11127
|
+
stop2(child);
|
|
10419
11128
|
setTimeout(() => child.kill("SIGKILL"), 3e3).unref?.();
|
|
10420
11129
|
return true;
|
|
10421
11130
|
},
|
|
10422
11131
|
/** Full — no room for another question — rather than "something runs":
|
|
10423
11132
|
* a second conversation is welcome while the first works. */
|
|
10424
11133
|
busy() {
|
|
10425
|
-
return
|
|
11134
|
+
return occupied() >= MAX_AT_ONCE;
|
|
10426
11135
|
}
|
|
10427
11136
|
};
|
|
10428
11137
|
function launch(ask, dir, mcpConfigPath, sessionId, attempt = 1) {
|
|
10429
11138
|
let child;
|
|
10430
11139
|
try {
|
|
10431
|
-
child =
|
|
11140
|
+
child = spawn3(
|
|
10432
11141
|
binary,
|
|
10433
11142
|
buildAskArgs({
|
|
10434
11143
|
prompt: ask.prompt,
|
|
@@ -10438,20 +11147,26 @@ function createClaudeCli(options) {
|
|
|
10438
11147
|
...sessionId ? { sessionId } : {},
|
|
10439
11148
|
...ask.folders?.length ? { folders: ask.folders } : {},
|
|
10440
11149
|
...ask.mode ? { mode: ask.mode } : {},
|
|
10441
|
-
|
|
11150
|
+
// Resumed, the conversation already holds its brief: a changed
|
|
11151
|
+
// one in the system prompt would throw away the whole cache.
|
|
11152
|
+
...ask.brief && !sessionId ? { brief: ask.brief } : {}
|
|
10442
11153
|
}),
|
|
10443
11154
|
// THE SCRATCH FOLDER, not the project: nothing here to read or write.
|
|
10444
|
-
|
|
11155
|
+
// Thinking is output, priced at five times input: capped per turn,
|
|
11156
|
+
// unless the person set their own.
|
|
11157
|
+
{ cwd: dir, stdio: ["ignore", "pipe", "pipe"], env: { ...process.env, MAX_THINKING_TOKENS: process.env["MAX_THINKING_TOKENS"] ?? "6000" } }
|
|
10445
11158
|
);
|
|
10446
11159
|
} catch {
|
|
10447
11160
|
return { started: false, detail: "The assistant\u2019s command-line tool could not be started." };
|
|
10448
11161
|
}
|
|
10449
|
-
|
|
11162
|
+
running2.set(ask.askId, child);
|
|
11163
|
+
if (sessionId) sessionsInUse.add(sessionId);
|
|
10450
11164
|
let buffer = "", stderr = "", sawDone = false, ended = false;
|
|
10451
11165
|
let silentFailure = false;
|
|
10452
11166
|
let toolsMissing = false;
|
|
10453
11167
|
let session;
|
|
10454
11168
|
let spent = 0;
|
|
11169
|
+
let lastContext = 0;
|
|
10455
11170
|
const handleLine = (line) => {
|
|
10456
11171
|
if (toolsMissing) return;
|
|
10457
11172
|
for (const event of eventsFromLine(ask.askId, line)) {
|
|
@@ -10464,6 +11179,7 @@ function createClaudeCli(options) {
|
|
|
10464
11179
|
continue;
|
|
10465
11180
|
}
|
|
10466
11181
|
if (event.name === "assistant.session") session = String(event.data["sessionId"] ?? "") || session;
|
|
11182
|
+
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
11183
|
if (event.name === "assistant.done" && event.data["ok"] === false && !String(event.data["result"] ?? "").trim()) {
|
|
10468
11184
|
silentFailure = true;
|
|
10469
11185
|
continue;
|
|
@@ -10485,10 +11201,18 @@ function createClaudeCli(options) {
|
|
|
10485
11201
|
ended = true;
|
|
10486
11202
|
clearTimeout(timer);
|
|
10487
11203
|
if (buffer.trim()) handleLine(buffer);
|
|
10488
|
-
|
|
10489
|
-
if (
|
|
11204
|
+
running2.delete(ask.askId);
|
|
11205
|
+
if (sessionId) sessionsInUse.delete(sessionId);
|
|
11206
|
+
const stopped = cancelled.has(ask.askId);
|
|
11207
|
+
if (toolsMissing && !stopped) {
|
|
10490
11208
|
if (attempt < 3) {
|
|
11209
|
+
retrying.add(ask.askId);
|
|
10491
11210
|
setTimeout(() => {
|
|
11211
|
+
retrying.delete(ask.askId);
|
|
11212
|
+
if (cancelled.delete(ask.askId)) {
|
|
11213
|
+
ask.emit("assistant.done", { askId: ask.askId, ok: false, result: "Stopped." });
|
|
11214
|
+
return;
|
|
11215
|
+
}
|
|
10492
11216
|
const again = launch(ask, dir, mcpConfigPath, sessionId, attempt + 1);
|
|
10493
11217
|
if (!again.started) ask.emit("assistant.done", { askId: ask.askId, ok: false, result: again.detail ?? "The assistant could not be started." });
|
|
10494
11218
|
}, 1500 * attempt + Math.floor(Math.random() * 1e3)).unref?.();
|
|
@@ -10497,22 +11221,22 @@ function createClaudeCli(options) {
|
|
|
10497
11221
|
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
11222
|
return;
|
|
10499
11223
|
}
|
|
10500
|
-
if (silentFailure && sessionId && /no conversation found/i.test(stderr) &&
|
|
11224
|
+
if (silentFailure && sessionId && /no conversation found/i.test(stderr) && !stopped) {
|
|
10501
11225
|
if (ask.platform) writeLane(dir, ask.platform, null);
|
|
10502
11226
|
const again = launch(ask, dir, mcpConfigPath, void 0);
|
|
10503
11227
|
if (again.started) return;
|
|
10504
11228
|
}
|
|
10505
11229
|
if (ask.platform && session && ok && !silentFailure) {
|
|
10506
|
-
|
|
10507
|
-
writeLane(dir, ask.platform, { sessionId: session, updatedAt: Date.now(), tokens: Math.max(before, spent) });
|
|
11230
|
+
writeLane(dir, ask.platform, { sessionId: session, updatedAt: Date.now(), tokens: lastContext || spent, kind: usageKindOf(ask.prompt) });
|
|
10508
11231
|
}
|
|
10509
11232
|
if (!sawDone) {
|
|
10510
11233
|
const why = detail || stderr.trim().split("\n").filter((l) => l && !/^Warning:/.test(l)).slice(-3).join(" ") || "The assistant stopped without saying why.";
|
|
10511
11234
|
ask.emit("assistant.done", { askId: ask.askId, ok: ok && !silentFailure, result: ok && !silentFailure ? "" : why });
|
|
10512
11235
|
}
|
|
11236
|
+
cancelled.delete(ask.askId);
|
|
10513
11237
|
};
|
|
10514
11238
|
const timer = setTimeout(() => {
|
|
10515
|
-
child
|
|
11239
|
+
stop2(child);
|
|
10516
11240
|
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
11241
|
}, maxAskMs);
|
|
10518
11242
|
timer.unref?.();
|
|
@@ -10520,7 +11244,12 @@ function createClaudeCli(options) {
|
|
|
10520
11244
|
buffer += chunk.toString();
|
|
10521
11245
|
let at;
|
|
10522
11246
|
while ((at = buffer.indexOf("\n")) >= 0) {
|
|
10523
|
-
|
|
11247
|
+
try {
|
|
11248
|
+
handleLine(buffer.slice(0, at));
|
|
11249
|
+
} catch (error) {
|
|
11250
|
+
stderr = `${stderr}
|
|
11251
|
+
${error.message}`.slice(-2e3);
|
|
11252
|
+
}
|
|
10524
11253
|
buffer = buffer.slice(at + 1);
|
|
10525
11254
|
}
|
|
10526
11255
|
});
|
|
@@ -10532,7 +11261,7 @@ function createClaudeCli(options) {
|
|
|
10532
11261
|
"close",
|
|
10533
11262
|
(code) => end(
|
|
10534
11263
|
code === 0,
|
|
10535
|
-
cancelled
|
|
11264
|
+
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
11265
|
)
|
|
10537
11266
|
);
|
|
10538
11267
|
return { started: true };
|
|
@@ -10723,11 +11452,11 @@ function createAgentRuntime(options) {
|
|
|
10723
11452
|
|
|
10724
11453
|
// src/lock.ts
|
|
10725
11454
|
import fs11 from "fs";
|
|
10726
|
-
import
|
|
11455
|
+
import os7 from "os";
|
|
10727
11456
|
function takeLock(env = process.env) {
|
|
10728
11457
|
ensureDataDir(env);
|
|
10729
11458
|
const file2 = lockPath(env);
|
|
10730
|
-
const mine = { pid: process.pid, since: (/* @__PURE__ */ new Date()).toISOString(), host:
|
|
11459
|
+
const mine = { pid: process.pid, since: (/* @__PURE__ */ new Date()).toISOString(), host: os7.hostname() };
|
|
10731
11460
|
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
10732
11461
|
try {
|
|
10733
11462
|
fs11.writeFileSync(file2, JSON.stringify(mine), { flag: "wx", mode: 384 });
|
|
@@ -10868,8 +11597,12 @@ async function start(options = {}) {
|
|
|
10868
11597
|
if (stopped) return;
|
|
10869
11598
|
stopped = true;
|
|
10870
11599
|
link.stop();
|
|
10871
|
-
|
|
10872
|
-
|
|
11600
|
+
try {
|
|
11601
|
+
agent.close();
|
|
11602
|
+
} catch {
|
|
11603
|
+
} finally {
|
|
11604
|
+
lock.lock.release();
|
|
11605
|
+
}
|
|
10873
11606
|
}
|
|
10874
11607
|
};
|
|
10875
11608
|
}
|
|
@@ -10879,7 +11612,7 @@ function status(serviceArg) {
|
|
|
10879
11612
|
const service = resolveService(serviceArg);
|
|
10880
11613
|
const connection = connectionFor(service);
|
|
10881
11614
|
const lock = takeLock();
|
|
10882
|
-
const
|
|
11615
|
+
const running2 = lock.ok ? null : lock.held;
|
|
10883
11616
|
if (lock.ok) lock.lock.release();
|
|
10884
11617
|
const dirs = connection?.checkouts ?? [];
|
|
10885
11618
|
const { projects, skipped } = resolveCheckouts(dirs);
|
|
@@ -10891,7 +11624,7 @@ function status(serviceArg) {
|
|
|
10891
11624
|
...projects.map((p) => ({ root: p.root, project: p.name })),
|
|
10892
11625
|
...skipped.map((s2) => ({ root: s2.root, project: null, why: s2.why }))
|
|
10893
11626
|
],
|
|
10894
|
-
running
|
|
11627
|
+
running: running2
|
|
10895
11628
|
};
|
|
10896
11629
|
}
|
|
10897
11630
|
async function doctor(serviceArg) {
|
|
@@ -10968,7 +11701,12 @@ export {
|
|
|
10968
11701
|
executeStep,
|
|
10969
11702
|
replayCase,
|
|
10970
11703
|
fieldNames,
|
|
11704
|
+
backupsFor,
|
|
11705
|
+
withBackups,
|
|
10971
11706
|
flowrikiHome,
|
|
11707
|
+
readApprovals,
|
|
11708
|
+
allowCommands,
|
|
11709
|
+
revokeCommands,
|
|
10972
11710
|
helperRunner,
|
|
10973
11711
|
resolveCheckouts,
|
|
10974
11712
|
start,
|