@runalabs/rill-cli 0.1.19 → 0.1.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -8
- package/dist/cli.js +898 -141
- package/dist/rill-chrome-extension.zip +0 -0
- package/package.json +5 -4
package/dist/cli.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
var __defProp = Object.defineProperty;
|
|
3
|
-
var __export = (
|
|
3
|
+
var __export = (target2, all) => {
|
|
4
4
|
for (var name in all)
|
|
5
|
-
__defProp(
|
|
5
|
+
__defProp(target2, name, { get: all[name], enumerable: true });
|
|
6
6
|
};
|
|
7
7
|
|
|
8
8
|
// src/windows-credentials.ts
|
|
@@ -1390,8 +1390,8 @@ function defineLazy(object2, key2, getter) {
|
|
|
1390
1390
|
function objectClone(obj) {
|
|
1391
1391
|
return Object.create(Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj));
|
|
1392
1392
|
}
|
|
1393
|
-
function assignProp(
|
|
1394
|
-
Object.defineProperty(
|
|
1393
|
+
function assignProp(target2, prop, value) {
|
|
1394
|
+
Object.defineProperty(target2, prop, {
|
|
1395
1395
|
value,
|
|
1396
1396
|
writable: true,
|
|
1397
1397
|
enumerable: true,
|
|
@@ -1574,35 +1574,35 @@ function normalizeParams(_params) {
|
|
|
1574
1574
|
return params;
|
|
1575
1575
|
}
|
|
1576
1576
|
function createTransparentProxy(getter) {
|
|
1577
|
-
let
|
|
1577
|
+
let target2;
|
|
1578
1578
|
return new Proxy({}, {
|
|
1579
1579
|
get(_, prop, receiver) {
|
|
1580
|
-
|
|
1581
|
-
return Reflect.get(
|
|
1580
|
+
target2 ?? (target2 = getter());
|
|
1581
|
+
return Reflect.get(target2, prop, receiver);
|
|
1582
1582
|
},
|
|
1583
1583
|
set(_, prop, value, receiver) {
|
|
1584
|
-
|
|
1585
|
-
return Reflect.set(
|
|
1584
|
+
target2 ?? (target2 = getter());
|
|
1585
|
+
return Reflect.set(target2, prop, value, receiver);
|
|
1586
1586
|
},
|
|
1587
1587
|
has(_, prop) {
|
|
1588
|
-
|
|
1589
|
-
return Reflect.has(
|
|
1588
|
+
target2 ?? (target2 = getter());
|
|
1589
|
+
return Reflect.has(target2, prop);
|
|
1590
1590
|
},
|
|
1591
1591
|
deleteProperty(_, prop) {
|
|
1592
|
-
|
|
1593
|
-
return Reflect.deleteProperty(
|
|
1592
|
+
target2 ?? (target2 = getter());
|
|
1593
|
+
return Reflect.deleteProperty(target2, prop);
|
|
1594
1594
|
},
|
|
1595
1595
|
ownKeys(_) {
|
|
1596
|
-
|
|
1597
|
-
return Reflect.ownKeys(
|
|
1596
|
+
target2 ?? (target2 = getter());
|
|
1597
|
+
return Reflect.ownKeys(target2);
|
|
1598
1598
|
},
|
|
1599
1599
|
getOwnPropertyDescriptor(_, prop) {
|
|
1600
|
-
|
|
1601
|
-
return Reflect.getOwnPropertyDescriptor(
|
|
1600
|
+
target2 ?? (target2 = getter());
|
|
1601
|
+
return Reflect.getOwnPropertyDescriptor(target2, prop);
|
|
1602
1602
|
},
|
|
1603
1603
|
defineProperty(_, prop, descriptor) {
|
|
1604
|
-
|
|
1605
|
-
return Reflect.defineProperty(
|
|
1604
|
+
target2 ?? (target2 = getter());
|
|
1605
|
+
return Reflect.defineProperty(target2, prop, descriptor);
|
|
1606
1606
|
}
|
|
1607
1607
|
});
|
|
1608
1608
|
}
|
|
@@ -2237,10 +2237,10 @@ var nanoid = /^[a-zA-Z0-9_-]{21}$/;
|
|
|
2237
2237
|
var duration = /^P(?:(\d+W)|(?!.*W)(?=\d|T\d)(\d+Y)?(\d+M)?(\d+D)?(T(?=\d)(\d+H)?(\d+M)?(\d+([.,]\d+)?S)?)?)$/;
|
|
2238
2238
|
var extendedDuration = /^[-+]?P(?!$)(?:(?:[-+]?\d+Y)|(?:[-+]?\d+[.,]\d+Y$))?(?:(?:[-+]?\d+M)|(?:[-+]?\d+[.,]\d+M$))?(?:(?:[-+]?\d+W)|(?:[-+]?\d+[.,]\d+W$))?(?:(?:[-+]?\d+D)|(?:[-+]?\d+[.,]\d+D$))?(?:T(?=[\d+-])(?:(?:[-+]?\d+H)|(?:[-+]?\d+[.,]\d+H$))?(?:(?:[-+]?\d+M)|(?:[-+]?\d+[.,]\d+M$))?(?:[-+]?\d+(?:[.,]\d+)?S)?)??$/;
|
|
2239
2239
|
var guid = /^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$/;
|
|
2240
|
-
var uuid = (
|
|
2241
|
-
if (!
|
|
2240
|
+
var uuid = (version3) => {
|
|
2241
|
+
if (!version3)
|
|
2242
2242
|
return /^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$/;
|
|
2243
|
-
return new RegExp(`^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-${
|
|
2243
|
+
return new RegExp(`^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-${version3}[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$`);
|
|
2244
2244
|
};
|
|
2245
2245
|
var uuid4 = /* @__PURE__ */ uuid(4);
|
|
2246
2246
|
var uuid6 = /* @__PURE__ */ uuid(6);
|
|
@@ -11701,10 +11701,10 @@ function _slugify() {
|
|
|
11701
11701
|
return /* @__PURE__ */ _overwrite((input) => slugify(input));
|
|
11702
11702
|
}
|
|
11703
11703
|
// @__NO_SIDE_EFFECTS__
|
|
11704
|
-
function _array(Class2,
|
|
11704
|
+
function _array(Class2, element2, params) {
|
|
11705
11705
|
return new Class2({
|
|
11706
11706
|
type: "array",
|
|
11707
|
-
element,
|
|
11707
|
+
element: element2,
|
|
11708
11708
|
// get element() {
|
|
11709
11709
|
// return element;
|
|
11710
11710
|
// },
|
|
@@ -12054,15 +12054,15 @@ function _stringFormat(Class2, format, fnOrRegex, _params = {}) {
|
|
|
12054
12054
|
|
|
12055
12055
|
// ../../node_modules/zod/v4/core/to-json-schema.js
|
|
12056
12056
|
function initializeContext(params) {
|
|
12057
|
-
let
|
|
12058
|
-
if (
|
|
12059
|
-
|
|
12060
|
-
if (
|
|
12061
|
-
|
|
12057
|
+
let target2 = params?.target ?? "draft-2020-12";
|
|
12058
|
+
if (target2 === "draft-4")
|
|
12059
|
+
target2 = "draft-04";
|
|
12060
|
+
if (target2 === "draft-7")
|
|
12061
|
+
target2 = "draft-07";
|
|
12062
12062
|
return {
|
|
12063
12063
|
processors: params.processors ?? {},
|
|
12064
12064
|
metadataRegistry: params?.metadata ?? globalRegistry,
|
|
12065
|
-
target,
|
|
12065
|
+
target: target2,
|
|
12066
12066
|
unrepresentable: params?.unrepresentable ?? "throw",
|
|
12067
12067
|
override: params?.override ?? (() => {
|
|
12068
12068
|
}),
|
|
@@ -12404,8 +12404,8 @@ var createToJSONSchemaMethod = (schema, processors = {}) => (params) => {
|
|
|
12404
12404
|
return finalize(ctx, schema);
|
|
12405
12405
|
};
|
|
12406
12406
|
var createStandardJSONSchemaMethod = (schema, io, processors = {}) => (params) => {
|
|
12407
|
-
const { libraryOptions, target } = params ?? {};
|
|
12408
|
-
const ctx = initializeContext({ ...libraryOptions ?? {}, target, io, processors });
|
|
12407
|
+
const { libraryOptions, target: target2 } = params ?? {};
|
|
12408
|
+
const ctx = initializeContext({ ...libraryOptions ?? {}, target: target2, io, processors });
|
|
12409
12409
|
process2(schema, ctx);
|
|
12410
12410
|
extractDefs(ctx, schema);
|
|
12411
12411
|
return finalize(ctx, schema);
|
|
@@ -13464,8 +13464,8 @@ var ZodType = /* @__PURE__ */ $constructor("ZodType", (inst, def) => {
|
|
|
13464
13464
|
catch(params) {
|
|
13465
13465
|
return _catch2(this, params);
|
|
13466
13466
|
},
|
|
13467
|
-
pipe(
|
|
13468
|
-
return pipe(this,
|
|
13467
|
+
pipe(target2) {
|
|
13468
|
+
return pipe(this, target2);
|
|
13469
13469
|
},
|
|
13470
13470
|
readonly() {
|
|
13471
13471
|
return readonly(this);
|
|
@@ -13986,8 +13986,8 @@ var ZodArray = /* @__PURE__ */ $constructor("ZodArray", (inst, def) => {
|
|
|
13986
13986
|
}
|
|
13987
13987
|
});
|
|
13988
13988
|
});
|
|
13989
|
-
function array(
|
|
13990
|
-
return _array(ZodArray,
|
|
13989
|
+
function array(element2, params) {
|
|
13990
|
+
return _array(ZodArray, element2, params);
|
|
13991
13991
|
}
|
|
13992
13992
|
function keyof(schema) {
|
|
13993
13993
|
const shape = schema._zod.def.shape;
|
|
@@ -15024,8 +15024,8 @@ function convertBaseSchema(schema, ctx) {
|
|
|
15024
15024
|
zodSchema = zodSchema.check(z.maxLength(schema.maxItems));
|
|
15025
15025
|
}
|
|
15026
15026
|
} else if (items !== void 0) {
|
|
15027
|
-
const
|
|
15028
|
-
let arraySchema = z.array(
|
|
15027
|
+
const element2 = convertSchema(items, ctx);
|
|
15028
|
+
let arraySchema = z.array(element2);
|
|
15029
15029
|
if (typeof schema.minItems === "number") {
|
|
15030
15030
|
arraySchema = arraySchema.min(schema.minItems);
|
|
15031
15031
|
}
|
|
@@ -15116,10 +15116,10 @@ function fromJSONSchema(schema, params) {
|
|
|
15116
15116
|
} catch {
|
|
15117
15117
|
throw new Error("fromJSONSchema input is not valid JSON (possibly cyclic); use $defs/$ref for recursive schemas");
|
|
15118
15118
|
}
|
|
15119
|
-
const
|
|
15119
|
+
const version3 = detectVersion(normalized, params?.defaultTarget);
|
|
15120
15120
|
const defs = normalized.$defs || normalized.definitions || {};
|
|
15121
15121
|
const ctx = {
|
|
15122
|
-
version:
|
|
15122
|
+
version: version3,
|
|
15123
15123
|
defs,
|
|
15124
15124
|
refs: /* @__PURE__ */ new Map(),
|
|
15125
15125
|
processing: /* @__PURE__ */ new Set(),
|
|
@@ -15160,12 +15160,14 @@ config(en_default());
|
|
|
15160
15160
|
// ../contracts/src/encoding.ts
|
|
15161
15161
|
var ENCODING_PART_BYTES = 4 * 1024 * 1024;
|
|
15162
15162
|
var ENCODING_MAX_BYTES = 1024 * 1024 * 1024;
|
|
15163
|
+
var ENCODING_MAX_SECONDS = 600;
|
|
15164
|
+
var ENCODING_MAX_FRAMES = 36e3;
|
|
15163
15165
|
var frameManifestSchema = external_exports.object({
|
|
15164
15166
|
version: external_exports.literal(1),
|
|
15165
|
-
durationSeconds: external_exports.number().positive().max(
|
|
15167
|
+
durationSeconds: external_exports.number().positive().max(ENCODING_MAX_SECONDS),
|
|
15166
15168
|
width: external_exports.number().int().min(320).max(3840),
|
|
15167
15169
|
height: external_exports.number().int().min(240).max(2160),
|
|
15168
|
-
frames: external_exports.array(external_exports.object({ timestamp: external_exports.number().nonnegative().finite(), size: external_exports.number().int().positive().max(ENCODING_PART_BYTES) }).strict()).min(1).max(
|
|
15170
|
+
frames: external_exports.array(external_exports.object({ timestamp: external_exports.number().nonnegative().finite(), size: external_exports.number().int().positive().max(ENCODING_PART_BYTES) }).strict()).min(1).max(ENCODING_MAX_FRAMES)
|
|
15169
15171
|
}).strict().superRefine((m, ctx) => {
|
|
15170
15172
|
if (m.frames.reduce((n, f) => n + f.size, 0) > ENCODING_MAX_BYTES) ctx.addIssue({ code: "custom", message: "Frame upload exceeds the trial byte limit." });
|
|
15171
15173
|
for (let i = 1; i < m.frames.length; i++) if (m.frames[i].timestamp < m.frames[i - 1].timestamp) ctx.addIssue({ code: "custom", message: "Frame timestamps must be ordered." });
|
|
@@ -15267,7 +15269,7 @@ async function waitForEncoding(client, id, timeoutMs = 30 * 6e4) {
|
|
|
15267
15269
|
import { spawn as spawn8 } from "child_process";
|
|
15268
15270
|
import { hostname as hostname4, tmpdir as tmpdir3 } from "os";
|
|
15269
15271
|
import { Command, CommanderError } from "commander";
|
|
15270
|
-
import { basename, dirname as
|
|
15272
|
+
import { basename, dirname as dirname3, join as join12 } from "path";
|
|
15271
15273
|
import { readdir as readdir2, rm as rm7, stat as stat5 } from "fs/promises";
|
|
15272
15274
|
|
|
15273
15275
|
// ../recorder/src/windows-ipc.ts
|
|
@@ -15354,10 +15356,10 @@ async function windowsIpcIdentity(directory = join5(rillDirectory(), "windows-ip
|
|
|
15354
15356
|
} catch (e) {
|
|
15355
15357
|
if (e.code !== "ENOENT") throw new Error("Cannot read the protected recorder identity.");
|
|
15356
15358
|
}
|
|
15357
|
-
const
|
|
15359
|
+
const identity2 = { version: 1, pipe: "\\\\.\\pipe\\rill-v3-" + randomBytes2(32).toString("hex"), token: randomBytes2(32).toString("hex") };
|
|
15358
15360
|
const temporary = join5(directory, randomBytes2(16).toString("hex") + ".tmp");
|
|
15359
15361
|
try {
|
|
15360
|
-
await writeFile2(temporary, JSON.stringify(
|
|
15362
|
+
await writeFile2(temporary, JSON.stringify(identity2), { mode: 384, flag: "wx" });
|
|
15361
15363
|
try {
|
|
15362
15364
|
await link(temporary, path);
|
|
15363
15365
|
} catch (e) {
|
|
@@ -15376,9 +15378,17 @@ import { spawn as spawn4 } from "child_process";
|
|
|
15376
15378
|
// ../recorder/src/browser-tabs.ts
|
|
15377
15379
|
import { chromium } from "playwright-core";
|
|
15378
15380
|
|
|
15381
|
+
// ../contracts/src/redaction.js
|
|
15382
|
+
var tokenValuePattern = /\b(?:sk|pk|ghp|github_pat|xox[baprs]|eyJ)[-_a-zA-Z0-9.]{16,}\b/g;
|
|
15383
|
+
function redactText(value, maxLength = 8e3) {
|
|
15384
|
+
return value.replace(tokenValuePattern, "[REDACTED]").slice(0, maxLength);
|
|
15385
|
+
}
|
|
15386
|
+
function redactDiagnosticText(value, maxLength = 2e3) {
|
|
15387
|
+
return redactText(value.replace(/(bearer\s+|["']?(?:password|secret|token|authorization|cookie|api[_-]?key)["']?\s*[:=]\s*["']?)[^\s"',;]+/gi, "$1[REDACTED]"), maxLength);
|
|
15388
|
+
}
|
|
15389
|
+
|
|
15379
15390
|
// ../recorder/src/redaction.ts
|
|
15380
15391
|
var secretKeyPattern = /(?:pass(?:word)?|secret|token|api[_-]?key|authorization|cookie|session|access[_-]?token|refresh[_-]?token|client[_-]?secret)/i;
|
|
15381
|
-
var tokenValuePattern = /\b(?:sk|pk|ghp|github_pat|xox[baprs]|eyJ)[-_a-zA-Z0-9.]{16,}\b/g;
|
|
15382
15392
|
function sanitizeUrl(input) {
|
|
15383
15393
|
try {
|
|
15384
15394
|
const url2 = new URL(input);
|
|
@@ -15391,9 +15401,6 @@ function sanitizeUrl(input) {
|
|
|
15391
15401
|
return "[invalid-url]";
|
|
15392
15402
|
}
|
|
15393
15403
|
}
|
|
15394
|
-
function redactText(value, maxLength = 8e3) {
|
|
15395
|
-
return value.replace(tokenValuePattern, "[REDACTED]").slice(0, maxLength);
|
|
15396
|
-
}
|
|
15397
15404
|
function redactBody(value, contentType, maxBytes = 65536) {
|
|
15398
15405
|
if (Buffer.byteLength(value) > maxBytes) return { body: void 0, omittedReason: "body_too_large" };
|
|
15399
15406
|
if (!isAllowedBodyType(contentType)) return { body: void 0, omittedReason: "unsupported_content_type" };
|
|
@@ -15428,16 +15435,16 @@ function validateTabSelection(options) {
|
|
|
15428
15435
|
}
|
|
15429
15436
|
async function tabs(browser) {
|
|
15430
15437
|
const result = [];
|
|
15431
|
-
for (const
|
|
15438
|
+
for (const context2 of browser.contexts()) for (const page of context2.pages()) {
|
|
15432
15439
|
if (page.isClosed()) continue;
|
|
15433
|
-
const session = await
|
|
15440
|
+
const session = await context2.newCDPSession(page).catch(() => null);
|
|
15434
15441
|
if (!session) {
|
|
15435
15442
|
if (page.isClosed()) continue;
|
|
15436
15443
|
throw new Error("browser_unavailable: Could not inspect an open tab.");
|
|
15437
15444
|
}
|
|
15438
15445
|
try {
|
|
15439
15446
|
const { targetInfo } = await session.send("Target.getTargetInfo");
|
|
15440
|
-
result.push({ page, context, tab: { tabId: targetInfo.targetId, url: /^(?:https?:|about:)/i.test(page.url()) ? sanitizeUrl(page.url()) : `[${page.url().split(":", 1)[0]} URL omitted]`, title: redactText(targetInfo.title, 200) } });
|
|
15447
|
+
result.push({ page, context: context2, tab: { tabId: targetInfo.targetId, url: /^(?:https?:|about:)/i.test(page.url()) ? sanitizeUrl(page.url()) : `[${page.url().split(":", 1)[0]} URL omitted]`, title: redactText(targetInfo.title, 200) } });
|
|
15441
15448
|
} catch (error51) {
|
|
15442
15449
|
if (!page.isClosed()) throw error51;
|
|
15443
15450
|
} finally {
|
|
@@ -15472,8 +15479,9 @@ async function connectRecordingTab(cdpUrl, outputDirectory, tabId) {
|
|
|
15472
15479
|
}
|
|
15473
15480
|
|
|
15474
15481
|
// ../recorder/src/client.ts
|
|
15475
|
-
async function ensureDaemon(cliEntryPath, requireTabSelection = false) {
|
|
15482
|
+
async function ensureDaemon(cliEntryPath, requireTabSelection = false, requireChromeConnection = false) {
|
|
15476
15483
|
const check2 = (health2) => {
|
|
15484
|
+
if (requireChromeConnection && !health2.capabilities?.includes("chrome-connection-v1")) throw new Error("browser_unavailable: Finish your current recordings, then restart Rill to connect Chrome.");
|
|
15477
15485
|
if (requireTabSelection && !health2.capabilities?.includes("tab-selection-v1")) throw new Error("browser_unavailable: The running Rill recorder predates tab selection. Stop active recordings, then restart the recorder service before attaching.");
|
|
15478
15486
|
};
|
|
15479
15487
|
let health;
|
|
@@ -15505,6 +15513,15 @@ async function ensureDaemon(cliEntryPath, requireTabSelection = false) {
|
|
|
15505
15513
|
}
|
|
15506
15514
|
throw new Error("The local recorder service did not start.");
|
|
15507
15515
|
}
|
|
15516
|
+
function connectBrowser() {
|
|
15517
|
+
return callDaemon("POST", "/browser/connect", {});
|
|
15518
|
+
}
|
|
15519
|
+
function browserConnectionStatus() {
|
|
15520
|
+
return callDaemon("GET", "/browser/status");
|
|
15521
|
+
}
|
|
15522
|
+
function disconnectBrowser() {
|
|
15523
|
+
return callDaemon("POST", "/browser/disconnect", {});
|
|
15524
|
+
}
|
|
15508
15525
|
function startRecording(input) {
|
|
15509
15526
|
return callDaemon("POST", "/sessions", input);
|
|
15510
15527
|
}
|
|
@@ -15517,13 +15534,13 @@ function recordingStatus(recordingId) {
|
|
|
15517
15534
|
var windowsIdentityPromise;
|
|
15518
15535
|
async function callDaemon(method, path, body) {
|
|
15519
15536
|
if (process.platform === "win32" && process.env.RILL_SOCKET) throw new Error("RILL_SOCKET overrides are not supported by authenticated Windows IPC. Unset it before recording.");
|
|
15520
|
-
const
|
|
15537
|
+
const identity2 = process.platform === "win32" ? await (windowsIdentityPromise ??= windowsIpcIdentity().catch((error51) => {
|
|
15521
15538
|
windowsIdentityPromise = void 0;
|
|
15522
15539
|
throw error51;
|
|
15523
15540
|
})) : void 0;
|
|
15524
15541
|
return new Promise((resolve, reject) => {
|
|
15525
15542
|
const payload = body === void 0 ? void 0 : Buffer.from(JSON.stringify(body));
|
|
15526
|
-
const outgoing = request2({ socketPath:
|
|
15543
|
+
const outgoing = request2({ socketPath: identity2?.pipe ?? daemonSocketPath(), method, path, headers: { ...payload ? { "Content-Type": "application/json", "Content-Length": payload.length } : {}, ...identity2 ? { Authorization: "Bearer " + identity2.token } : {} } }, (incoming) => {
|
|
15527
15544
|
const chunks = [];
|
|
15528
15545
|
incoming.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
|
|
15529
15546
|
incoming.on("end", () => {
|
|
@@ -15543,6 +15560,20 @@ async function callDaemon(method, path, body) {
|
|
|
15543
15560
|
});
|
|
15544
15561
|
}
|
|
15545
15562
|
|
|
15563
|
+
// src/browser-recording.ts
|
|
15564
|
+
async function prepareBrowserRecording(options, getConnection) {
|
|
15565
|
+
if (!["existing", "new"].includes(options.browser ?? "new")) throw new Error("validation_failed: Choose an existing tab or a new browser.");
|
|
15566
|
+
if (!["human", "agent"].includes(options.operator ?? "human")) throw new Error("validation_failed: Choose who will perform the steps: human or agent.");
|
|
15567
|
+
if (options.cdpUrl || (options.browser ?? "new") === "new") return void 0;
|
|
15568
|
+
const connection = await getConnection();
|
|
15569
|
+
if (!connection.connected || !connection.tab) throw Object.assign(new Error("browser_unavailable: Connect Chrome and choose the tab you want to record."), { recovery: "Run rill browser connect, then choose your tab in Rill for Chrome." });
|
|
15570
|
+
if (options.url || options.tabId) throw new Error("validation_failed: Open the page you want to record, then choose it in Rill for Chrome.");
|
|
15571
|
+
if (options.encoding && options.encoding !== "remote") throw new Error("validation_failed: Connected Chrome uses the standard recording upload.");
|
|
15572
|
+
if (options.operator === "agent" && !options.controllerTarget) throw new Error("browser_unavailable: Confirm your agent can control the selected tab before recording. Use its target ID with --controller-target, or use --operator human to perform the steps yourself.");
|
|
15573
|
+
if (options.controllerTarget && options.controllerTarget !== connection.tab.targetId) throw new Error("browser_unavailable: Your agent is using a different tab. Select the same tab before recording.");
|
|
15574
|
+
return connection;
|
|
15575
|
+
}
|
|
15576
|
+
|
|
15546
15577
|
// ../recorder/src/retention.ts
|
|
15547
15578
|
import { readdir, readFile as readFile5, rm as rm4 } from "fs/promises";
|
|
15548
15579
|
import { join as join6 } from "path";
|
|
@@ -15574,9 +15605,9 @@ async function hasRetainedRemoteCaptures(root) {
|
|
|
15574
15605
|
}
|
|
15575
15606
|
|
|
15576
15607
|
// ../recorder/src/daemon.ts
|
|
15577
|
-
import { createServer as
|
|
15578
|
-
import { mkdir as
|
|
15579
|
-
import { dirname, join as
|
|
15608
|
+
import { createServer as createServer3 } from "http";
|
|
15609
|
+
import { mkdir as mkdir9, readFile as readFile8, unlink, writeFile as writeFile8, rename as rename2, stat as stat4, chmod } from "fs/promises";
|
|
15610
|
+
import { dirname as dirname2, join as join11 } from "path";
|
|
15580
15611
|
|
|
15581
15612
|
// ../recorder/src/click-capture.ts
|
|
15582
15613
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
@@ -15604,16 +15635,16 @@ async function captureClicks(page, session, onClick) {
|
|
|
15604
15635
|
let identifier;
|
|
15605
15636
|
session.on("Runtime.bindingCalled", receive);
|
|
15606
15637
|
const cleanup = async () => {
|
|
15607
|
-
session.off("Runtime.bindingCalled", receive);
|
|
15608
15638
|
if (identifier) await session.send("Page.removeScriptToEvaluateOnNewDocument", { identifier }).catch(() => void 0);
|
|
15609
15639
|
await Promise.all(page.frames().map((frame) => frame.evaluate((key2) => {
|
|
15610
|
-
const
|
|
15611
|
-
const listener =
|
|
15640
|
+
const target2 = window;
|
|
15641
|
+
const listener = target2[key2 + "_listener"];
|
|
15612
15642
|
if (typeof listener === "function") document.removeEventListener("click", listener, { capture: true });
|
|
15613
|
-
delete
|
|
15614
|
-
delete
|
|
15643
|
+
delete target2[key2 + "_listener"];
|
|
15644
|
+
delete target2[key2];
|
|
15615
15645
|
}, binding).catch(() => void 0)));
|
|
15616
15646
|
await session.send("Runtime.removeBinding", { name: binding }).catch(() => void 0);
|
|
15647
|
+
session.off("Runtime.bindingCalled", receive);
|
|
15617
15648
|
};
|
|
15618
15649
|
try {
|
|
15619
15650
|
await session.send("Runtime.enable");
|
|
@@ -15800,7 +15831,8 @@ var interactionReportFields = {
|
|
|
15800
15831
|
cleanup: external_exports.literal("not_attempted").optional()
|
|
15801
15832
|
};
|
|
15802
15833
|
var cloudRunPrompt = external_exports.string().min(1).max(2e3).refine((value) => Boolean(value.trim()), "Describe what you want Rill to do.");
|
|
15803
|
-
var
|
|
15834
|
+
var practiceReference = external_exports.object({ missionId: external_exports.string().regex(/^[a-z]+-[a-z]+$/).max(60), version: external_exports.literal(1) }).strict();
|
|
15835
|
+
var cloudSubmission = external_exports.object({ ...interactionFields, id: external_exports.string().uuid(), url: cloudRunTarget, prompt: cloudRunPrompt, expectation: external_exports.string().trim().min(1).max(1e3).optional(), account: external_exports.string().regex(/^[a-z0-9-]{1,60}$/).optional(), practice: practiceReference.optional() }).strict();
|
|
15804
15836
|
var siteAccountSchema = external_exports.object({
|
|
15805
15837
|
name: external_exports.string().regex(/^[a-z0-9-]{1,60}$/),
|
|
15806
15838
|
origin: publicTarget.refine((v) => new URL(v).protocol === "https:" && new URL(v).origin === v, "Use an exact HTTPS origin."),
|
|
@@ -15994,11 +16026,11 @@ function installCursorOverlay(options) {
|
|
|
15994
16026
|
positions.set(document, { x: window.scrollX, y: window.scrollY });
|
|
15995
16027
|
document.addEventListener("scroll", (event) => {
|
|
15996
16028
|
if (!event.isTrusted || document.hidden) return;
|
|
15997
|
-
const
|
|
15998
|
-
if (
|
|
15999
|
-
const position =
|
|
16000
|
-
const previous = positions.get(
|
|
16001
|
-
positions.set(
|
|
16029
|
+
const target2 = event.target;
|
|
16030
|
+
if (target2 !== document && !(target2 instanceof Element)) return;
|
|
16031
|
+
const position = target2 === document ? { x: window.scrollX, y: window.scrollY } : { x: target2.scrollLeft, y: target2.scrollTop };
|
|
16032
|
+
const previous = positions.get(target2);
|
|
16033
|
+
positions.set(target2, position);
|
|
16002
16034
|
const dx = previous ? position.x - previous.x : 0;
|
|
16003
16035
|
const dy = previous ? position.y - previous.y : 0;
|
|
16004
16036
|
if (previous && !dx && !dy) return;
|
|
@@ -16029,13 +16061,13 @@ function installCursorOverlay(options) {
|
|
|
16029
16061
|
document.addEventListener("pointerover", move, listenerOptions);
|
|
16030
16062
|
document.addEventListener("pointerup", release, listenerOptions);
|
|
16031
16063
|
document.addEventListener("pointercancel", release, listenerOptions);
|
|
16032
|
-
const animateFeedback = (
|
|
16033
|
-
root.insertBefore(
|
|
16034
|
-
const animation =
|
|
16064
|
+
const animateFeedback = (element2, frames, duration3, easing) => {
|
|
16065
|
+
root.insertBefore(element2, pointer);
|
|
16066
|
+
const animation = element2.animate(frames, { duration: duration3, easing });
|
|
16035
16067
|
animations.add(animation);
|
|
16036
16068
|
void animation.finished.catch(() => void 0).finally(() => {
|
|
16037
16069
|
animations.delete(animation);
|
|
16038
|
-
|
|
16070
|
+
element2.remove();
|
|
16039
16071
|
});
|
|
16040
16072
|
};
|
|
16041
16073
|
document.addEventListener("pointerdown", (event) => {
|
|
@@ -16126,8 +16158,10 @@ async function writeBundle(bundle, outputPath) {
|
|
|
16126
16158
|
const output = createWriteStream(outputPath, { mode: 384 });
|
|
16127
16159
|
const gzip = createGzip({ level: 9 });
|
|
16128
16160
|
gzip.pipe(output);
|
|
16129
|
-
const { console: consoleEvents, network, timeline, ...manifest } = bundle;
|
|
16161
|
+
const { console: consoleEvents, network, timeline, actions, ...manifest } = bundle;
|
|
16130
16162
|
gzip.write(`${JSON.stringify({ kind: "manifest", ...manifest })}
|
|
16163
|
+
`);
|
|
16164
|
+
for (const event of actions ?? []) gzip.write(`${JSON.stringify(event)}
|
|
16131
16165
|
`);
|
|
16132
16166
|
for (const event of consoleEvents) gzip.write(`${JSON.stringify({ kind: "console", ...event })}
|
|
16133
16167
|
`);
|
|
@@ -16139,6 +16173,203 @@ async function writeBundle(bundle, outputPath) {
|
|
|
16139
16173
|
await once(output, "close");
|
|
16140
16174
|
}
|
|
16141
16175
|
|
|
16176
|
+
// ../contracts/src/browser.ts
|
|
16177
|
+
var browserContractVersion = 1;
|
|
16178
|
+
var identity = external_exports.string().min(1).max(100);
|
|
16179
|
+
var element = external_exports.number().int().min(1).max(100);
|
|
16180
|
+
var version2 = { schemaVersion: external_exports.literal(browserContractVersion) };
|
|
16181
|
+
var target = { observationId: identity, element };
|
|
16182
|
+
var browserActionSchema = external_exports.enum(["observe", "click", "fill", "check", "select", "press", "navigate", "scroll", "finish"]);
|
|
16183
|
+
var browserCommandSchema = external_exports.discriminatedUnion("action", [
|
|
16184
|
+
external_exports.object({ ...version2, action: external_exports.literal("observe") }).strict(),
|
|
16185
|
+
external_exports.object({ ...version2, action: external_exports.literal("click"), ...target }).strict(),
|
|
16186
|
+
external_exports.object({ ...version2, action: external_exports.literal("fill"), ...target, value: external_exports.string().max(2e3) }).strict(),
|
|
16187
|
+
external_exports.object({ ...version2, action: external_exports.literal("check"), ...target, checked: external_exports.boolean() }).strict(),
|
|
16188
|
+
external_exports.object({ ...version2, action: external_exports.literal("select"), ...target, value: external_exports.string().max(200) }).strict(),
|
|
16189
|
+
external_exports.object({ ...version2, action: external_exports.literal("press"), ...target, key: external_exports.enum(["Enter", "Tab", "ArrowDown", "ArrowUp", "Escape"]) }).strict(),
|
|
16190
|
+
external_exports.object({ ...version2, action: external_exports.literal("navigate"), url: external_exports.string().url().max(2048) }).strict(),
|
|
16191
|
+
external_exports.object({ ...version2, action: external_exports.literal("scroll"), observationId: identity, direction: external_exports.enum(["up", "down"]) }).strict(),
|
|
16192
|
+
external_exports.object({ ...version2, action: external_exports.literal("finish"), outcome: external_exports.enum(["walkthrough", "passed", "failed", "inconclusive"]), summary: external_exports.string().min(1).max(1e3), evidence: external_exports.string().max(1e3) }).strict()
|
|
16193
|
+
]);
|
|
16194
|
+
var context = { recordingId: identity, pageId: identity, frameId: external_exports.literal("main"), documentId: identity };
|
|
16195
|
+
var browserObservationSchema = external_exports.object({
|
|
16196
|
+
...version2,
|
|
16197
|
+
kind: external_exports.literal("observation"),
|
|
16198
|
+
...context,
|
|
16199
|
+
observationId: identity,
|
|
16200
|
+
offsetMs: external_exports.number().nonnegative(),
|
|
16201
|
+
url: external_exports.string(),
|
|
16202
|
+
title: external_exports.string().max(500),
|
|
16203
|
+
text: external_exports.string().max(12e3),
|
|
16204
|
+
viewport: external_exports.object({ width: external_exports.number().nonnegative(), height: external_exports.number().nonnegative(), scrollX: external_exports.number(), scrollY: external_exports.number(), documentHeight: external_exports.number().nonnegative(), text: external_exports.string().max(12e3), textTruncated: external_exports.boolean(), evidence: external_exports.literal("dom_geometry") }).strict(),
|
|
16205
|
+
elements: external_exports.array(external_exports.object({
|
|
16206
|
+
kind: external_exports.literal("dom"),
|
|
16207
|
+
id: element,
|
|
16208
|
+
tag: external_exports.string(),
|
|
16209
|
+
label: external_exports.string().max(160),
|
|
16210
|
+
type: external_exports.string().nullable(),
|
|
16211
|
+
role: external_exports.string().nullable(),
|
|
16212
|
+
disabled: external_exports.boolean(),
|
|
16213
|
+
checked: external_exports.union([external_exports.boolean(), external_exports.string(), external_exports.null()]),
|
|
16214
|
+
options: external_exports.array(external_exports.object({ value: external_exports.string().max(200), label: external_exports.string().max(100), selected: external_exports.boolean() }).strict()).max(50).optional(),
|
|
16215
|
+
optionsTruncated: external_exports.boolean().optional(),
|
|
16216
|
+
validation: external_exports.string().max(160).optional()
|
|
16217
|
+
}).strict()).max(100),
|
|
16218
|
+
completeness: external_exports.object({
|
|
16219
|
+
scope: external_exports.literal("main_document"),
|
|
16220
|
+
childFrames: external_exports.literal("excluded"),
|
|
16221
|
+
shadowRoots: external_exports.literal("excluded"),
|
|
16222
|
+
targetsTruncated: external_exports.boolean(),
|
|
16223
|
+
textTruncated: external_exports.boolean(),
|
|
16224
|
+
targetLimit: external_exports.literal(100),
|
|
16225
|
+
textLimit: external_exports.literal(12e3)
|
|
16226
|
+
}).strict(),
|
|
16227
|
+
blockedRequests: external_exports.number().int().nonnegative(),
|
|
16228
|
+
submissionOutcomeUnknown: external_exports.boolean()
|
|
16229
|
+
}).strict();
|
|
16230
|
+
var browserErrorSchema = external_exports.enum([
|
|
16231
|
+
"invalid_browser_command",
|
|
16232
|
+
"browser_busy",
|
|
16233
|
+
"run_finished",
|
|
16234
|
+
"stale_observation",
|
|
16235
|
+
"stale_target",
|
|
16236
|
+
"observation_unavailable",
|
|
16237
|
+
"interaction_required",
|
|
16238
|
+
"consequential_action_blocked",
|
|
16239
|
+
"unsupported_control",
|
|
16240
|
+
"target_not_allowed",
|
|
16241
|
+
"submission_outcome_unknown",
|
|
16242
|
+
"action_outcome_unknown",
|
|
16243
|
+
"browser_action_unavailable",
|
|
16244
|
+
"verification_requires_expectation_and_evidence"
|
|
16245
|
+
]);
|
|
16246
|
+
var browserActionReceiptSchema = external_exports.object({
|
|
16247
|
+
...version2,
|
|
16248
|
+
kind: external_exports.literal("action"),
|
|
16249
|
+
...context,
|
|
16250
|
+
actionId: identity,
|
|
16251
|
+
action: browserActionSchema.nullable(),
|
|
16252
|
+
inputObservationId: identity.nullable(),
|
|
16253
|
+
target: external_exports.object({ kind: external_exports.literal("dom"), element }).strict().optional(),
|
|
16254
|
+
startedAtMs: external_exports.number().nonnegative(),
|
|
16255
|
+
finishedAtMs: external_exports.number().nonnegative(),
|
|
16256
|
+
status: external_exports.enum(["completed", "rejected", "failed"]),
|
|
16257
|
+
effectState: external_exports.enum(["none", "dispatched", "unknown"]),
|
|
16258
|
+
next: external_exports.enum(["observe", "inspect_only", "finished"]),
|
|
16259
|
+
errorCode: browserErrorSchema.optional(),
|
|
16260
|
+
blockedRequests: external_exports.number().int().nonnegative()
|
|
16261
|
+
}).strict().refine((value) => value.finishedAtMs >= value.startedAtMs, "Receipt timestamps must be ordered").refine((value) => value.status !== "rejected" || value.effectState === "none", "Rejected actions cannot have effects").refine((value) => value.status === "completed" === (value.errorCode === void 0), "Errors require a non-completed status").refine((value) => value.effectState !== "unknown" || value.next === "inspect_only", "Unknown effects require inspection");
|
|
16262
|
+
|
|
16263
|
+
// ../contracts/src/diagnostics.ts
|
|
16264
|
+
var diagnosticSeveritySchema = external_exports.enum(["debug", "info", "warning", "error"]);
|
|
16265
|
+
var consoleEventSchema = external_exports.object({
|
|
16266
|
+
id: external_exports.string(),
|
|
16267
|
+
offsetMs: external_exports.number().nonnegative(),
|
|
16268
|
+
severity: diagnosticSeveritySchema,
|
|
16269
|
+
text: external_exports.string(),
|
|
16270
|
+
sourceUrl: external_exports.string().optional(),
|
|
16271
|
+
lineNumber: external_exports.number().optional(),
|
|
16272
|
+
stack: external_exports.array(external_exports.string()).default([])
|
|
16273
|
+
});
|
|
16274
|
+
var networkEventSchema = external_exports.object({
|
|
16275
|
+
id: external_exports.string(),
|
|
16276
|
+
offsetMs: external_exports.number().nonnegative(),
|
|
16277
|
+
method: external_exports.string(),
|
|
16278
|
+
url: external_exports.string(),
|
|
16279
|
+
resourceType: external_exports.string(),
|
|
16280
|
+
status: external_exports.number().int().optional(),
|
|
16281
|
+
mimeType: external_exports.string().optional(),
|
|
16282
|
+
durationMs: external_exports.number().nonnegative().optional(),
|
|
16283
|
+
transferSize: external_exports.number().nonnegative().optional(),
|
|
16284
|
+
failureReason: external_exports.string().optional(),
|
|
16285
|
+
completedOffsetMs: external_exports.number().nonnegative().optional(),
|
|
16286
|
+
pageUrl: external_exports.string().optional(),
|
|
16287
|
+
blockedBy: external_exports.enum(["rill"]).optional(),
|
|
16288
|
+
blockReason: external_exports.enum(["write_policy", "navigation_policy", "interception_error"]).optional(),
|
|
16289
|
+
requestBody: external_exports.string().optional(),
|
|
16290
|
+
responseBody: external_exports.string().optional(),
|
|
16291
|
+
bodyOmittedReason: external_exports.string().optional()
|
|
16292
|
+
});
|
|
16293
|
+
var timelineEventSchema = external_exports.object({
|
|
16294
|
+
id: external_exports.string(),
|
|
16295
|
+
offsetMs: external_exports.number().nonnegative(),
|
|
16296
|
+
type: external_exports.enum(["navigation", "click", "console", "network", "exception", "lifecycle"]),
|
|
16297
|
+
label: external_exports.string(),
|
|
16298
|
+
severity: diagnosticSeveritySchema.optional()
|
|
16299
|
+
});
|
|
16300
|
+
var browserActionEventSchema = external_exports.object({
|
|
16301
|
+
schemaVersion: external_exports.literal(1),
|
|
16302
|
+
kind: external_exports.literal("browser_action"),
|
|
16303
|
+
id: external_exports.string().regex(/^action_[1-9][0-9]{0,3}$/),
|
|
16304
|
+
offsetMs: external_exports.number().int().min(0).max(864e5),
|
|
16305
|
+
finishedAtMs: external_exports.number().int().min(0).max(864e5),
|
|
16306
|
+
action: browserActionSchema.nullable(),
|
|
16307
|
+
status: external_exports.enum(["completed", "rejected", "failed"]),
|
|
16308
|
+
effectState: external_exports.enum(["none", "dispatched", "unknown"]),
|
|
16309
|
+
next: external_exports.enum(["observe", "inspect_only", "finished"]),
|
|
16310
|
+
errorCode: browserErrorSchema.optional(),
|
|
16311
|
+
blockedRequests: external_exports.number().int().nonnegative()
|
|
16312
|
+
}).strict().refine((event) => event.finishedAtMs >= event.offsetMs, "Action timestamps must be ordered").refine((event) => event.status !== "rejected" || event.effectState === "none", "Rejected actions cannot have effects").refine((event) => event.status === "completed" === (event.errorCode === void 0), "Errors require a non-completed status").refine((event) => event.effectState !== "unknown" || event.next === "inspect_only", "Unknown effects require inspection");
|
|
16313
|
+
var diagnosticBundleSchema = external_exports.object({
|
|
16314
|
+
schemaVersion: external_exports.literal(1),
|
|
16315
|
+
capturePolicyVersion: external_exports.string(),
|
|
16316
|
+
videoStartOffsetMs: external_exports.number().optional(),
|
|
16317
|
+
recordingId: external_exports.string(),
|
|
16318
|
+
startedAt: external_exports.string().datetime(),
|
|
16319
|
+
durationMs: external_exports.number().nonnegative(),
|
|
16320
|
+
bodyCaptureEnabled: external_exports.boolean(),
|
|
16321
|
+
bodyCaptureRedacted: external_exports.boolean(),
|
|
16322
|
+
truncated: external_exports.boolean(),
|
|
16323
|
+
actions: external_exports.array(browserActionEventSchema).max(1e3).optional(),
|
|
16324
|
+
actionsTruncated: external_exports.boolean().optional(),
|
|
16325
|
+
console: external_exports.array(consoleEventSchema),
|
|
16326
|
+
network: external_exports.array(networkEventSchema),
|
|
16327
|
+
timeline: external_exports.array(timelineEventSchema)
|
|
16328
|
+
});
|
|
16329
|
+
|
|
16330
|
+
// ../recorder/src/browser-actions.ts
|
|
16331
|
+
var BrowserActionCapture = class {
|
|
16332
|
+
constructor(recordingId) {
|
|
16333
|
+
this.recordingId = recordingId;
|
|
16334
|
+
}
|
|
16335
|
+
recordingId;
|
|
16336
|
+
events = [];
|
|
16337
|
+
truncated = false;
|
|
16338
|
+
seen = /* @__PURE__ */ new Set();
|
|
16339
|
+
add(input) {
|
|
16340
|
+
const parsed = browserActionReceiptSchema.safeParse(input);
|
|
16341
|
+
if (!parsed.success || parsed.data.recordingId !== this.recordingId) {
|
|
16342
|
+
this.truncated = true;
|
|
16343
|
+
return;
|
|
16344
|
+
}
|
|
16345
|
+
const receipt = parsed.data;
|
|
16346
|
+
if (this.seen.has(receipt.actionId)) return;
|
|
16347
|
+
if (this.events.length >= 1e3) {
|
|
16348
|
+
this.truncated = true;
|
|
16349
|
+
return;
|
|
16350
|
+
}
|
|
16351
|
+
const event = browserActionEventSchema.safeParse({
|
|
16352
|
+
schemaVersion: 1,
|
|
16353
|
+
kind: "browser_action",
|
|
16354
|
+
id: `action_${this.events.length + 1}`,
|
|
16355
|
+
offsetMs: Math.round(receipt.startedAtMs),
|
|
16356
|
+
finishedAtMs: Math.round(receipt.finishedAtMs),
|
|
16357
|
+
action: receipt.action,
|
|
16358
|
+
status: receipt.status,
|
|
16359
|
+
effectState: receipt.effectState,
|
|
16360
|
+
next: receipt.next,
|
|
16361
|
+
...receipt.errorCode ? { errorCode: receipt.errorCode } : {},
|
|
16362
|
+
blockedRequests: receipt.blockedRequests
|
|
16363
|
+
});
|
|
16364
|
+
if (!event.success) {
|
|
16365
|
+
this.truncated = true;
|
|
16366
|
+
return;
|
|
16367
|
+
}
|
|
16368
|
+
this.seen.add(receipt.actionId);
|
|
16369
|
+
this.events.push(event.data);
|
|
16370
|
+
}
|
|
16371
|
+
};
|
|
16372
|
+
|
|
16142
16373
|
// ../recorder/src/environment.ts
|
|
16143
16374
|
import { existsSync } from "fs";
|
|
16144
16375
|
import { win32 as win323 } from "path";
|
|
@@ -16194,18 +16425,34 @@ async function encodeFrames(directory, manifest, output, signal) {
|
|
|
16194
16425
|
return withEncodingSlot(async () => {
|
|
16195
16426
|
signal?.throwIfAborted();
|
|
16196
16427
|
const lines = ["ffconcat version 1.0"];
|
|
16197
|
-
|
|
16198
|
-
|
|
16199
|
-
|
|
16200
|
-
|
|
16428
|
+
if (output.endsWith(".mp4")) {
|
|
16429
|
+
for (let i = 0; i < manifest.frames.length; i++) {
|
|
16430
|
+
const next = manifest.frames[i + 1];
|
|
16431
|
+
const duration3 = next ? next.timestamp - manifest.frames[i].timestamp : manifest.durationSeconds - (manifest.frames[i].timestamp - manifest.frames[0].timestamp);
|
|
16432
|
+
lines.push(`file 'frame-${String(i).padStart(6, "0")}.jpg'`, `duration ${Math.max(1 / 60, duration3).toFixed(6)}`);
|
|
16433
|
+
}
|
|
16434
|
+
lines.push(`file 'frame-${String(manifest.frames.length - 1).padStart(6, "0")}.jpg'`);
|
|
16435
|
+
} else {
|
|
16436
|
+
const endTick = Math.max(0, Math.ceil(manifest.durationSeconds * 25) - 2);
|
|
16437
|
+
const frames = [];
|
|
16438
|
+
manifest.frames.forEach((frame, index) => {
|
|
16439
|
+
const tick = Math.min(endTick, Math.max(0, Math.round((frame.timestamp - manifest.frames[0].timestamp) * 25)));
|
|
16440
|
+
if (frames.at(-1)?.tick === tick) frames[frames.length - 1] = { index, tick };
|
|
16441
|
+
else frames.push({ index, tick });
|
|
16442
|
+
});
|
|
16443
|
+
if (frames.at(-1).tick < endTick) frames.push({ ...frames.at(-1), tick: endTick });
|
|
16444
|
+
frames.forEach((frame, index) => {
|
|
16445
|
+
lines.push(`file 'frame-${String(frame.index).padStart(6, "0")}.jpg'`);
|
|
16446
|
+
if (frames[index + 1]) lines.push(`duration ${((frames[index + 1].tick - frame.tick) / 25).toFixed(6)}`);
|
|
16447
|
+
});
|
|
16201
16448
|
}
|
|
16202
|
-
lines.push(`file 'frame-${String(manifest.frames.length - 1).padStart(6, "0")}.jpg'`);
|
|
16203
16449
|
await writeFile3(join7(directory, "frames.ffconcat"), lines.join("\n") + "\n", { mode: 384 });
|
|
16204
16450
|
const threads = Number(process.env.RILL_FFMPEG_THREADS ?? 1);
|
|
16205
16451
|
if (!Number.isInteger(threads) || threads < 1 || threads > 16) throw new Error("Invalid RILL_FFMPEG_THREADS");
|
|
16206
16452
|
signal?.throwIfAborted();
|
|
16207
|
-
const
|
|
16208
|
-
const
|
|
16453
|
+
const width = Math.ceil(manifest.width / 2) * 2, height = Math.ceil(manifest.height / 2) * 2;
|
|
16454
|
+
const dimensions = `scale=${width}:${height}:force_original_aspect_ratio=decrease:force_divisible_by=2,pad=${width}:${height}:(ow-iw)/2:(oh-ih)/2,setsar=1`;
|
|
16455
|
+
const filters = output.endsWith(".mp4") ? `${dimensions},fps=30,tpad=stop_mode=clone:stop_duration=1` : `${dimensions},fps=25`;
|
|
16209
16456
|
const codec2 = output.endsWith(".mp4") ? ["-c:v", "libx264", "-preset", "veryfast", "-crf", "23", "-movflags", "+faststart", "-fps_mode", "cfr", "-t", String(Math.min(60, manifest.durationSeconds))] : ["-c:v", "libvpx-vp9", "-deadline", "good", "-cpu-used", "4", "-b:v", "0", "-crf", "32"];
|
|
16210
16457
|
await new Promise((resolve, reject) => {
|
|
16211
16458
|
const child = spawn5(process.env.RILL_FFMPEG ?? "ffmpeg", ["-hide_banner", "-loglevel", "error", "-protocol_whitelist", "file", "-f", "concat", "-safe", "1", "-i", "frames.ffconcat", "-vsync", "vfr", "-pix_fmt", "yuv420p", "-vf", filters, ...codec2, "-threads", String(threads), "-y", output], { cwd: directory, windowsHide: true, stdio: ["ignore", "ignore", "pipe"] });
|
|
@@ -16250,12 +16497,27 @@ var ScreencastRecorder = class {
|
|
|
16250
16497
|
frames = [];
|
|
16251
16498
|
pendingWrites = /* @__PURE__ */ new Set();
|
|
16252
16499
|
started = false;
|
|
16500
|
+
firstFrameEpochMs;
|
|
16501
|
+
get videoStartedAtMs() {
|
|
16502
|
+
return this.firstFrameEpochMs;
|
|
16503
|
+
}
|
|
16253
16504
|
frameCounter = 0;
|
|
16254
16505
|
frameBytes = 0;
|
|
16255
16506
|
captureError;
|
|
16507
|
+
frameListener;
|
|
16508
|
+
async waitForFirstFrame(timeoutMs = 8e3) {
|
|
16509
|
+
const deadline = Date.now() + timeoutMs;
|
|
16510
|
+
while (!this.frames.length) {
|
|
16511
|
+
if (this.captureError) throw this.captureError;
|
|
16512
|
+
if (Date.now() >= deadline) throw new Error("browser_unavailable: No picture received. Keep the selected tab open and try again.");
|
|
16513
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
16514
|
+
}
|
|
16515
|
+
}
|
|
16256
16516
|
async start() {
|
|
16257
16517
|
await mkdir5(this.directory, { recursive: true, mode: 448 });
|
|
16258
|
-
this.
|
|
16518
|
+
this.frameListener = (payload) => {
|
|
16519
|
+
const frameEpochMs = (payload.metadata.timestamp ?? Date.now() / 1e3) * 1e3;
|
|
16520
|
+
this.firstFrameEpochMs = Math.min(this.firstFrameEpochMs ?? frameEpochMs, frameEpochMs);
|
|
16259
16521
|
const bytes = Buffer.from(payload.data, "base64");
|
|
16260
16522
|
if (this.frameCounter >= 36e3 || this.frameBytes + bytes.length > ENCODING_MAX_BYTES || bytes.length > ENCODING_PART_BYTES) {
|
|
16261
16523
|
this.captureError = new Error("Capture exceeds the remote encoding frame or byte limit.");
|
|
@@ -16266,7 +16528,7 @@ var ScreencastRecorder = class {
|
|
|
16266
16528
|
const path = join8(this.directory, `frame-${String(this.frameCounter).padStart(6, "0")}.jpg`);
|
|
16267
16529
|
this.frameCounter += 1;
|
|
16268
16530
|
const write = writeFile4(path, bytes, { mode: 384 }).then(() => {
|
|
16269
|
-
this.frames.push({ path, timestamp:
|
|
16531
|
+
this.frames.push({ path, timestamp: frameEpochMs / 1e3 });
|
|
16270
16532
|
}).catch((error51) => {
|
|
16271
16533
|
this.captureError = error51 instanceof Error ? error51 : new Error(String(error51));
|
|
16272
16534
|
}).finally(() => {
|
|
@@ -16274,14 +16536,17 @@ var ScreencastRecorder = class {
|
|
|
16274
16536
|
void this.session.send("Page.screencastFrameAck", { sessionId: payload.sessionId }).catch(() => void 0);
|
|
16275
16537
|
});
|
|
16276
16538
|
this.pendingWrites.add(write);
|
|
16277
|
-
}
|
|
16539
|
+
};
|
|
16540
|
+
this.session.on("Page.screencastFrame", this.frameListener);
|
|
16278
16541
|
await this.session.send("Page.enable");
|
|
16279
|
-
|
|
16542
|
+
const options = { format: "jpeg", quality: 82, maxWidth: this.width, maxHeight: this.height, everyNthFrame: 1, sendLastFrame: true };
|
|
16543
|
+
await this.session.send("Page.startScreencast", options);
|
|
16280
16544
|
this.started = true;
|
|
16281
16545
|
}
|
|
16282
16546
|
async stop(outputPath, expectedDurationSeconds, captureOnly = false, signal) {
|
|
16283
16547
|
if (this.started) await this.session.send("Page.stopScreencast").catch(() => void 0);
|
|
16284
16548
|
await new Promise((resolve) => setTimeout(resolve, 120));
|
|
16549
|
+
if (this.frameListener) this.session.off("Page.screencastFrame", this.frameListener);
|
|
16285
16550
|
await Promise.all(this.pendingWrites);
|
|
16286
16551
|
if (this.captureError) throw this.captureError;
|
|
16287
16552
|
if (!this.frames.length) throw new Error("No video frames were captured.");
|
|
@@ -16320,6 +16585,17 @@ var RecordingSession = class _RecordingSession {
|
|
|
16320
16585
|
consoleEvents = [];
|
|
16321
16586
|
networkEvents = [];
|
|
16322
16587
|
timelineEvents = [];
|
|
16588
|
+
browserActions;
|
|
16589
|
+
captureBrowserAction(receipt) {
|
|
16590
|
+
if (!this.diagnosticsPaused && !this.stopPromise) this.browserActions.add(receipt);
|
|
16591
|
+
}
|
|
16592
|
+
captureRequestBlock(request3, reason) {
|
|
16593
|
+
const event = this.requestEvents.get(request3);
|
|
16594
|
+
if (event) {
|
|
16595
|
+
event.blockedBy = "rill";
|
|
16596
|
+
event.blockReason = reason;
|
|
16597
|
+
}
|
|
16598
|
+
}
|
|
16323
16599
|
requestStart = /* @__PURE__ */ new Map();
|
|
16324
16600
|
requestEvents = /* @__PURE__ */ new Map();
|
|
16325
16601
|
stopPromise;
|
|
@@ -16341,6 +16617,7 @@ var RecordingSession = class _RecordingSession {
|
|
|
16341
16617
|
this.videoFormat = options.videoFormat ?? "webm";
|
|
16342
16618
|
this.captureSignal = options.signal;
|
|
16343
16619
|
this.recordingId = options.recordingId;
|
|
16620
|
+
this.browserActions = new BrowserActionCapture(options.recordingId);
|
|
16344
16621
|
this.cdpUrl = runtime.cdpUrl;
|
|
16345
16622
|
this.tabId = runtime.tabId;
|
|
16346
16623
|
this.browser = runtime.browser;
|
|
@@ -16375,6 +16652,7 @@ var RecordingSession = class _RecordingSession {
|
|
|
16375
16652
|
await options.beforeCapture?.(runtime.page);
|
|
16376
16653
|
session.startedAt = /* @__PURE__ */ new Date();
|
|
16377
16654
|
await screencast.start();
|
|
16655
|
+
await screencast.waitForFirstFrame();
|
|
16378
16656
|
if (options.url) await session.page.goto(options.url, { waitUntil: "domcontentloaded" });
|
|
16379
16657
|
} catch (error51) {
|
|
16380
16658
|
await session.stop("failed").catch(() => void 0);
|
|
@@ -16410,6 +16688,10 @@ var RecordingSession = class _RecordingSession {
|
|
|
16410
16688
|
}
|
|
16411
16689
|
async finalize(reason) {
|
|
16412
16690
|
if (this.maxTimer) clearTimeout(this.maxTimer);
|
|
16691
|
+
await this.removeClickCapture?.().catch(() => {
|
|
16692
|
+
this.truncated = true;
|
|
16693
|
+
});
|
|
16694
|
+
this.removeClickCapture = void 0;
|
|
16413
16695
|
const durationSeconds = Math.max(0.1, (Date.now() - this.startedAt.getTime()) / 1e3);
|
|
16414
16696
|
const outputDirectory = this.outputDirectory;
|
|
16415
16697
|
const videoPath = join9(outputDirectory, `recording.${this.videoFormat}`);
|
|
@@ -16417,22 +16699,25 @@ var RecordingSession = class _RecordingSession {
|
|
|
16417
16699
|
const bundle = {
|
|
16418
16700
|
schemaVersion: 1,
|
|
16419
16701
|
capturePolicyVersion: "safe-default-v1",
|
|
16702
|
+
...this.screencast.videoStartedAtMs == null ? {} : { videoStartOffsetMs: this.screencast.videoStartedAtMs - this.startedAt.getTime() },
|
|
16420
16703
|
recordingId: this.recordingId,
|
|
16421
16704
|
startedAt: this.startedAt.toISOString(),
|
|
16422
16705
|
durationMs: Math.round(durationSeconds * 1e3),
|
|
16423
16706
|
bodyCaptureEnabled: this.captureNetworkBodies,
|
|
16424
16707
|
bodyCaptureRedacted: this.captureNetworkBodies,
|
|
16425
|
-
truncated: this.truncated,
|
|
16708
|
+
truncated: this.truncated || this.browserActions.truncated,
|
|
16709
|
+
actionsTruncated: this.browserActions.truncated,
|
|
16426
16710
|
console: this.consoleEvents,
|
|
16427
16711
|
network: this.networkEvents,
|
|
16428
16712
|
timeline: this.timelineEvents.toSorted((left, right) => left.offsetMs - right.offsetMs)
|
|
16429
16713
|
};
|
|
16430
16714
|
try {
|
|
16431
16715
|
const safeBundle = JSON.parse(this.secretValues.reduce((text, secret) => text.split(JSON.stringify(secret).slice(1, -1)).join("[redacted]"), JSON.stringify(bundle)));
|
|
16716
|
+
safeBundle.actions = this.browserActions.events;
|
|
16717
|
+
safeBundle.actionsTruncated = this.browserActions.truncated;
|
|
16432
16718
|
await writeBundle(safeBundle, diagnosticsPath);
|
|
16433
16719
|
await this.screencast.stop(videoPath, durationSeconds, this.encoding === "remote", this.captureSignal);
|
|
16434
16720
|
} finally {
|
|
16435
|
-
await this.removeClickCapture?.().catch(() => void 0);
|
|
16436
16721
|
await this.removeCursorOverlay?.().catch(() => void 0);
|
|
16437
16722
|
await this.browser.close().catch(() => void 0);
|
|
16438
16723
|
if (this.browserProcess) {
|
|
@@ -16443,7 +16728,7 @@ var RecordingSession = class _RecordingSession {
|
|
|
16443
16728
|
const consoleErrors = this.consoleEvents.filter((event) => event.severity === "error").length;
|
|
16444
16729
|
const failedRequests = this.networkEvents.filter((event) => event.failureReason).length;
|
|
16445
16730
|
const httpErrors = this.networkEvents.filter((event) => (event.status ?? 0) >= 400).length;
|
|
16446
|
-
return { recordingId: this.recordingId, videoPath, ...this.encoding === "remote" ? { frameDirectory: join9(outputDirectory, "frames") } : {}, diagnosticsPath, durationSeconds, stoppedReason: reason, diagnostics: { consoleErrors, failedRequests, httpErrors, truncated: this.truncated, bodyCaptureEnabled: this.captureNetworkBodies } };
|
|
16731
|
+
return { recordingId: this.recordingId, videoPath, ...this.encoding === "remote" ? { frameDirectory: join9(outputDirectory, "frames") } : {}, diagnosticsPath, durationSeconds, stoppedReason: reason, diagnostics: { consoleErrors, failedRequests, httpErrors, truncated: this.truncated || this.browserActions.truncated, bodyCaptureEnabled: this.captureNetworkBodies } };
|
|
16447
16732
|
}
|
|
16448
16733
|
captureNetworkBodies = false;
|
|
16449
16734
|
captureSignal;
|
|
@@ -16498,7 +16783,7 @@ var RecordingSession = class _RecordingSession {
|
|
|
16498
16783
|
}
|
|
16499
16784
|
const started = offset();
|
|
16500
16785
|
this.requestStart.set(request3, started);
|
|
16501
|
-
const event = { id: `net_${this.networkEvents.length + 1}`, offsetMs: started, method: request3.method(), url: sanitizeUrl(request3.url()), resourceType: request3.resourceType() };
|
|
16786
|
+
const event = { id: `net_${this.networkEvents.length + 1}`, offsetMs: started, method: request3.method(), url: sanitizeUrl(request3.url()), resourceType: request3.resourceType(), pageUrl: sanitizeUrl(this.page.url()) };
|
|
16502
16787
|
if (captureNetworkBodies && request3.postData()) {
|
|
16503
16788
|
const redacted = redactBody(request3.postData(), request3.headers()["content-type"]);
|
|
16504
16789
|
event.requestBody = redacted.body;
|
|
@@ -16513,6 +16798,7 @@ var RecordingSession = class _RecordingSession {
|
|
|
16513
16798
|
const event = this.requestEvents.get(request3);
|
|
16514
16799
|
if (!event) return;
|
|
16515
16800
|
event.failureReason = request3.failure()?.errorText ?? "request_failed";
|
|
16801
|
+
event.completedOffsetMs = offset();
|
|
16516
16802
|
event.durationMs = Math.max(0, offset() - (this.requestStart.get(request3) ?? offset()));
|
|
16517
16803
|
pushTimeline({ offsetMs: event.offsetMs, type: "network", label: `${event.method} ${event.url} \u2192 ${event.failureReason}`, severity: "error" });
|
|
16518
16804
|
});
|
|
@@ -16525,6 +16811,7 @@ var RecordingSession = class _RecordingSession {
|
|
|
16525
16811
|
const event = this.requestEvents.get(response.request());
|
|
16526
16812
|
if (!event) return;
|
|
16527
16813
|
event.status = response.status();
|
|
16814
|
+
event.completedOffsetMs = Math.max(0, Date.now() - this.startedAt.getTime());
|
|
16528
16815
|
event.mimeType = response.headers()["content-type"];
|
|
16529
16816
|
event.durationMs = Math.max(0, Date.now() - this.startedAt.getTime() - event.offsetMs);
|
|
16530
16817
|
event.transferSize = Number(response.headers()["content-length"] ?? 0) || void 0;
|
|
@@ -16573,10 +16860,10 @@ async function launchManaged(options) {
|
|
|
16573
16860
|
try {
|
|
16574
16861
|
await waitForCdp(cdpUrl, browserProcess);
|
|
16575
16862
|
const browser = await chromium2.connectOverCDP(cdpUrl, { artifactsDir: options.outputDirectory, isLocal: true });
|
|
16576
|
-
const
|
|
16577
|
-
const page =
|
|
16863
|
+
const context2 = options.blockServiceWorkers ? await browser.newContext({ serviceWorkers: "block" }) : browser.contexts()[0];
|
|
16864
|
+
const page = context2.pages()[0] ?? await context2.newPage();
|
|
16578
16865
|
await page.setViewportSize({ width: options.width, height: options.height });
|
|
16579
|
-
return { cdpUrl, browser, context, page, browserProcess, profileDirectory };
|
|
16866
|
+
return { cdpUrl, browser, context: context2, page, browserProcess, profileDirectory };
|
|
16580
16867
|
} catch (error51) {
|
|
16581
16868
|
await terminateBrowser(browserProcess);
|
|
16582
16869
|
await rm5(profileDirectory, { recursive: true, force: true }).catch(() => void 0);
|
|
@@ -16618,22 +16905,413 @@ async function waitForCdp(cdpUrl, browserProcess) {
|
|
|
16618
16905
|
throw new Error("Timed out waiting for Chrome DevTools Protocol.");
|
|
16619
16906
|
}
|
|
16620
16907
|
|
|
16908
|
+
// ../chrome-extension/brand.ts
|
|
16909
|
+
var rillLogo = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgdmlld0JveD0iMCAwIDMyIDMyIj4KICA8cmVjdCB3aWR0aD0iMzIiIGhlaWdodD0iMzIiIHJ4PSI4IiBmaWxsPSIjZjdmNmYxIiAvPgogIDxnIHRyYW5zZm9ybT0idHJhbnNsYXRlKDE2IDE2KSBzY2FsZSguOSkgdHJhbnNsYXRlKC0xNiAtMTYpIj48ZyB0cmFuc2Zvcm09InJvdGF0ZSgtMTAgMTYgMTYpIj48cmVjdCB4PSI2IiB5PSIxMSIgd2lkdGg9IjgiIGhlaWdodD0iMTciIHJ4PSI0IiBmaWxsPSIjMTcxNzE3IiAvPjxyZWN0IHg9IjE4LjQiIHk9IjUuNCIgd2lkdGg9IjgiIGhlaWdodD0iMTciIHJ4PSI0IiBmaWxsPSIjMTcxNzE3IiAvPjxyZWN0IHg9IjE3IiB5PSI0IiB3aWR0aD0iOCIgaGVpZ2h0PSIxNyIgcng9IjQiIGZpbGw9IiM3MTQ2ZmYiIHN0cm9rZT0iIzE3MTcxNyIgc3Ryb2tlLXdpZHRoPSIxLjgiIC8+PC9nPjwvZz4KPC9zdmc+Cg==";
|
|
16910
|
+
var rillFont = "data:font/woff2;base64,d09GMgABAAAAAHLYABQAAAABAQgAAHJiAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGoYFG4GfHhyHTD9IVkFSh1s/TVZBUjwGYD9TVEFUgTgAhRAvfhEICvZo3iMLhEIAMIGoaAE2AiQDiQAEIAWIGgeLSQwHGyLxFWxXY6CewOalNZfPMyrYrmbcDkTU+r1qNsKGjQMg3hwT/X9SUhlDE9iTFqA63e4PtWQjlBo5goYcMo0ZqbViQ0jvqjjmPNf7mgM3sj6o9UXp+5FQOzPJbOE3Jx5hd/ytpmpjm0pYR9BzruG68ZnXxBVYEMSHMTAwnWpYTZ0wG3f74drkjkO9/6wik1904hQkUpdYDWmJLZFY0pNBEcb7IndF19PSj1kEuE38iIQcX4SK1qgisnr6gFERPc4+OgJHpJCEZJaAxtzpl8/CAc8Rzfk/exe9hCSEEEII4fAAIZilNJgFLUVSVAJFvJRHwV/VaP29R8UpddoiNaXiVOXz+nnW8vzTuOd/7TP3wUxgI76kJCdlOHSryZ2cIZquptBglVCzlLbxtmnUViK7SXY3ySbepI23Th21In7G/T/8PXD3nBn2TjlRgXtD5X9y8/sLj1BKDapUxtT7bcyW//zF1njpmb9ZiFmfWWVarEkKASLEBdHC8/y9O39WtzE54I1pQBMnULgfMHXC/yed9r1BDQhtGTCKA5+y+w8Yu+u5KTKSj4v2ymuaz5wFYG/AcWQSNAC4Gc+jnNqMoTBjlTBQYlhwJCcyUAFAfd3JRQxiCQASnZhHqyFanw2SSW3yoCX0NbQNST3t3wtitjLDMjrC5AKDqIA84tpO+X06q19d3aKW2DLteMgws7s+RPtdEF0YdkmaA4IIKEPg+LL735x+fyVLjuNAgT8DDtDyE8VJ5/zdX+xoswDARzSdN21KaWKzLNlCv34uhbKnT9eI6JKKPh395+QWaAu0BXoHtIXfybyEh3836/8kJIGEQAIRQoAECxKklEmpQGl9rGsus5371L9E9JqP7HZnzdqZmk8LlCIJoX/k9iCCRPK2rIWh3PYL4J+AWGsmndQZSiFWlgOPgZDfTBA9mlsoDTKxz8xQv/38r2Up7Xf/esMojtKulKeklnaUQvEVkpgOEEUkKHYzvaO5v32jkPtSnpPkmC+vzpmFSM1ChCEkZmZqQ1ED6jKABowZGnBonv//5Ts7529ae2sUMo7ILxzFmpQ23KS7P9QiUaXLCBVnQXUjMR6jHCDqZv81T3ETF1IgRjKJJB6jsPi8Nst2/GZ9wAogFg0Avz5FhfaXZNmjGdM4u2Ovj9m5AFmWtePzAVIVIKiqtCnTXV2t97qtAh1g0Vb5e1PN9j8EYcVLSyr9y//yvwzHWjacAi9Url26i4tdgNhdENICJO8Wq7QAeeeF4gdFn0BS8hHgRVKekUNMdEofIH3zSZ2sZTgfpHMIoWhCyJ2L0k2fq9pFpXFRea4oO5eRnseb+MCsd5v0rcACzGRv+jFSDL5rR3UVmRIi8/WOe9fUUvNIKU12j6zTMTxCIATCUNraqc9r6/omVq3JfU/2LaGEQYYiEoJIENcL3e/m91MrGjK6RrIPCSoSZQkEqSUm9d3XvQOzYiqtAy27c6EL4OPV3wvhCgBLAZ8AzmKjCObOihAGadQMEAWeVGgw8LQnvyyX5FkeJYJ28yAgeri/vhBImAZti/nf7CJ/usjUXBR6sFgmIo65HKSrbQ4BKwHpaBFUHkZoocuC6HIR7791yCyoIEMB/BLgHMA+wHoIUIDAf9gqBByCQYDnn/XrBzD/O7gICECv+OSXH37/oxc+evGj73/+zW8/+S7Nt6adaVIcEx+U7pSelL/zy7u/PqQt6yn9Gv16/Q79PmPOWDSWjWtMN06aaXPeituIHbfn7AXnJudW53bnbuce5wkX2Q/3V/eUffvB6n58v7hfqtjHOxW5EqykKn2V/ip22DscHG5Vhaq3qla1GjZ5dC1Qk2tKLX1gO3rcQbj+ZWvailucozRIw7SfrtelQ7VRa8jMn4WzT9yE5t35SjPZzLZaEi/Iwl88tn2+T5fb7by+OWCaZMNsXY/r43dlQ+1sjtJRY7XFBSme08lAz4WnUReneIGXOMMFFljhyCK8gjyMhSQjmEbIU9AY6dIlzShlFtN87E/81J/5mU/FzUQrmAVZIISPrxB7yUZST5pJO+mG4zAN52EWFiFTllKqQqwTO/ahfXvSnPT76VC5AUU87a+nfHZt3pxPrrOsJLgEIcDFG7lwf2tccvQTuBYACKDzvG7ablGymjetVNpiyi2hbMcNBVFKqMABAATg70VLW0wZVxVWAlmAUF5kCADnUQEOHqWahWkqYG7/a8tRdlwUmQBWZ9ZXlID43IrcYhBflFmlBSsU8E4Z40S9e97iwlsrtCDreF0gubh4fx8QAIKGLg4FvSio1PWEALOnvaKu9ATayqQSofhoeBACBg15iBU0JAs2hAJXV9fgIWBYNwJheiOAUHhFAhVH1sydmDMy41JSr+zsJRlVf6gY0JA2lkFDCGCjSKFiq/nDeGjwYmALWEcAt6sWsHlgRNWqVvw98jV9EDt/t6dJ4t38UWbZvNuC7zRW2/yOyRe4u3cTvMM85eBusFEccQn2k0cMrF+2bUoEc0Q0Cw35AJgLWuBptqK8NQxQJ79YEpIozULEpYRlx2qsRXMKCGV3QHai2S0OIuvP79huwNbY9k24R9KWqqkMEM061+nLuWQQ+Rm4LD/yqVz8plpK2c6Z7My6LEnXwN+csfSmq6E/Tg+OOoqCRcRH5wXQj76iMnJzL3liryh+gbvj6VbX2GODvYalBhta1eus2XcVyYCpygCPfQYhdpwFmPobyf0m0Q5fQFD2zTkYvxXNiI3Q6YbYubiiP79YtvIZGNag1Yzfw25N44dILrWOE/tGy07v+I7wtizXnozyygvx+XZSeGAY+aRs0Ynvt7pxlYldax4X5FlrG9ti3nrGhgxcWjSWT2mo37fRC6+vHq3cNEk9rWHcOUZi63kT9BnlQEKXX/YbzbHSkOe8du6C6aWTzdruMYlJOzhSYteOXvh3LrehC91O2A2R54sy2nJASFgckLmLiZqYZHoE3M551aqGndjctE57MNyLwb1Fawgyk8FeSauiw2xI7LQqYOaw6Z1QRvjO43xSsKfTaX7INVZ2SiQVWHJy2iTsVALOnv/6TlGOyNy6hNlwLTD8Z9ycKHdGvXW/3b5E9MBzDHoV/OxCnfqhwdWAu359A+AywAnAXsBaq50rhdh2RcwWcvtzgVIXcoBSbLaysi1L6dmTBP/S85VPLebSYKs6Pdh8tH4QvB3sApaqm6yJIDhz8B/rPhVTBf+52l19taC8+esGK1ZEoSUvixIU5WYbbDIvQuRKw/9mq7csbalqSiGGo9NBI48FCUopaNIXEhoXw138VzY86FYMucw285OptCEtqlyLWwukwGQNP5O8LzB3y32AUdjLUjRZWp8ZW3M1fb/czl606sIua1m64BrEsuEoidz0rjJA6/IGmTQqxkk7fH3Gr5oz3YEwT9v+jOhj/q2133BJDQg0R9WGeBLX2G5AGRFtAoRoh4eXaZdFhSI8JbHRocrBuzirxt9kXBVQZgiWpkGf2mu0uvMCjh20D/ssaczOzCZ4VW+wD2fc9SKmUyGkqbP4YC6k2goCp+MzAo4Lt32RWJiAqP8DqEaho3qyn6Cb5TR6pUKILZNAaqj1XJA6yOmhUhwRMWT/hP2vjGHAR9fML4cNG0kSCOZtzH9sYwB+KPe/rvdAE5itjuNwyb+ubUQWFj7flxVrSSYVGN0t27wtF3uCy9FcHWW++jEY8Fb5lvXJbbqUDLX3/P46HusWhwuM1t7424JxnZFwTe4341jYJbKJe7VN+OQ4Jvvq8wSXRAucgEN/t2pvr1+CcroGRq7VSUKnMU+X6l7dL6xitWr7GYTKP6m5P7l5KF5dF+wOx9ewPAD9HCw2sOG4j2pvMdD6jhG7HHhaL2xagjYzwTotcqvkJ5hDAdUYnuVhr/Viuo/do6cRrmptPFKBNc+qbXqeJepuWNYf37sd/2RMAItyJmV62GDjqh0u1dTW4h60pdVyb+75Qcor8mV2FXn8faldbosJTJT5mEPQnen8qeMs7Fa6QfdjHgvg1ZbHNP/kLDhG+RVeN1Oo5629XL4z6JZrDcegt5o+kt+0Soh7jL5r+7snuE7qZQtVm8uXM5Yh6un2HIx5OILHs9/GYu3QG/4xyE6redq069CpS7cevY474aRThgwbc94Fl113x133PfTIE89MeOO9KX/52z/+9Z8ffgoKwhAOiO6NALH/0e4smmndCW4eEWojYu3EUAfR00n4ugium4j0EF29Up+TRF+LmCEiyGlCcZbQDRPPJuAsmYbzZBLeJTPwPpmHj+US/2IEvgJJwtdkAr4hSZglEzBHELhI4nCJIDAv450Wx2EdZAq2SAoae3cABn8/NmbzNmdTttNmbLftuDoLerDH7Al73o4VU/AReWkAeznOOzpOGVOudsJO2pJm8vvqQhLCOEAtNSsjFCALMYBEU7dN+t1EY0R0oU5vIHWXObUdi5iq5Jpy/yp/CP1kTiVMt1wNzk8JkaMAEsgkAkRAQ2p2MD0UIAcqQKKrQ7ffnHGIERzuOgX9IgW0juXZQhGNRwuKe+gic9yPXG8HnOveYhkz36GCHrPiUCqrrpXW1i4aGqTYfSSUUH9gBtKvyk+pu6yjTW3bG7mshTEtVemEZSuCOo0CAyrDMKI1/N1QvnRc0NB8USn7tLRhrnrOmL8lk0oK0dFBZ5TBRXOdn0+yBP95o+fIhLiAtksPzdS6ngKbOdKRcZmnXOdaHXcuF2j68D4nj1iMgyapoQOvujO41Gxdjk5O4mR/Z2Qn7khkO8bvbqTKZU156rI2+lxl+DILdOdJ+TftWubGm6e1rpd3FQhqjgGdgcRLpSlo5lPDkdaypvbK206RbLwXO2et96Qd0U+QuC8k6RqKkXFzVsToa/MioW4uDcF1nZgYhbCo9bcs11Glipm3bVy58p42vIziqHNqZLDuD6rbS5o5jwQkQm4R97iMCvIOBCzUGkxI4f0zRt+f8ESIvRjTkyn/+FGovyoydZ9vAx+OeFVm+H5A9diI46htvsJK/eRpygPEPKK2VGtvSKCjvOotlMsI1J0jozpfaZ37G19ya1wj+1xJjaafAJwT1kdVKsy6jg/J9kAXSjiDbr7BKFxz5jJpajawGoorCce5f2bOCq8YGNnbAz0q2c+rla3hEdQqXGU0tT3Ztwm1QHMsY051861utJY5Mnm17QovFnTmRVCHDp1RJciOzlVYyxbtRxB4RdI+K3R2DsmqhDepv0jNkvAK6Iww4BIujfT7HqtA99av+RBeN+odHZqnxW5gRwlL4xGqr22Z2eBpcp7jLcwAbDBGo+soapNJg++5l9mH5xLD4jbaaZGCUq+9pMp3G7YvlzeaOYKLgISPfISwRysrsj4F1z73hyxzNABOjYken+yiDj5YnurFBWI02F61pRuf0GAESga1QgdeC0upyUWMIrjhRw2QJZyXIfA0ptobkw/rYsYcX2kUjwEuzm0WBic6iuXz3vWYmBie0oALBc5RjNgLvhkIHwR7bDyeNDNekUmpWYvLAhzF+3hK6Lj2uGcmk3xxKqCuIYOBTYBL7VZljrMl8p7zOVk/xwVWEhKq7M0Rj5Tel6LwJDESWGES4DhXsC1+FVAbSaj3L5YiYuohdJalyYWRS07cIAEG9hnMZz2OxSHSaP87LZU6f9f0aGWMa4bwrVk1gxNBWeg9bmN+yLkthL/EiGh01n5HXZr4BUbVHpvtpDivtb40D//FHIqTYeDRZnMQs2xsKx7nYsQwhcExKWEnHDPejyPWp6HL1ei7nqQFVYuLNh4jInTm85tVr9X6xjq/rC/ZV2X0aPWQ/pbxUWr0ES8d4ODau2d9dcU17qmaBR141Z3ABZDgCKDrKT1LeEfCuxAfTjObB5+mlyZ45t8xkItazLxzb/7mdbGWjTFhm/WsT9cQ+njXJMje1a8GCE4F8FgAZkLFeG/6bBZjXhjq4qdrKnQuSk9eMdNKf9BtRfW/M5loPAgG7UicUls1J7OMDi+afUcjOJ0pedfOIz5S3DOXfKO1m4xk7N+4zZ6cdEYEtcBQOsFLXyiXmv5Nz0OGfbo5MUc4VnBQowsij+KnrMWkZNFkvOusfaHBKbpunmwXdJrLpkcGf9yXDles6xbjeQQF1hlurdgl/j+3OY8axbkuvTuld6G7c04VO2ZZ3cUF1Do6kWDT5iuhBNAxV6/u0jWtNXfocZqjcT3iPqyGWmJ54bpRFea3ZEQfkq0b9ybqq3dhZfBRriuhMcVKeLslCzR0ur93Vnrw4I1+snvs5del/7W/UR7bMaetU2x1BpB4/0n4mio8RmjjAKNWMebaC3kMvb9K6+vKnAufB296rlBc20KnBxosjW2IHn1F9Fy5oXtne4400lGrcODR2ncPGVgzqBU68NKskC18ZqlkDbR4odZ2CAfw6wi/1QB66Yd4wujb7PsJ1IzXORoe4ntOucz1Du8I3dJDnzVw6c4Z9tk7eub/tJWiozCOfK1gZ6SwfsX+a/vOuVI+AGIa81fNFK5VXodjdWtmPf2tUX+dhcq/zsQ08PW1oS14YKVBxi2toT20Od79BFjrq1eNpBLWQrqPeArURFYg42gE6sWvMB5de03hyvBfOj7/ls39qYeB8N/zcSvBzYMWhtZfn7o5598XqtJvt9DNqsr/kOiFKB19/eBQIgvg598LTeotJ9Hb/DVH9tpbC6QWe38Esgn0DOU/XpyT/oS8VKJX1SFExdaHcwcHUlK8T4t+fKtecx03pNvwPlO1BcamFzrvskWuTy93xz0rPJhe7ZEJa7xpG73/+iZTwWEzzO83mp035rQLOdOgj8JyaowooDWqzXCqGsyWUafJAsN1r53gcp1sM+p9klurkweeuivfY3eyfgQIgIeoCsENS7wxmClUglY2icpp6+Cg2joC/sMVBY11BVQ6LmNb6rfwKq/cs8zt2gH8rjpQzzOfZBU1dPW/5VrWxVrH3O89kOWZJ+qcc0qF87RL4rKrKqd9z6NLXHMJc6yPjWJTN4r/iFM80GJXBbThkmpLAzsdvRBGFUPFPVpvniab7M2SLcapxx5FaEnrZUCaZT3MQVRtNgvYWg0k18KL6P9HJECYaAgEhIUNp0MHwsGB4xJA9OnDCRlBJIyJSJlgIZlBzNnC2ZFD7NnDKSggDhzgHDnicOKGwZ0HNk+edHnxpsOHDyGlABSBAiFBomDU1PiiRcPEmAlJkkIgVSpEQ0PfLOmoMuTDFChAVagQUqQIplgxqhIlMKVKIVpaXGUq0VWpxlSjhp5a9ZgaNDDQaA6+Zs245moh1aqV2DzzOGjTxkW7ds46dJDr1MlOly5i3bo56dHDVq9eCsedwHPSKYaGDOE57TQjZ51laswYa+edZ+mCC2QuusjEJZeQLrvM3g03OLrjLp577jF3330WHnjA3EMPWXjkEYnHHjP2xBMSTz1l7JkJZt54w8p779mYMsXVX8GBgImFMNAwMdERCDpY2Hh06MBxcBjj0kMnIGBDnz6CkBGMhDEmKSkOEyaESGYw5mzgbNmis2NHSq6Jq/XoXs3EwY3BM39UIfuqGhgFzcWgiIURoBZFeqj4chNBGAQYMVzgZqSpYeYUNRExJ2HGnRsHCi6c2ZPQJy59J2QVuwW66Sr6WSYsWJajQ5YT0C+Z3CUTGskUyZQ1W3Jm32NBFAIBS3PJc/Lpl9ef1lEzNlu2/L0WFYokQviYCFw8uvj02FJwpJaqWKMz/yt2CeP+UBwe8G3MHzcCOfa/aWi7j8AoRr5AV2r7wdG8jJQ/PuqGiMnbywX1UYPgJESoMOEiRIoSLUOmLNly5MqTr0jJiNnMpo0jbiPiXQS3gXADAAAAALCycXuyAAd6084t/bMQBdCVSxPImfy/+o4SBP9vxzGOjcLxeRKpZEnKfBr3fkMeS/8/l8Y0DXNwOEUNMSJapbeBuTaGBPvrfNX8F/m+w1GFvvWGIBJStvslHC1we9zllP1+KZcOiQycKY7yI9pRcRHLf6/vxOz7aOjZYUzyxtTjlJECmJOzVi7+nTjz5tJyqDtLW1YKFcci5VUouKgwPxP64zCWgGYh8bWYS6pLKyD8sm4GgVfhwNc7/zJwMdFR4NGt2lyWTh6iMovRt+dXRuxpjsC77nnGZvultSrlL+Sdh4YNsELBgMBNtjVL0gbwh1CQRHgIz2EuewHfJJhXcdUPL1H05KtwQhwMx2B+OMrTkTABqjZ3vV44gRBzjGZD1xKrcaOqJVZN68fPBOw8dSsi+/O0zsEgv1oOaPj+FBogrBBwCB+ikLgLYZdX2AA4SMr57kZ/wGv7q1UPqILzwveeEYIiKUx/x4jt6PcjxlJnCpqQiWgsTIUsJxFTBPEEyWyiTBQXP9UCPVNCNLSKMxybrSMwzkVMKgsDOWlIUzsovbwHFWZRikzTE8Tq5GLULQhPoKWgAdlYSdbwVsywW5tA4xPbGkBJEg0zyR/jT7Ma5DyAHHdhufErrwpJI+75thxcNMb8EJP13tJLMDsiqPyhPPlzLg9y87iYwE1PZ6np8IfxCBscGsbK8uYIMWttiGj5xywAlwsgJqV/Bh/uAsqj0xYh1PJge9nCWUvEhjwm+Kz+mTLDQHREQzjY0pMolZMqFTQyaClUKlc/+DvPJDdbmSSFEqQplkC9Pld0GILjaqKa9yLZMGAeBhgFSH4ICTc3RSBQQh8SKWoABtzAWmUTDEEOtvrCQkxwvJKnUVmqGdoaAihyNL/nDGJBJwhMML9ZhFI0SeegVh1d8eI+S+drmUqJDXiG7oqaruSr/a6YppEp4ssQAp4LDrgWsA2w/A/+nvHNzLvAae7Nh+14xMJHDT+h62lHn9f/Dux8D65+CCOflfHncPSr/0v3ZxwBzwEC8JZdHTXRbe56zHt44Aj+9Phx51/df6NOD4/YRnVj2NjQueyFJVfMV/uvtV+zXgtcZ35QdUN6S3/LeAu/rRznjgvHpeO6O+DTsLtVd/l3hXc9d4P3Ku/p7ssfFH8FP3Q+an4UfeJ90v+08Dv2s2vPGp9xn41NMH+onmiccE5MPi/8uexl9UvpS/lL02vpa/1rw2vza+i1+3XP69E38AjOD8/GtHu1N3670fjWf+bNW82Hzc/++7D9uHO/89KF8Yv5/YmLN14Sq6XqyctTV45due7KXVfFg56DE9eI9DUNf6PUmLpx1e2+drk9pCf0Qf3+e+GPBh4ClpNRDgwGoG+e5T/32vxv8/0Xf6/S/ohuJ+vGjqN1sAsAPHxeftXaYA0wfwErM6g4HW5h0EcCpYLcBRXk7iQBXB5fvafgHF/G5GlwuyPu8jG75oiASBhCLuTjH///ArQtziGBlwFkYkYBNiOgtfZ8gOUIwHbYtbcXVCEA9I9mMeK+rwIkOU+hEmQoUacNAuBkckC6YirnNkxX/hzbEcsiIM9yG2Y76VbMTdJ150tWLCyydXG13XFBsFgDPaRCerlXYyulmr7STiFRSDjw0mynn+bIyficymhqoSqCU3Ambr5ZQIpJKWlGWpEupDcZSO5cNTNdvLp0wP/pwGaOvG20uzgSdxLgDWecokeKSMmFO5NeXVcY/yGc9InA/2fSkHn+j3kA/33fMQDfP97lnlRZsoGzbz6pYkeZc4AoAdd5agqADzFo9V4eCO92AbcmfeO0Z875MxR4Y8J1N4z5ywmXHXfeSaf85D8/nPUaQmDRwSWgT8iAhJQJkilzduwpOHDkxJ0HT1583HbBeI7BtWRQChQkRBi1aDFiJUmlMUuaDAWKFCuhVaZKtRq1Gtz01a1EYNAZ//N/k6Z88y5LvDfXo0ThQ67iUwA+tS+G/OKtkVzNHi0e22mHXYbRYCgYqOiY2PTw6OIzImJIjMOMNQuWbFj5h4wbZy5ceZMrpTKNL3/T+QkQLEq4CJFmipdghlDpcmTKkifbv3JVKjdbhTqF6tnKz1zE44GH7rjnvrsQYDEMIsBEdCEJctSXvvbyzWFnMeESO0WhCuw0RepFnzMUi492lhL6V8A5SrXOvnaeZBfCLtBM7RzYRVqoF4tdsgl1Z9F5cL65Rf1nDyw8F1h+JMCwEqhXAaDB+riX8HIz6UgSoaq86q6GxqUyATQaHhsLoGk6alKN1cjE1a2cukzw42JQ02px9aXmOnHy0evjUSUjqj52pav6VMUaFpIWr0O5HGXSUTP19KTUNW/is9SXsU6qkSSboV1kcHkiAgqi61MbEUY1JmNiyPPNsbG2WeXOTZJ/xo2goIleZCB0BVqtjrvZqsCl82v22TVlGu9yyCPII0vMYgfcrH337LZTKVImpwPRer2U+1XgI1dSmJ1CczXN4jivNxEhQj/2I54yfroPzYnNxm5na40dwxAw3iAFXs8ZwnJprelWCtPQH9M+s7lPcs+qalZJcnZRLgbpjEUSGbc52qXdlkmUabPZA21JpapONB7sksg+KnQf32eV4R8VX/veHaFDawevYZvzSQWExSavMyAAH/PAgGYvpdnPmRkS/KZOq0ZmSeXzLuggInw4JuKiv7nl34tZKZZR5MJv5OY/t6yWJH8XoBIuxX46dLNzeqZ9VRdUE37Kv96EhodUx6sFxi/MPqXYR96p1+K54zvWVzrrNPSy5zDWs1IcT5ApACPWgGUu32461R+iAl9qzbAOrfWVm4IpqzJ2KXTKr0YvMxObqPnzrFXNNYI83vp2aOQZQx4EEn72bVDlB6J8kISMZsrOiuW3XD2DseyYTg8vIyWpwlfxrO6yBI56hU5ymu8vH2UhDCD51v2TTyX7CVsDnTi4Vd1seWMtj4ppLvhAGu+/K8SWyXkWB/PIDLJ385M9jOsCaEDaAOlSe67WDiL3k6XofjdwGGeGsxq5zU3s+MYnrUTx+jH3JNjm33j9Fwa7YnBd4Z4qF59A/bK50uObRo3iz19umCA/Q5PkVojOdMyfnqL0sIRTDXrL4liNfLmhyD5S9dKM3yrsUbGRQUetYlsHnPApFwUtTEwYzbJ/DZjBzOmBNtGA7Mfk6GvPl10Q64EYgRCS5tiuyyZQ5HKnt/e5l3Pylkq1Pitwzbn4UD78IX0/VuSR8t3XwdE2MyRPHt9U5rPM4/cGw2z8Y8ivlHTmIWruSJL0rT/hVPb9mdPZo+VQkM1lJLKsbxhosyluIO+fiMKJfUd20ByUB62e+68hC5fr1Ur2Wq3vLG90nI/zstRLoJxJ2eJy8LGb5QHJ/zD2XPECErA3XxiZ3+0NIwkbltlZM/k7XcsQjecnUb2VVjLKR9gx5bnACGRoSRdoG5bVT2qIwJGSKXcfTdnmuPogxlR7seLDLPfV7z/fGh5kdVFlFtavaJz8ymNW5h37lVsDbAuZqNJZe2tnTa9yK70SNGt6eQleF9lPNFvbhOzjcZT+9O1WtX/ycSmhSJJikFmKyq3qfK9cftx7hayCwGO+w8AAptjDQBA1vNJxSkh56V8whCwDAsFOV96PglHmO/oX/nzRTwO1jMs/7IKLqd85J11Hd/14sitY0F1Hm1iG6irDu6Pu+5sS0XuVYt2qvxejAdSYnmX5Z3FfOfMv2cPUfe+LFYlu2Eq3k322+6BosLemdZwWvFryufuM0iMtFB89InssR301eXGVe6VuNeUua55CfXP6TJhQ7oEFy73dnDn/Uo3tO4MMrdZyjq26zCua4wup6/PGROa0kFAzPB+jGK1Mr+NcuIptUK2PxIWyQonKxTvvmT3nslPvm3pcaRGrVhGU6rtc7S3P8OmEBjyH3/A0YD++MkCn5Davzx1HZdpUrX9vrTMuc5rmT4L7IJ5KciK+78vYmwc31duK7WFkkJowROhC9BeRGzaXJfarVXNy+Zoz9goYSJ+Lix+NRRxMqHkSlkXfJ9yPoCGpTftLf01+lCIfhgX3u6vgQphBNhBq93bToX9astcbjc57axnt2anlUi8VDCSXKLY5FNWGCCuYU8p9vDtCe1mjAR+kLRKNTX/RWbPzzEnSt+yKR30TGyE6yUPCTHsLOVRi+AvQl8bAnRnclobOwT8LyhmPdxMTKYGa9I8fMLjV9WSSCDVnIB9LNPRsM/uLU+WaWCZ3nBipfZ2RJJnYWqsmDMJScpBNbcOA6i7JFMf3tMP2oLAX9+2T9O6dhGTMds87JpK4Dza8N6qRNv85fzmdL9p3yH414qIEzrYQqc5eYbLK/D+Vkq+ZR6tVjyssr01uaqqz1OAVymIYImnaJ+2ja7DCvdx6NDYp0+V8g+EtU+32dZPPKcE14+7qQspnPNt+8sNdmACFuaq6k6J5jvw04cER/59B9TLmC4XqTb9KiTQVDiTCRhWOPJk9/2gLks52cP0e17n4Szu3u8ur03ixnMZa5YSyyltT2+lASYoAuY00pm0m55HFwYCXPd0KI08NQ8a2rJZadc0f1nlamoVszs0snxGUl1/aHNJh0jixwlQfpD6pT3W+iFV6ZUStlqZC+OWONOdmKiCN1bO6OUf/HlGMoiE+8ozfxlfLdCLkQs6qTKtgJzmbgQ7DAoMzox63o8O6VXvZE22L/NTKuLk6sSLmDuiM44v34Dpz0wh7DEvUVZ3FGIMoRiAW7VDY5JyTRTWMuc2wWqeS3AgizXTroQ32otkQJ4aBXF5XFueRYnEW353E4ebC6zi9LRnHVCLuZ982iCcxUcA+spGBpQXsMyuWtmZhShrIdnvxtbrz2lwuhqz3h87fzu75mWVVy9a5Rb9Kw+azqOw5yfue0zneMPxjowE21YVZHa0D2nq1MUzpUIvuy706K8JfaTkVhUiB/pzhPHlO4afibeqqgzVVx8T8maKZf5tfXHlx7i7nuxud5Rvf4+wBwf2tqhmnBb8uPBQljS9MRoFg2ZnbCfaf9XH6X6mEI28ssvVeDcjQMaCZudK8IpIU8SwYV8aI5LqI7hXdY2e8o5hap1YTRmyG7iO8WoWaUrp/D4qZ8Z1B8vDoKHlkZ3J8zfZf9MvCjGJHdyCEZlDN6nBYsyqDoJ3prkJPmFHodIwuNwPNzFdbnx3c0b/r8Kbe+I0rVmzc1C1KjJcn+Qud+zkaF/11OGHri23vseBja+LWHEsDhczU9jh+pK/PcXRHRso3ZkB7CqEQoxAN+SPuFCla63J3rE47SFNELYnPfcsjyZoZj50+FYDHhCA6409+7vS+ZL/CLHPWHr+jgM2E00b1EAgNMECsLQCh9k7/O/tGt8dEyoQxQ6axa6BCxt/dc9MdSf33gNN3bEG1mKmJ7LNs3epavcGpvBtW3MTStKFh/8zqYDIFypnpXUHX4dFR15FdyemMKBq3+uGSKO3Fc4hqVTisWp1DcYwVjrO+inmgjJnd2ek4MjCIX/GVlqUm9dn0QRr/2Rhn/NWT8Hgqs37wSS+ZtctX017ZmhxOEllcusZLy1Zn7EAzc3+yXZxGdYkhGflEecETFIUgELXzngBV++mz7hgIhlO0+yw9Ntm2OJNxJr/df/K+WlCtY8mdlrwdCcfRbA4/ujM7k0Nz2NFcwvIYJAkMlmyuFJzJtEQdyTni5R5SuiZDbFaaUIOkQ07GBTQzRY4+mbYLFrfhllZ3EMJ1DKjsvYDuWV2h8R1WxT+gCMhgdMH4KyCa6VlisPYzbCFGqgOjOBqppQZiN/3gt6XL3eVdEqFNoVeHe6QYMaSF0nPNIUZYCGMcBewyypp+I0m8NFiR07eRMqMpPCQG5yse0Tz66xFIYnqmTOYJ9zv+mE/rDEATn6yamBRUgDNpD7L3s2lgBL4/ajcuJneRiU89Ptn9vZzdDzDfJF91573aMNCQ80v8XTrvRFOIgqvzvx/L+6tDz+hgM6gPLqsZot+Ph5iNNHjM8nQ5UJR0+ZknmwOMkEl14XTSnGJ8JBrO9I2kvyy9wqzAxQsXsGfEst3dfpLFxhQ4n2dt4x5rTN7cZGkHT1lk2m5TiyBn0+GTIsB0KKr6+aHgHKZjcSSQHhocqtk7YfQk2nnYIZTNQyT1xyoGRMa86bPWiH5Eu5ymtct6UczSY8YnJ1SzJp+mfBY5LD4bopVbx8L/Qy0ZTqdNZyBw4F+g8ol7Z5HOGfqHOF6usqYR1TKfTxR8cnAScfVEoq4cJLGI2AGjnh1EOwCrAO9HdMtpWresH8UNMaHOPR1grMfUSsydi0ahY5Ol21oQTesSYx2ggGVJIYplPh8p+sVjUQTagvfoGSdGhkNiLtlh7mgK6o1NAbOow+rJRSOeHvBJZ5Wkb1XXyGr91hXe+NwyLDZ/nZNa1nIwfnEBNmDQjZKUbiQ0g6mCIhPe5OOtWre1lMSoaMxFhTDMHfH58MwE0gIu/GNog3WI3U7jROP3LOWBs7gpDkSHNKkcg+WpAOPFNwyhTo3WL/BK5EgO0o46nJqRDASrg7I6mvqf0SOYFeFFqDsGYuewCVQosjpjHg8RAzEztM3uA/+31Ao1hJQf1vlog0uJqc0uF64HVCldCvKX4621N5osaL71czv+gVzPqx9+77t7jnlPXs8OTINB0QZ5Fh1ZCBaK8PSZcM3a8ELvFciMb/E7FiAXVjyNNLVpXCJeyOyjEbvKruIkG2CxgTbYJKSOk2QhHQYiwcaH+iTBDQIDoeCG1b7n0ZJKuJRvIJx4kVWADYgT+RTFGNaPYB+tW16wkxg1ISopVzGI3v+4Om/xS1qN3kCmXOIVTxRRVEQKrHV5a04z9n9uic52dqKuRQS3sgMQ1BJAWlvqRf3X0xRf40q4OqJCiopcGpI3+snOz1X+KlZYqgADq1VKRANLLsHXhqjBrOX0vNNrkLVOUI7YGNULX8pbTJx/28G/tSUAQb3CGRHNAwAxpfjsDSPzEdiollMyvvPxNhXgFWCJ9uyQF2CsOmd1/yNPv3UshBV5BXVCWy7cZQ2K2114gFF9QCvXRJprzeinLnaAJxRiwxSIuYYourvkUGIg7UF9fms+2B+WSL3K2Lq+KTNlJmmDWdjrZGnbyDajxaToOGFU2uzQNcC3o5B1Dp3Pvt0UiYvO4g44+C+Qsd6Yjza6lHaNyUbZ9HQptW6GaQ5ZwZ0hXc3DJ3ddwzaceMx6y46vOB6+28uPRye7aHzP/sEBDv9gvpqXmD678WxqYPZfixK7P97zMcjsxw31BxNNDX+fG/3d8RXu+p/Q715RgIwmRi34oeOTH8DB2kn5vVb7Ec1gRXNeQ/1Nm2/2ekJtsmNWZEXR9PwF+kUXC8obmhoTg19prWs5VZ6S/ZMlpV5wvVYdiVGUlAhXJE4qz2kWHJe7nBDYnpTS2xJE71/uff71UPcpicKZ/hUh9PDICHpkRdgfYMWiUf1wie/3RpP7tmjaVtOw0UCKWtEt+T8ZwfHifbie96PJFp0BeodoamKgRzX/5HMWltf+3xHhdy2whRgF2U3LRrGsTBk1KFsIGsG4cWkiy99CAzlzYtVgfiDIYJiDKkTkqr4hDumEXGpabfJIDCqr2ma1KgZDjPnkkkSvPStS+XSwh6KKS/3z01kChePjErDpkUIi8XX0bjAh+Ij/v3k08GA6+iXGEB5iLPPrEUeAdki5downl5ACRlS0Y0eCqZFx39rRcq6tz6gbtNlMg4sNoJ1JDdu129PB+ofNY70wGc9Gy/HQ3OOHfJJS+2c9WLaln2LLrD06bT9GmIaH9TavNRgkJRwbwm0XO7itNomE67AJwfODui+GLxMpj9A3iGB6ZEZxYwrsmc6JDrjT4oqkHCVlf8MjYbU6kJZaLBmpKqCGMed7ZZEl4ZTLAneOdzi9ZNSMGD14KOjFjUjUTERhm1ojs+hQxKqTaWxqUMANDvrLoPCcF67Lk2M7QB9SDWftFkuPQT+EE6bhIb0tNOxW7EmF6h4waXMGk1WCcYQ2mYTrtLVKJRYhxyGRcu0ID7Qy3aMh4+GREeOR0bDbQ0cbDo+OcEJur4/et6UnYf+wT9/+6e4Z+0AhvZE+V/x73CrsD+7xaT8iEjSwUqherf/DM7d9L1nWtzmLP13k7YItkMf2OW8tx2KSX90EVecfcN/N3nbPvR2JuXPhn97Bvr5YjO612SSuvD6rrq3dZkVF9tbwL5LFZpa4wMSi/3wPo/C9OHCubnuL77QeWMPCt/e+orT1PFQm1lWzpciloBU1axGDVKx8ecOTx8uHtHBQ63rykPRblT2f01Xf4Gmt0dTU19RUfb3IpLDscgTSaU23Osw4N+tcmlSUEZyGwy/KDl81flZ6K8PP7NXuBDu1Xla5+VKAy2rZwCELWurZzxmMvvvEe+OC5OmBaqPOoTY5LeCwVkXWucX/4kI2fdr4yamSSObmHwPJkKF2sbSXPK6ltHWrelgTFrBbG4xrFS0PGv64xnYwKwzGqx0W155QbG9xwyqVGu62BuONXMsHjopH2Uca3tUJzxl/5yqNfgMGHbHlWVBHmUqVQjGkUG6PAB5z2wvjcy8k0gfrG8BFO1hxh0nMIoB8MTNAcaBjrUx9JHVPgLi3mmndjDT/F/8rPiU9OM2BtzAs/0PnANVQrZCfy4zs/3C+PRcqp03Z9Z+xXFHEgBOOubYQgwGZ0n0aDErIVBG98KnRY0teV+S+lj1H3/TYEtX1bjnqmnj04WPnVpCOObnZHyd11r7RYnSwkTxYZ67SIpNK2LdCAqenzFN9fwnUC/dKkC2EsHAPeDfvjH0muuECy/Gu6X/tHOY6XA6sB0pHo5Q4oG9gwVLA7jbpIG/H3CGuEyCTXcTHszw8MP3e14973nU0f/pJ8fRPjMUgecBhNFbfeEF3lP198wgMI2Q47vS2Lv+2Kb7p2+V2Xufqi1QTxCxYoIg6IM8XeefNMxxReWCIPAIEj7wRMg8SyJWifT7vi7pCuuI8AXIWoaZC+ShoDCIGLkQvWA++dxBwSpe6FevBmo+Fs+rTtZGHWCB1ZLf+Mfy+Lf/Y2fGx9ar9Nmh1vGeOoQSgOll2hAoy5kcZ84MUF4DyJDPXKf7uwXO7MqVSoRxSKPQUfxYx68Hpfo+vPnEpwpImpabFsb7y+xi6WtkyrL3KeitFnEnELgA1qEXKB8qNo3+k/YcWZ0xHJ5YeghOLjwYifxigOpZ4UZXocPvzUxK/+hYd1S5wKxH5QMxsVuCi/Q/TohjzfYattHKyWqbpi3Va1mz0T3g2qTIz8u2BTnTtuoCYMlf+/KmFzk9ZkYKW3CJh+zuNkSK52AJwbmY8ERHVWPUQOnCF0eiK+t0KjM1FOjRImJQfekeS551bwB76B52Xdr6ZIg2kDKnKe+Zr2t7SIYV16sWBz3SG7HE2/zhX8MAWEEgtWbEmbDRqwjkx+tNMTAO6FBiZkKRITcF+xiqL2iySdFc2mtsJJlLs4QW4bffxm3fNHWRohGPQh3kqR9rHWGGTaBov/lVgAtvLXKWhVkc7/4FdSxtFrQo4ymlUdfS2NFXjpdy6fz1fKXzDagR+V+DcoM5Nqzc4Xer16fQGdaRr/S5Z73KqN6D1HFKXaSOJtpxeL4oqdotOl20jUoUU4hkduohC1mU0ymLwrZMSXIlF78r37RJYrL0nsLmPWkjOfeHziZiQYpQAGJ7XZAlxXJDQaEQhKSHYo1uw66NbuNkc1yoTOp2yO64xm7s0iNsPYqJL2wYLk1pzR2tj3a/ORhM6Jhp/TGZ8o0JwtcEFGDOrYqopU+USk7Kf7nRMHzvG9axTi7hxbXyeiMD51a0hAqG1vV1os/FbqZUvtBnWUhNYSzGH29jc3MjlsADv+/bnPvcfQWQgSkWPsukklSRv3Bhwk2X83hqH5UfwflS3jO4IX70+TqVqkzZdrY5rtaLodbWbfI2Fe3WIxxu6yrW+Ggc/fvVY08SGDbWN6ze01Qa5nayVanZK7TX/vgRJaCm+tB6jJvSWXoYlwEiKbARbLUGrxS1N7/tsPGVkeVzMt8q1qmBGYncOqM3JVUEGJoDsbDnkMEgXfUGY2Ev8FRltKyHVG4MDYvA0E+5VOhOCjW43f5Mr2auETWmNJVVsDjGYZh+lhUOO9jUed/uaEA4bA5EiS4RRgKL9PYZTt/DdC7AND4cfXoABT89ifcI0v6xmBO+eYSKNamUwLtM74mIZOZic83fUsY27+hzH4KtXng8/Emrr//8jATBzBzFDjOwlZghZ87fkiv97PCYeV4wR80lgff/tx93pwcFMbypnk2SAjgurtK6ulJXs2dCzVZj9O+Itt9FLuYtvr3sm9PbniLU84cybRfDRhBQkDvBBhr+Bs3reIC4UnCboYCD93y39H6rzGAW1LymiPba0lvv27t2b9cRH5DsC8Av0bBj/0y1gvXKyqZexp+unwqbAmdIAf9jn94cQ1Bfy+3xhxCDYqnTfKpAdoKf7wOkDyO6/ugUf7CONHkqFYkiUkhffsS9uX68fPunKRIYUCp5/kAfYpRLBhF8SG1zQ7gutikGrgJ0P0kBICq331lvx5ML7ewwOhw0Oi0ALPdNs/geJtwCzgE6TNcq7O6G7dLL0ixtQMstYhUU6ZmmuTF6D86s7iu6YhWVatpxvBK3D9kI7WOrSy14q5ZZWFf1rrbY1v1L8EtesDrTuDwfDYOOX4nUHzds/rBG/WKb3qvhExNyQ9x1P8noVrJpqtwceWCYncgSYe+NnOiQkWDf4cnOB84cNX72yiwCswgsIMCsE/Odg1p4NK9qzYHV7OmxYyoQ/Av3JdeAMt4QDbWkWTuJzFoBOqUfwimUz3NrBDwnXngVd7emwYAkTVgUN2hPg3p7181J7+s8bS5g/p0HJEgLNz5iaDA5mzLnrkq49C463p8OZtFdgwqUbe/S3wd18DvyWOCkSQzrRVulE/dKJDiZeK0dXE6VfOAJe0sPKTAv0fDqZsynYVwcWOJxICnL49ydhuu6V3teH1RS5PejlDrOB4goMMwqEDQXWCM8u/JHcYd6m5CcwnOEQXpTmcJJPZJgzQgTtI2hEeAjm663EtFJ2xlJh2Z6U6dLF+rKafDeTXqpRV5vUFtBxqnXoaUYdBtRZRHZ5l7gk5S9hoHYk8zWZNFrnatPe07X9sXDa/3Y6mtP5q9PVne7ZTs//ud4RN59lN/j/OZV75F65T+6XB+RBHMLzZiCv0nQCpj20l/bRfjpAB+kQBsxO+S136Y8YqJ3L8Ojk/P1ZIvfJSBoeOr4KYBbfhmO6sY4B8wnGTgMXZ4+QnEtFXAL4c2Z/2QNcX6LF7gnbHpJ522sG9Frkt7b21AigV/+pg89NfcYN80IHYhJBLNIhDnEF/zc99+59DI03sfimH3z5Et7tlMJvXY36gacB/o8A5M7qcbtbeLKRh97CO2rnjguw70/Jyox792UUDS/0VZxceE0jfcpCfvrB4+uCknek4OT5/GpA3ySXm3v0G3MMVKJcOE0jPTPjgR9RyKk4uVMuK52e/gayaxfknUWQcflgujEnp3B/9VMC2Nx2KnYL8t7JEYSdxvuoxoj59N9qfIqADXsAB/ArYh4IHww105ABhPGUjDwsE5r7eqxLlSgf1I7RKCWp5SW1JFJiTvm5HnlZP7njbxT2+zIbO46hBLk1o8mIHBfAw7s/C3AxS+vZPPbEpsSb6uwItmE4sYzYsYzlsCo4Z/zTZL0xw60IAF+njZcA1wWAQhBFNzgvQOITm8PleaezlKBiWfWWGXkHQQLaFdD8AOWNZSgEUVSZ0QCJTzThvPW3miCtU90K5M+MOfkm/10Wmi7OpIcZPf1ykHEw2z9lfQrRRauVx42btiPX73DMzguvb58jBmQmT997W4vMT1LIMuFNGJZXrtKnr1iJXBrhFmvMA0/Fi+p4qWbzxarZqnNksD9F3d+ntBjw/2/HRQDkBIBZrLkLkiBbhRYLjWTBZLPucoNHPGdF2WZE0eaBbM+hDAcPX6kyKb8K1co6UP31qYaNTTFqts2zhbTH28MhPZQPx7tF9+qFfapv6iv6tn6sPzZ9LGr1rM+2vo1+yP/52MEYO2aN9QIh/hesjt+JvwqPwoXk7cQriXcQM4k8Yod129/BSmTls6pZ8+zv27+w/2q/Zy9NWJMz9mfIH8VnJvGpOccTHc93vNbxbp0knTydKp3R6UXnPZwlaX3p99xfsv/OfjJTmKFm9FR0OUlPLXxl4a/mf5z/Z/7j/IMUk7LS75b8TnFmNr4acz9X+sVsm5kq+9/qq/n18u/2apzIxbg8V+ZOVNxYcW/FkxWvVvxK/Qf1n9f/XP9//bOFwkJ9QemAldMrHZWBymTlUOWyyk2VSys3VO6owqvKqpqrJFXmqtVVG6reVgdXZ6qfqqHWtNbsq1lbs7nmZM39mu+1tNqmWmktXOuuXVH7RO3/64LqvHXL6p6se1D3ru7vemZ9VT2/XlOP10fql9Y/XP+xwa/B3zDYsLRha8PhhvMNzxu+sUQsHsvI8rEmWDtZO1hjrOeNRONfFwUv6mua37SlmdIsah5u3tC8rHlr82ALvWWw5Tu2L/sAe5T9iP2Jw+LYOTHOUs6DnIOc71yC28Tdw93Lvcp9zv3Ko/CqeFJeiDfNO8+7wyf41XwvP8lfzF/F38Pv4/fzB/hn+Ff49/mv+N8EdDMYgtV5gAFUQLAI8EUDaq51CpAgAAQiAMRUv/RyxL+2cyUmwq7OhEJga3IHGnfsvT33Wcey8Xmu4CGd/pcHcHnx7P3RFm8rQIIVICAiAJw6gHcdKAhhZP8RfWd5kCO7sdZ3Hy2N0LH2d6elLFmBMErhHY8bLwRPfdS8i+xHnoIUHBlYU/XzR+66a4Y/LTMXbxm/A3+Xxi8UVuWVlXf3siYU2jIeeOB06gAIj7sQuhxY2LeAzfhrtzBPK90oH8mOrwm/Pf/mNfDkg//+7xGpBcel4zEzSh4INJdIAidIZTStoEUXoLegdDBCoA6UtZGBSsmJUtKMincx1cQhuog92ivQ07NFPnN9f2nY7nDixEScH0DPiygTI+sxC7fZp55aB1zTRqYBoWhWGVSMilFggQedo0WsbxyeS97SsoUBaSRve79mqZUFyw+5xnLG2Fl4Xl+44Nod7zwn6CC5uGrrrq5BxSjYL+d4SiQG5nCbocgSvtONxGD5Au5tqVf30oynjavM7D72SEYGmhFtWsj/oqo2W89tQr0iwfKXj7/GjoCfRENpEqdMkE53igMahKUwMBUl41iSMv1zvtfTLXsGdBcF7+oYMN84qLInVpJSov0DrNen9N8qC2yHfjy4Gbq9WVujlxGweVwYhZ4C1BYKgM9FXuD0QWGvy/+IC8uZvay3eP96cOS3WdpjrilwGQaLj+P6NZq8+zwyEor1ZsQemYFf8sPwlNIHmn+u4H91Aa/Ee2XAzuy6676VLvJ33GH/QEwByILq2i7ffux/N218U+cwXANXevv7LY+1ABrbI6rBr87VaxmVp5K27LbnVGq1H9/xBJUVK6wR3WpB8LjGvCPxOE2q2w820jkfh80d75KQZW1Uzp3ed3TREgH2WkrPiOUjsvPo0p29NB3RyOjt/EEmM2Epo3b8AQ4MEzL0bg2JU+lwE+qDEILPHJ9S0Fjg0ulTvN0SWlMgBCFeQcnHBe9saupcZVgPvLkZqGsYHZkIysQgTEyKRX6dEqyce3aZ1vrlNrOg+AXs3FCcmyt7lD+pO+CAm6a4jYC50JBZQHowihMKnzzq4EUp5F1ORLhYZyJmREJMSgC3Gh8CpR8XZR8ajRb4kgkSCXSNiqMcc+BRautJ5TNnPP82Kfd1FtnPUxB/6C+hvRNRutR/ouRIkZIO4TISVlbvUwYf+MyEI8xNLMHf3N/PBIoG/y4wDan66YQG89tHU/5f7jcL3M/QMXTY850PKIBLcsGagFKlVOf91M7e3kZAWCowCwPqk3XQj8PwIZAO6nEd+rr/5AVuaem9lYCkcvwiWFsrYnTTbX6v8ZNPOv/v3DGHTPba4ZulvYSTI6EfwIEH2QN5rnTPUncVCtDQ4FyA8dlF/u4zTx9Rl68HCuG7sau5suwyNI58PAnjUm/k7UxaiDWFFiyReUq2QueLFG2ys0qermigELINTKc2TQkhmInWfPA2bSeqFNGChnI38P0kRDCm/n3WopS3MjAKa62X8Ye/22t4O4NQ6YC3AqEfCzc3/1z0qOG1ezN97e5zyEgw1qUIcafepVagq+zkNny95mMEioRwq3RrheXEZNtUvGB+2+PR9QPuQ5kZEWVy0E6RlCy1OB+sug1NojsWCInp6xPiFwrLYibz7l62PM4FM9FDExa3NLlpHmzncuhS8tsVU/W7ElEz9g57IvGhzEwd9vGTg1DS+NWFW3pFQRXoMaKbArSMKaDjf/sxS8i+hq0pZ0TYMRov2AxtVu4N3Ucl/QVkpAyYfQ+kK4V12z8n+3Qlh5bqxbY5EgZiQANUu/qhp0Z9e+763uBsgLlazp/4uiTE1oH9sre3zBB5JVh8MmM575cgK2WBGSzbZ8PHmzU2Hky9xDVaLKbHduG8UVgebSZRlSus57r/HNh7vep5rOCxlRtkKabDNsxjm6JzrDAbfPOKSMaoSDo1uFm6uc1H63aykAti9ozjw+XERohXsvw8Mtmm2976gA8NZYPCEcVj3WxAijiySah+KvgwWmlk048JG1Y+NQhzIlhN5tCjciwRk0WbCoUCOTUwwrW6Ao0u49B9kcowKHn8mp4be4cGFKBkFAlmsaq49UNjr2DWefKdYVZY0fNIoA5syOMEDZa+CGMwinYK93VV3Pi1/Vyd5+i5Rd6MC8gqrHUjJ10qIaJtyVBuLStHaStsKYElpx0OntGOe+tdfWUFE4hyYByBDDNEWWl+APnmRsmvEVsHoelCCyNoyeq6FKP0YpVAgqscJCZEp03gfrhfMBw3jWMfbhVlX66EQwVoLJRMgMvRoLUrX31j0AkrMaiDSk2xdIFmgkN6kGvq6pEcc6m6wLyGwFF9lye9thHoqtvwKdpcldiW02yknZwoU06qOxLUgW3PGqHMyTv8aCsg7VRpyk2dtwZbqbEkVMTccmQc51ukYELTQ3tu1D8JXxNhsXwPOyAapq/Os+39mUztnPJdfmZ3/aoUDvl4ijpEsJfbARiKMBZIGzA/6OAvgILRe/aDcXLWYXxbMCFRmYoAqMc1Q0Qfr2341F+1qO6ZqGPGehXJ2xt2kpAA2z8TfUURWz/m16lW7u7DD6eEty3igvPHFceY1MEss8CKdCtTE3mXMxEp1jsRW2QJlERDg6CN/Dt43FIeFI+0//vAsPfIofuLQI9mbfUFfhOkaVMf+affP+us0gdT3lXgOvQvc/uXFm8uxYr7YNbNMvvXysqK94Ex4uwjItva9a7OE4L0WUxYn/hv77l7s8GLOuegwwE2+Bw6oA5Nu7v8uoXpDzvxDn6bXRPLgLnN0uLK4b+nYZce9ceEvuksVwkCaC3OPESHwj7QsXdnJgOiHNZhCtYlnkG5sa7B3PSerD+k3WExQoC4+En47LJmxLYfhgGE3Pk1X60CW+mG+Hzlt/GLd20CD0uP5J6UW3hPFwBvCxfH+UIcWYpECLLTnQuaqlvzeBhHl051v4Xc3Dsn4CEkCVzwIg228/C8FkdjD4dmk0DO1hCbI9t1OJyIKM1/vMBbmxMCPQlBE6fz6EMdubH/+siVg/76aDBwo1GPx6XK9kSdw8rSc+WFxqby0x8lBVZgJ8fBRFhK7t+C6B0xVERnr+zOyaDzzQrngvEP9fUKWOSJy7Dbz017xTYL1FYcwo9SqLuUCUkxvwnZVqLCaGczHddLFDJXYuE34ffcVWH4PedSQquN2LN8IR5Z8upIbErZnvaW+Q5VT5hIBoIUjfcqdtT0dbrT62u9HSCuPxl/vWpnt2DMSIKWqWLo2sL9fFgCFrebsQSGT9MNv9pgWZwfVrLaJuHwK+rVD4Ma1yq2PGwN3BtAA4OOV7Bm4WQheWhEbindEJkbdjfGOoPFNEKf9hmigS89pNbZxqG6y1Jugmg6GtJJ1kR7h+i4/VzcSl5duB7AZIqQXBNVPfiao52OCGdPZydyAMePBVCe54Wvu7lTGzBhXNRYPOOJlX7U6IDLaUHrx+VOS5owChMVu/drhWIRQQZTolVZ4EIE2YosE2ghkcUuUu7kzRgtTNM56TiZLkkuFDTgBS9EQLVWQHdv8IbCOae+6w/5fB7SBSE91e2LZLipTvqL1lYwYhWLdjzElM0SLFEYil/1oWlxAN2kYabIEgzBsLc1qWpfK6v2lXNRvXbJxo4L3qAUBr3IlmoGcIDjbKl3OrqXF/NgM15HabUWDbcUDzTyUUMpXJdKXK6vKiNJ3w3EYRrVL6X9GnAjGXOoVCxYmmx0LWStmAlnFcVrm4SAgf3lCrhx1SDvXOIxHg4jpOIXhEU+A8FLNWgShyoPCgaHyotpbhfNB2RwXKIDsFbWPAe2YmHHCdhWViRhbBjfQUVi3ZBubJHOSmQ4aUIpHUKUcanOZPEk73RKSYrO95NtINeaBoQb4HZmObeWxXZzyTZzY0UQQok5NF6w/EwSsBGSDkbYUaE5ETRJFJhrjUYnyoLaWuEE5t8VETgje90xYbFRVTZ9LSvb199qOq9F5/smqECAQzas6Ka/oWjHAhjn4LPO8SZuBjikX7CZ1AD6EIFBpNDYMSO9muk8I1cOiFPAF7/IrWz/gWd73Vl8RBNhzNueNxyXykUL88WvlMK+3O0PlG27de72u4Tm4i40tbQ122Fe8F4z1neFP7/SU90Il6qLj/tV9184WI2Vbwc40wBsjS0CPqHEjhCGzjAuj32AvMtr4Sj2QlgFuZMYiKBUKq/CA78DYN0E34qrVbhN08lDdf+AFCDITR44jngq8rJwIl6QNcgmxQH0YPMqEscJ67/WyALLS8WPzONhAMaU4PAkcUa90vRK7t5ENNptnVNHFvhJOqPqP427fQX+dKnzTxyAu4V3B3DctGmC4KuS0aatUCwhJeJROLe9CsctNVJc7re9AgX1gYEOBZVUFg8XrdFrb8U5I/OTCjCOaZnI5oyTwnsRFZID5drbegQ9TsV5w++IwUL7HGRND16Z2cHJrH+YyTBpit4xjf5OatJ7VcKaI0YLh6pEErcCdaZWISKUbq95XRJB9jhn4Yekz24dy2T7GtBtDu2cd1jEWek9zbdXl8joYquBDp+Xeb0TkWHGBt+y6EDDoU0rt8IQDNtwoK9wd7h1IwdITRMaraWKKGlx41WvnX8cX9jRvQCxvArfAFwiI9OwEa86FzIcbE7o/rtGQBjiUDt4WAt1l9pDXNB+9RhK+dgEAqjaoM0Bi8vT4CXwQcgTu9erNpqL4m3gOf0xn+uNqGV2PYSuKCGcUbAT5vnpwOu2C4Yy4fLoAuI7ZlRCeDBXIzajcou5hd3S79T3WTAJvY8MKepWRcRceTDbfTMaC8RD+CWCCKTFxeqY4Zpkv270UrEmk+RYKdSIs3qFwppsJFFkht614tmRBc6aJI3a1wSZkiwEroNb9KOr6U8TdaIi6I16uV5t6JZAEZolFsMOcDRenLf5nkoB52/VK9pIb9YZuRWpdmtG/mFvS1oEhzeGSfv223aPrquvOXGnE7Mi+sH+YrlYONJbBn1IrOhstozWCSw/NeFvBLjGVURY4VnKHxSUJbUqg+HGv1Q6X/YbVWlqWan01CTGIQU9MNoJJ/EyK0ui5m9wQSUcdBB78ofG1uttcne0uP3XCLO+ODRuGuvTIzNPgEHOEuoOE/pJ8umC/83BVvdU365vaFNgbbkD8tADg85k0rAgJTRj+Sw+NtU9J1U0MP8F01gzugdCm90Txa4X64p2wuOMJYf9ZQQTxqxGcRijXYtWP5qoPn2c6YhOYGOFAT9Noe5SLhSMuRAi7gwXRAPucRuiEDSuCNo8gFzegO/ACbiCO2f0xAFBcBZHc81leEeuYkSx2O60IFgd14pa/MJr1M8vMcu9ETbU3qOS18pFcCFuhG0kgEK7w52vq++FgkGvx4G7qh6GUd5g0Of313Te2kY7qYAt52ItM6XMh1I0k1EJUdERyQfRefTqAW10WWMASG1rTEGQKFpoj6YkiTh2e2apL0UZqRdkzhgI2w3P0UO7Al1Fnj8P3o9hMiw+lXJI1AW283l0ZpYMpUok8iX1/M4auu7YLTn8Kh+q2l5JHfe9KawV5EwTZnzV4+0twpufgkXZ5dM+0kdNWu2Z0gI4CVPc+c1MhuGcaEYld0rFF36V4flFt+l13WAKDV/CxbLGv6PrDGwVq4uXLmeTG1uFPrBeENdgfsu/DVPTbK8JiuoKnlmapuof8PnW29H9zcRYAGnInYCheNzSqeefOXhKK/XVYpcHrR76zRV1qAEIgzJDwegpokRSSrnMsBVDRRSr5iXnuMm1J+HBsPho9GHUHwnHgsFI0N9CkRMl+K7UD/VXszZX53wS3AF3co+dGZjxj3aVzmpCSmHabU/zWVosNOPG671Yk+JQFbnLy7yRhgkpO01WHoZZAl92a4LH7QhFXGED5UYi71vGTBlruuWxZnJs1CkZ+3ynheMsoUTQtLKYj9wxeEN3/HoT11PHIlA+VD50XTcXA15yGMIEV9JwJcKbl6lj+ZIoZI3H0oyZytOdMBCKyJyT1gNuglusJZOUojwVqHK+UGE52jl6ZBzZLRuv82mxC2v1ytTClQmZLo6uFJumZiG3HVUzTThX9upoCbc63TxmvrVcJ53HD929kurz+1MTq+QeDyFJXVGh8vUul63N7cSHjvEajfpSP/48jCKzzIvMcVnTyKg3JemgjfkSWsL1i/n/9jzxnHNcpUi2exS5zMkIMoYhyOACSWh/L+vNklgnwzlbQLnoDiudKemcygVBkjCqY/A9lckt4L2z5AdaDQnI5PoMoE+w0g76ylP8q1cZnwvsOPDqM8KprKNEOWvkpL8t+ztXF0T29fE2cWHDlxK+YBprUarZfZKvc3uQCI2Kp92nA4/AzkUcwBd214cKxWJ+FaJXcgAFzPseWPhtKbZ7GfY0OHMfWIr7DNS3q8ESThPqbwTB2bBvxjSA/+EkC2qK2Jowc1AmmhucCXDN0rr9QltHs1568MmWt/9chmjDt2DH1waD3x2eSYYMzWBe/kmeKW7nJ9VLUOTQgo9Bh8hS0BH8MzLyK7MH53k8CPqZzBDqS0SIDd69BRCF5JDyYH/P6ACt2k6z5cI+TgAZKHlsOMaZ94Vayt2S7Pe8HIfX9g7B7reTnRQCPHfFUsnvZ1Baz0+tQ+xyrg2BL1thuLYWKiSHV0ePadV9Bi7+5VXk/buhcDLIv1NX3cAWujIy3/O9JPZMQLoEArx1Ds5dge4HcydmYAsQIm428JELnHoyE5h7choLxG0bsA93Bv9cjWx7H34Hjy/6F9wH986KpipaQu3Qhe3q7hYcTWKW2IZUmVqtLlteDNaG2uS2EvlWbYupQY0kYZoyr1bj8gQrxfEGbBKBQY5G6YeIcS45zNV8veYcsPmCRomy4qV1GaW+GEllAKTa6Z4g7u3VtFzEL4sMw7rcUQ5FXsvmHRVoZbCI5FBOc20hpUxgqi2Wc2EwinMTsUJBEKCuaiTmOnDtkIfuSrrvR73KOlsSs2o7wNNZbXdjDUoaVzWUoHJkvySdIzstYMyURIm3Rz1iM5IJDvM2gYO6UmiogIwRBYE5TKBoow2HA/s6IFr0oAs0c2wXrpJzeC4C5R8Lfezj9V7ASjxfAleygAIMOlvzThexyIu2uoTkmUEbrDTFwm1GY0IdPB2gfyczOR2mNzR4ihM2o+iW6DQcyg7bilYiPYpYf59qRFe6r007epC/ukLoIIJiQMPkeFg92IlsuvktC9Z+XF/wgrc81lqpwqHOmUNgOS8Wq4dp5YMxxjNtO6Hf//wY6H9i//kdX9sjBNoKkzBZuatCn5DdKC0/5LApUUokRaE4g1nIZnX7yl4IBZfXN6mDitTG7qbmQfghi+Ko4zACWJC7e3sbu4YBh9V6Wcrc0Ubf3ZkbNVINaWWZ0ogztnRpCUG/cbkvl/T73FC5v73p/t4T1E4UGaLMmQL5+SFjuAPu4B5Llhy1g+nH5hVV2qXvF3orxaVwecgsrrhhVVeQE4bRfC63kJFx65DFOQjbQTxCm3hsqqM1G2Fd9sjCXFolwaUxmAyPjVGc35HUxbj9L6HhKp8OgNkGR53CtfRvxthqV3/Aw0jSbZOLKMCgkTGtquXjEKSdrX6Nkni2v7t5E16rM25kOw1rRcMLiwmuB7CIdUKBHJD6IRQd4okripRb9Wy1BWDPFwu8AvMetP76HhnwnX9p3rNbQb0Bt/8f5HAlupKPRW6RWu3g6rA/aF9+0pa38ATy2ML2sFJVsYNMp5HdhkNSmJJXVw94CiSjnNwATZ9KCoVlivcttrGw5k8WUbrTau9IG6P4sY2DlVjuKv+l+oc8OO35P0HLQb83hyt6GmyBDGqerhMSgbN9uKiX799nNvn2YyVtwwghIl1a1jRzj/NaczwbRNj9KTRNNf2Ga20ixkpklbObdVU04Y6ESUoiVHEbc+kAluHrRm2tS6YwlQ3NM+HXylIoudoiDDNRedSLMnAjLkzs5dDjZVJxfGLfJ8TGoP9a0aWem/1uyqLtHxyBIdqUCGYpPTYfmwi6mAj0fQmMLeA8bls0rS0ypdNSf83P23jMd4WWGr7xDX9rLX+BAXmlN9gqA5BBmlH7NOflT/lztKnVakCfiw9yvbdQOMX5kXZ8ojc0vnhW/HB44JpAADQb4SHmAcLf/+JIOQl4Gp4++rTCZIVGMMh9MLS0VpRdOKFpGZ4wdlO1y2EvoGZHfdvq7myYrlJ/ECxCC+DI4kxJGs+QrmLMQhw6BxRmolqvu1ocXJ7lHOfI7mlwAqTL0NPyIsQhnk/DgrLb+an0AOmm6a5BksKlXhN7n7k+jR1SL4Tb+RPJmRwgRaxpDWKlbbzLtjATWhbWUZRbB6+/ah+6kIOs9p2o8hgrpRNc9r403ArOVt2rBzfCN3y+Bbnvt9KpSMQVgyyxXJbZzFaNK9VKutfKNpkGfKas7qdMTpK4KyKSPhUF/S/qW5eqbpLCFwWxZXtrGkycBvVO5dNH0oPqj/uMSdeGKglNUTMuJYN6pHi8zJCLRV37YKyJa7SrYrqEHAzJlNjTkj0TkuJ7mJrwSbNGmqgP80NaDODTn45RNr9yiSvh9qC/3j0NrwL9jmhEbo4pTf3zlIPF4+XwYNPGm+g0YI2Fm8lNjpnIdMdbkVODk00Madn4Ij98/NB8ARMoBayxdDN/mqQcx3IIRTJCiFHp44gVd/dbhz0mdrIbQMk/z0zk1U+/YcqsGb5HD47Tf27p06MJIIB9PUBJpyAomQ6HQcUywY/VBi9WA1QsGMpYDlB3yfZKHUABfTAEuzrBQGA2xmhAj5EItGOUA5dj5KE+KjuB7UWFiPWtqICZCMTZ9RsFwTVwrVsf7MddqeoBAqPCmjtuQNtdI6PsoEwYfVtujbVWcxgRRqD9G01Mq5MGOH6rYOXYYOxEEYXnp3B84EFwet+bKuBDFV1Uo54eCfGUR6ltgwUsG0qOxAUatWYQ4HAZSnMeTVCia6IY4ZZwGJjAt32Qdf7udGG2nKKSWBn9Y6ObAg54pdPMQgE4Fx6QxdY6qI/opwRtePYsvqvscTYKpmojl/wHf30RWXD3Me1LcPGEchk8DX1QrKRpGw2bh3XaOBSogi0ipuqqnDN1BLYWUJzY7CITgjuQa4fdencvfv00BeyvueRjswPibIBFnFogHtj7TsepdTBgRJkQjSsYTFjrknDYddcQZNc52HPQXt1xEHijVoQIHw4+xB4VuzaE0nJ02YDuZmFMuG0D5nzHf7WZyTe1Axh6g/FK2cLXGEB3A82jZDllIAKtn5btydIfVchRIMmLDSHi0GLsSzZDjWCJQwQrT/lwMeU3djDfrrhwWmrqDJ1eMmlC0l4PvjNpRS0UVBUkjMvZsOSlSbIoZzwCr9UkDpu8baS0jzape3Aj3NjIEUrN4lLDhHSJxYAxwbleDqnnvsrCZeNlyxSPQlszMhE6mIKdIamiGtbLgqzzD/qXsbIqZRiGzu7wqMxYMfR9qUt2YdJ+yOMJeZ28VjCtabreiQbGwGa1VuaOAKORRLddVZHAtE0n1PGwmSfCD6NtuzgOYIpm0nHAgtuPkQbrHUeqTTgJ5D5Ih7ByATYKu5f3D8SQF9nOkoA/gPHLdzAMI7ztXEcwXzP6naZCRJWH7NC07X6ZEIO0Ba+FlAAxUW0KboPfiXKOQ0laNI5EFNAhRZDRRp+myl4EoWqFshqwfEsl7dEyuUIJs+BtmQy3vYYkFmxof3+1UiOebySOxcouxNAFwDUCWspoiuD5wnTSU2jH1qmGGZZxcFs6d1ipxbzB8X6CdWDMgzCtBMWtL7UuhwGF+aZnOy2ao6C1aKAUIKIp+VglXi5S61wwruXzqlo4QjojE8/rx7r7TGdsADZDn/Daej2XPXoQkiwaEEh9JxyOtzg97PYDBAh2ygGq7A2HBZWyrvIcxgwEjs4LxqknHLhFiW40gmW3b64o+pRcDojG6fqcKZezfIGQe5OgDe5zKPOUh+XxoqnbpnGZ+g1mOuhiq8738s6gY1td+tokHsxI0LJdFoB0xTtLHpeFhJttcxA2agihY75ZK6pSAjeq6jFJcLTE1q1ajPv9w1iZ6C3hfonFcqHkCty4xFHipqRkzITsuIGl9BFAmJB4ZwgwApiVHkh9jraqcDo+J0EoTXWeLbrdQsk4mQkCOMzaUWsLnlkzEQDiJxIjKHQX5EEorKuZISDE5gyizzyyzT899SSQT7836jSQhYVOM3eR0/1MAPX4OOmwkjFGRwveO1B+r72HZHQrMWj6tfOdMpSo2tt1YIz5+UdADrvyFK5AFWTDH5Z6j58OYHvyaNGmAkXfew4eC8j/dwwEENNxYeEkBzbhXRuWTObnGIAHcESt8xahobIY6UWB5Lb43AUOJ26wzw3jXVsd93WnTYsbfbiuSJQHgHmOiWJhZnnbGFFJH5Y2inYjj0Uf+KBd+O32+wiA00pE9vlO+WOCckQBY088Wcehi1ZcI4o59YcerVjMcjCvqOMa+w7lchVfB7uNepeLDoVCybjN+3ThisTR9TrNSdJx+uEzKRyDSTT18BxZshvNTmt7uXhskSrS8JKLE3l8O2MicRKDQzmFjziya9IPOSKPxXkkji1e3y/53MwtLnyY3Jgui4LM18lu1FsfuCtxpSZomP1u6loxq1MfF/bejAClSeM9PekIh8mDc+zrhyM0jPVWCoW524/XuYo1d9fVflyMBd6yOyWCOsznrE/b/VB+clBX8/ljx3h2Mu7sTUVetm3HK5xWJVvwlK9SH6LcjCiX0w3balY4T9w5/U2Pl6LDsfqGsP4F8Txs6p60Xq7CkSHyk6GokOW0Ws3tE7TR81dG8+obITemHz4T7Aat6HOwP3vR4OveS1CREUESxTfxVt9fXrrws+d/EyjCSvHSpehNwK7QlfJxXajqIYz0ClfFSeV1C4694W3HYsLi16eLA1WsAkC15CTM73NtzK/vfvrZbS1jQJqC7nBctRrOHumEK+X7Y9uh1enKvVYO2TVltrcwudbpPIY86MFYsRDcrMylD/F1XKwwX1SDgx2GQZ65GwCyKrNlwGCHhuy2uconLRf0ua/PDbdjLaB0h8cI+BPO7y5KI+BJ/K0SKneZwXpyiyPgKP7CrR2Xls+trJxDef7+++cLBxvgIuEPi1Byoamwu1LA+ltjnMqZbi/ML0Nngnd/Mj8FzmCuqHLYaepe5YSbYCoB60B1Hr4SyILYQw8eUw7kh+GWFqWnAZ50c7wxC4ARYKQ1693nmyU/otyyH9B3QECCi3iEtDw69b546xR+LE5ZoQXwP+MDhVx/rKsQ8QN3uQ2DZi1i9xTSzyIj+Kk4BB1go1pgJfHt/xwWyuoz/ji0w31N4AmAK+DKwpx3bV4sWWKjxC9TNAVXEEXhU5sdLHAyGOcgq6LPt6aobot5rHIZcmQNhVAbHJxewaLo3NtZ5X52R6km3Sgb7USjwyQkHFNXuvnUH5DN9LIDk7Y6mvJrazZWlnaYfT73+FOvW02JF0gFhaFyObBNsVm8hCBkXiyMsav4KX+pcriZ1dSsbhiVNxQf8EmihTHnCC3NSJHHcu+kGJ7nM0VZXRUZHIxQwpS6XWIoqyudOc37q/3Urzsejca7NzpDbE779NNDBReBFnhRJ7unH38iCYqyZEHXv8g3Q9sGR71xnE9fJS5sZZX6OcRuvtkDbGvUf54PAIHcHZiV7IC+thUp3378DIBJxhZO9E7barp6r/TOAl8tKgIB9gXhDfhroZYc43prO4UaDUsQQskisjNS5H8hBz9++Kab3i2cwl+K07J8Fck5tRSe9obdryRut3vfhX34Dfz1n84XS3Fg7ncvEl3/+on/tHQkZSA38rtH8EsH2jutbqNnH+E/gAEmE9C+DM5x1MLBJXA8/7Fm0HIJFZSTrXA8/EPwQWSX7EIm7je/qi0HAUuI39yQwlfeXZPbmT/fbQYz+x/p0N7Z2b0eqCoJQeWEaw1IEWYMvmlVr/AXnQEa6OsGZaj5h4Mu4C1urAexz8wZf6da3aloaXNxI6iVq/gWp1NbMtEQRBCnt3PKTlWK+K/ufgh/L8QSglghEU5bQ6nnZwK+q0gXwC9kN3tAek9rzgdwg7u+v6TsFE4DiTxE49GNab9Z6OoGakO4VxXYTR68wsDyytOorCehMS8IzXUVFkAzg+kPwHoZj38D/72PbLMLviSBNNcGx2gP6WEgqfy00m8C/XgqlUAU/KFMO/Ksz31afuqp6WxPMILDxYHVQEmDZTwZzF0YG7RFGiD8B/r3wXCrdl12FNsoAvqe8gStZHcmlU0mQjSwSk49oaxqLJ05glCaua10vldawbVIBJqnd9ZbBrVKFyagBAzPpkRUAKGJXASZDBWAKY5Xei0fCbJpRcsHyeFFl3Kch7e/U2zxF2pdqmUfogaukegXFe1QabtYXbs0R07v4FoohD4xQKFRA11dA0m7JGwckWT3reuSJQhZNVGWiSZjkk8K+OGKHrcgB+z9X0bgDrALQwx06UhciYZDPIVLdxf7jhoiuIXyu92T8d6oCIiJGmXKd1mrASUrmGaNhOq4Ls8bSihgFPEWzFvIJacpSkEIxtmcpxVXAtM2R/nZrNeoxeQ+A2JMrxNm3Qy8DL/mujPxUKoue52UkvSkjh80LOVHSJ3HyxQ2M/znaLyNUHMZk4TzNpE4PaFyvjAGKBIZGkmBAq+HJ6L4pq6WB6dPVk48/b7bjMZeGnyYFFvPkjE9LcmuIm5/+KqSd9n4OOsieEnakzAaRrfRkrSATrAS1ukFC2bp+EIUrDuJF4AOsLreSCRokpX/71fXmHSIN2KXtmIdFh5wehPMAdznh7sJYUSgsZyJonU8rh3zJGt9aluIhiE1Gq+H8LsfVl4Qkat7r68lxUF5wG7L6zWx3Tc2cKDYJpuFGLbxN2Yd0Xn9sw+2ftchaywxnl5DFPgZX237Nm8AW1V31xP5VE4KG+Enjh/+FP6Mv8pawNlOSRiUvaclHoOmUdzMBzABmpf5CcvMpuwiruxDZL/KM7x36hOPxE6TRHSYWG3uUnhD2oSQjCAEnYn38y+3zTKNGwlYQXvXtnB8uN0CUs1pKexln2dOS4KYc221rjiMpkOf6qq+HHiRL0x01ROyKXV32iFi2jXqGMje60YmvHoS2B2KTiZfMn3r3tAdDqQkAbnE1oUzPT1pErMvzL9JCyG3Ovzxxj4ZiLM2cMgwpMasdtuXf57Eh3kaOJEVeZnZbRsd1mupzR06/7qk+yqs8g5icUaFj1rzKNHhpSfG1dc636+TMDno17dBZABYRBJGJYowtUwCrCcEgRkGKPUGXb29XWHqIRwPMQRKPSvhRvnVFvX6SfXErw237QfFitTudpLdPlyMCzR0GmpJ4POhrBXU0sFdWwWCxTBEMSJ2XIRI0lSXqXQZ3mp/0nl+6vLAE0tDGc6Vg2LK6IebhUKMUMVeITdyMHofAYoXELCBcdl0ImmWRS3P88zSpQpE7PSirBf1MCyzHtFHMl+vv1o+TSnGBBGNIYRXk7mJ07TlF3ufNgdIKLOoK449J2TpMrHl3TLG036/CycDm88wwMy650wsh8NeS6Ykk1Lr3PTcBxdVg0EAztYBLlRHWSy0W/P6aS/n7FlCU8LSAU3SvsswniBFPDXrOyB3bshquAdhOqvAY4IFTlBsj7UmOtZosiJVBK7XpiiVzCjVzeyglpdabqIH1ZQPJDhWhGrS9CDiOwIAo7pbbwzkJF9112qEEhKyjdPfO+eggCOXwmfg+++ItM9TO60n+GdyyeRNvlnn+dPjEPpSHxDNpfgAPngZ5yENREdPRoFjyiqWa1iRqmz6Vt1RbEIGklcoZMMia40K67oYzhaIsCoGVbYPigwxLl28VoXQg4lSNB2QhjHz2JSWYuFh6I/F/AnXT3ZJtQVpYdffz9XasuazAE7JCXWc1v9Zgh1oLHpwQmrvJABm1DBmcDNN29fwK7gXjtUc+/+AT2JQBofkADNEJ3FaLg2TeKFL4QQcfcWm3L9MjdPhQ7eptyQii6KUTfEGsUYa063l3W85zq0aJcdTVdW03+xw2kmrpayDCJWaHVcr2C5Wjuo9OyZkmYHlOIJafs8J5D5EkNDxDiOsmn+m+1vBMe0ayvL8yv4+IcTly/aEum9MOzkJ4S8dXD1hau+W2e2EAdbaxlf1Z4+6YO77KdBR/Cb0MFrCJQSZ1VMnhIIPO1x3nNDLOsyCM2bv6rSlWWE4lmNcbKttzVJ/F5l1O1SV3rwGlssQfFy5MUUteo4jtXQcXsMj8ISPDdm9Qgetduyj7ZISyOVQZ/yjbcNIukG2VuNSWKWuWgarW+A6qPVazvbz6jVbqVvf3HM4iqGlHEkch7PUl5mcKgjRqExZqb7URUhke8ZJmZO3+7BW4LXV4kmS+M3sb6qj3VAhYzg/ikWhUjCuPd5GOVU+Meysbxb6nc1xvUaroSJllszzTCxdcT+NNvI6SjIRkXgySEZnt8BN1/UARzH6tYEY5T+qIDDiM08NlmDihSit+YfPpgSsRjyYUdB/TDSxeAXqaFJJN9gt5BoIRxghkMaVx4FN00ddDhOxFwwHQaBwDPuc7ffXR90RwGyyvUW8aG4X2u0lL7wVCoZMYyAEplFxT9+uk7lN6h0H0p89zhcFA05sPLuG1iH7t1UPVHhHKFHd0D4RpRtKUOAh6CxR5dvuViCZqRyU5iDIg/C4Fo9DlUvfI4y25/P8pu4VQIvBOiWua3oJniSTi/rirvjwzs7G2adTpbJsuMCuRpXCCfI7bfpa7eo4sxjsR5BcqnOYJIHeuDSfoHx7qWuEoP2hXNKrzNG34uTFdvGpcmXL40vHsdlchzDEN5azxpH4UTVy1vS72cXwF1fC39BuLEXxWk9a9VvaK4dzQfWwMjSnv6lNuZg+JgJIm3uU1lqW63zpyvfdr+Lu3cCGiJ4tFtyV5Ii2v5CR00QM9ZmlbUok4BcSEHiJB58u2D9HAx334GP+Ng0zMDedQS9GO77lAo58qwiG3yRauYSxs8ilW2I3d0eAqbRQ9Su5sjB+6IrrLHpKkgpgwZzOpqDZeq8h8vUd4sqVhjW4EgPKdR3dBH/dPsaumoYreXkBIPGk88/GQC8uKunrw661FEEZyNVkNKBym7fBnMaY6T8dZpNtgM3PW0XOAgIncVAUikZ5xYeU71mIRTcus7egZyUYY0IB8ZbsvSZ9sbBy+zzKl7hf917qvoQtuPFLzk1RcgMFYbxVtxCLc+/fvnKBufNJIPt7I4AxgPltGiHt6PSnpongfwNozbZhRbUT9jKQ/U6wGiygJA1A8xujoOQtTxQeP4DY0ZwXWN4wQ8LP4TxUjH2NGecydxLN8+iJbDvmh8+d64NHT0eVtsHCq3kjXV2nItTHOnienzIYlkU7GGN+HnNxHU8/KtFCTCqR1OUshIrIFdglKmpMx7O/6jiTMd1QW5h+S8hdmA+heSbbL68r/pnt1ilbgm1di376NKpYpJa2MqCoJOzgSXjI3bof+o0nM2oPU6oR4oUqUVj6yT1P9Lvn1ivPuwJ65e/eesc94jtuvenKJm76QPz3nZgWHQyKNBplqC8GOx3i+8Nw/DRiR3Vr77gBR3IlXK2LrsZEZ/kR8+NTbFDDILwOpoL5HHeDRgx7ggKJm8H4ERwPumAcBLvKw7q8beoYiJtIHjfOMGjuNJtWaA59frpgNfcn4qnglbgoZ/VF1J31auiyAykT9LX7dlx3qA9KS2y5yz/ua1mefxLv8RLENE+qwXg0rqNO07cvCVXKMeuQCVB44ZkWHZ8yBcSLklJi+Q75a64u/XHw5APqYbCPmR06xkUuBQyUh/TIqhdN7shDpQxqFuDMf7+EgjQgE66xdxkP5UsNfqqxTuT2Tm3x7Gl8sMEdVA8Q+fE5ffHJnwWaQn2SENuuuMkpqmY/FG0OHMle8WCTPzigjAiCEPSdpE7zu4rHJbXuCRaaUJ4kZJG01/OI9E4FNMzYU8Ie2mbNczMB3B2rOxB5yNlznrjeAeEI0B5ySKm3Ot7C5+M5TRtNnjazbWcmZalO5MDzDxs2dDXi+qY0CTgU/ZSMDZsCZn1XFsO6xFs3nYfaA/RBn3i0Z/m15FVmmvGxrKqKU6FhmJaF4yI6YllOEMQmDM9jsoUz1DbZCaeBmb6O6YrgE2sVNXSrHhdOCqufpHhfY/b2En8uSXS/dF0cDETiXlAQbLmjyBKP0b25Y58BPXER0pAuVFchSgQRIjgumlkNc2gFH50TS7Tym6KrtrR/0Q3umrxYNuyQVBLcii7KGoZt6nIhsazxhKXD6biGAbwT4eHYV0CbddBA4w0OdMIfqYYnXKImtbe8TtdZnmiiJU1bLBMlzQiwhMikZPVIWZQ/PbB6MzawBSsVUeDMsvlrQvJQ3j5Ot007xIx/twxD6zswno3mcDgxQ/Og3dv/O4FeuHtrlJPcaUeEGjL89RpB5H4mcvOaiPr9Lf0OdaVAanuPiiplXvcIxGG1374YmWzT2jSIqnFk+Ed7bfOyRny5BZ8H81f/cOAnMa4OLMUgA1/F4AONGP7ACjDVfSDazM2FMhCIiWYSQ8hw3/cYKNEVdOUWgQDfPWP51/jWU7n63yyOvwHgl2ceb19c+K/5b6XntDzHrJkBdBgABP31sOacVpX+e6hFVeMK5ycJWa3j0UfOevSi1S7hdup3NPVX7FsH0CglSK0JIIxTHnT+khRD+mkXA+UUTs0ggKxGU6jUBo6oXy/Wj9hLA1W2emD0w/gaxkb9eq/Go2Swax9Rzj2AoGG4VaXBLdX0eFCRSMoaebatBIknppo1QDxMi+FUll6UFWfLg4gxSY1QjNRjDw61FYXyU4GKmpgUSG2A/JllCfXHhXPtpJ0IVp5GFfNyFVGSzXjU9NGxqhphc7lJheFQhngo/C61A2XNROvWh/DIYmTZosvSB3FOi11VjLJyfZWPtCoQW8oGQWmQBURJKIQL5DCFY+IUYXVTp3Qh842R04lEGg6RZMsVwvK8JQ2pmxRZif7uWWRJPUTQOBMu6/E3mJXMOQ3fYYzcYO8S58mXxAbN6dLlLbc5o1gOyCOePYHdWz+e6FNkGeu1lC4ntuQq+nTrVWHfHT72ZLlpB9ptKhoYrpS1mFeO/UwX36X0kZch1olgDU4pi/2hvdDmcbunW483LUFWCepZ4tGUTY34jNaMJvuQu0mWKJnHAI1+yXI5ierTdYeQHb7sUwWyYpD4fr4kziVxdmUlFqx8JkV/tbmWc9hjfwL2Yzcer+72+aQ1TyGfIvCwFDkB/zWucIGA86nDk4YKGZuWf9WGZIFDAyYAb5U4VQhRPIXhFKZwjrmhKMQZBFX2gSbb1nC4R70JApgffyWAEW+ljyBWEEq+MoD0TYkgvFGGCN2oEVt7AjKquwmSwDaqjBHZraTw9CkTJFoVCUODMkUgT5nBl6DMkXJXFhDkyhIhIawk8V2sIQBXKFMqk1aYKt22RKFsIXK5smDt+Sdk1hQnI3kbpE5thUxwWXqYHGKkPApjcxxvO0sVIsrHliIKOU+rVupHSjhytvkKKMZ4rMf8CGrsEpugwJ7Uxs6ZcmXhrDH6y1Xy5sBllnM9nDe3aVU1VxNcebCi+PQK+eo2Ti6b6kbbCokRFaaw3JKcWg725MhxN+RDJexDaZwAkWacNUZmD5WZAPOd9SpWLiU5c5RWV+TKc66OsMExXlumqGCyL1r9VLfElK1CI00vaw/nUyi4dbUsCtkvxRYHNSut22RKJLGP29hrAzM+0vQGZ3+w8wG2O58ojMmyS7bdFrFkJYe1t2RynXPBRTZs2ZG75LIrrrIP3l+dn57H2TXX5btpsT32cvGRK7ee7PCtKAp8vHjzse9NW0yh56L0xaU2ChKsTIh3QpU3on22sH7ifPMdVWrUJh+qRYWBaD3guV7qxKnXqEmDTebYJ94nCWZI1G2mJM1a/GKuZClSaXxw1KzMhSGnk4D1+sIEvR75VPqz82FGJCEQC7GRzhZj/WtXV756CtRXqIEiDRVrpERjpZpIaqqZ5lpoqZXWHDfpT98j00Zb7ZRrr0IHHcHjpLMuuqKLy5jUDhRbmNnsrE46aPimx013ePY7wB8LW5oMvlSGjTjokAGH/aHfSadQEfHQUy+9McfUpUOvHr+aFx+VpHujzQn0TKPdChyffTGIZMrEUpm28sOIr9NVCQmLiGrRqk27mLgOnbokJFkgwHx3PXLPfY9TM2oOlBhJT6QLxgd7xGI5JJaJJfN7RP1DyX7a45hLtkllKpaUw/My8Yxom9R6543Fx5aeR/TJYGTuofT7jWXmDA2m756xyWzm7Z7uxffcvdYV354Jfo32TNk735xd7e12DfYMzny9+sJ2zwuWCmEWBGHmB4lAVgBhLwv7FwuwQ31pC5YCHBRAAhDcHAgAIKk95Y6RilXieKJYDGuiUk/Ge3JEFVFlVOWq5XtB4qbcklP32SR+z/ecmD4hC1n70ILKKLdyL4G95M4h0yd/JxCIrhvh4IA7C/c7mOtTjcaqnyj01bCUh+1qnSV3zm4du3P2bdjJP3guwdzukfexk+PD8yP2N5i98J4Qq7Gsav6xIo9fyA4kPKlkMxLuolJPGV8Gmj/W05+6m0telBsa6ttNYpBjC1NDY3tHXHVbkKr8ETJ1oVS0gv9gq+1iSywHTbIO9GwYeTXv/u5eynYA";
|
|
16911
|
+
|
|
16912
|
+
// ../chrome-extension/connection-page.ts
|
|
16913
|
+
function renderChromeConnectionPage(downloadUrl, storeUrl, content) {
|
|
16914
|
+
const hosted = downloadUrl !== "/extension.zip";
|
|
16915
|
+
const installUrl = storeUrl && /^https:\/\/chromewebstore\.google\.com\/detail\/[a-z0-9-]+\/[a-p]{32}$/.test(storeUrl) ? storeUrl : downloadUrl;
|
|
16916
|
+
const page = `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${content?.title ?? (hosted ? "Record with your chat" : "Record with your agent")} \xB7 Rill</title><style>
|
|
16917
|
+
@font-face{font-family:Geist;src:url('${rillFont}') format('woff2');font-weight:100 900;font-display:swap}:root{font-family:Geist,system-ui,sans-serif;-webkit-font-smoothing:antialiased;color:#171717;background:#f7f6f1;--purple:#7146ff;--muted:#6d6a66}*{box-sizing:border-box}body{margin:0;line-height:1.6}header{display:flex;align-items:center;justify-content:space-between;padding:24px clamp(24px,6vw,80px);border-bottom:1px solid #dedbd4}.brand{display:flex;align-items:center;gap:10px;font-size:22px;font-weight:650;letter-spacing:-.6px}.brand img{width:32px;height:32px}.context{font-size:12px;color:var(--muted)}main{width:min(520px,calc(100% - 40px));margin:8vh auto 5vh;background:#fff;padding:32px;border-radius:28px;box-shadow:0 1px 3px #00000008,0 12px 40px #00000006}.step{font-size:12px;color:#4c25cf;margin:0 0 16px}h1{font-weight:550;font-size:34px;line-height:1.14;letter-spacing:-1.4px;text-wrap:balance;margin:0 0 18px}p{color:var(--muted);text-wrap:pretty;font-size:15px;margin:0 0 22px}button,.button{display:flex;align-items:center;justify-content:center;font:550 15px Geist,system-ui,sans-serif;min-height:48px;width:100%;padding:13px 18px;border:0;border-radius:12px;color:white;background:var(--purple);cursor:pointer;text-decoration:none;transition:scale .16s cubic-bezier(.2,0,0,1),background-color .16s}button:hover,.button:hover{background:#4c25cf}button:active,.button:active{scale:.96}button:disabled{opacity:.5;cursor:default}a:focus-visible,button:focus-visible{outline:3px solid var(--purple);outline-offset:4px}#status{font-size:13px;margin:16px 0 0;min-height:21px}.privacy{margin-top:28px;padding-top:22px;border-top:1px solid #eee9e1;font-size:12px;line-height:1.7}footer{text-align:center;color:var(--muted);font-size:12px;padding:0 24px 32px}[hidden]{display:none!important}@media(max-width:540px){main{margin-top:5vh;padding:24px}h1{font-size:30px}header{padding:20px 24px}}@media(prefers-reduced-motion:reduce){button,.button{transition:none}button:active,.button:active{scale:1}}
|
|
16918
|
+
</style></head><body><header><div class="brand"><img src="${rillLogo}" alt="">Rill</div><span class="context">Chrome recorder</span></header><main><p class="step" id="step">One-time setup</p><h1 id="heading">Record right where you work.</h1><p id="intro">${hosted && !content?.direct ? "Connect Rill to Chrome. We\u2019ll guide you through recording your page, then bring the video back to your chat." : "Connect Rill to Chrome, then choose the page you want to record."}</p><button id="connect-chrome" hidden disabled>Connect Chrome</button><a id="install" class="button" href="${installUrl}">Get Rill for Chrome</a><p id="status" role="status" aria-live="polite">Add Rill for Chrome, then reload this page to continue.</p><p class="privacy">You choose what to record. Rill saves only that tab, including anything visible in it, with browser errors and request details. ${hosted ? "Your video starts private in your Rill workspace." : "Rill creates a recording link. Anyone with that link can watch until you turn off sharing."}</p></main><footer>Your page. Your steps. One clear recording.</footer></body></html>`;
|
|
16919
|
+
return content?.body ? page.replace(/<main>[\s\S]*<\/main>/, () => `<main>${content.body}</main>`) : page;
|
|
16920
|
+
}
|
|
16921
|
+
|
|
16922
|
+
// ../recorder/src/browser-connection.ts
|
|
16923
|
+
import { EventEmitter } from "events";
|
|
16924
|
+
import { randomBytes as randomBytes3, timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
16925
|
+
import { createServer as createServer2 } from "http";
|
|
16926
|
+
import { WebSocketServer, WebSocket } from "ws";
|
|
16927
|
+
import { readFile as readFile7, writeFile as writeFile6, mkdir as mkdir7 } from "fs/promises";
|
|
16928
|
+
import { dirname } from "path";
|
|
16929
|
+
var allowedMethods = /* @__PURE__ */ new Set(["Page.enable", "Page.getLayoutMetrics", "Page.startScreencast", "Page.stopScreencast", "Page.screencastFrameAck", "Runtime.enable", "Network.enable"]);
|
|
16930
|
+
var allowedEvents = /* @__PURE__ */ new Set(["Page.screencastFrame", "Page.frameNavigated", "Runtime.consoleAPICalled", "Runtime.exceptionThrown", "Network.requestWillBeSent", "Network.responseReceived", "Network.loadingFailed", "Network.loadingFinished", "Rill.coverage"]);
|
|
16931
|
+
var BrowserConnection = class extends EventEmitter {
|
|
16932
|
+
constructor(options = {}) {
|
|
16933
|
+
super();
|
|
16934
|
+
this.options = options;
|
|
16935
|
+
}
|
|
16936
|
+
options;
|
|
16937
|
+
server;
|
|
16938
|
+
websocketServer;
|
|
16939
|
+
socket;
|
|
16940
|
+
token = randomBytes3(32).toString("hex");
|
|
16941
|
+
pending = /* @__PURE__ */ new Map();
|
|
16942
|
+
sequence = 0;
|
|
16943
|
+
address = "";
|
|
16944
|
+
tab;
|
|
16945
|
+
starting;
|
|
16946
|
+
lastRecordingActivity = Date.now();
|
|
16947
|
+
captureInUse = false;
|
|
16948
|
+
status() {
|
|
16949
|
+
return { connected: Boolean(this.tab && this.socket?.readyState === WebSocket.OPEN), tab: this.tab, captureOnly: true };
|
|
16950
|
+
}
|
|
16951
|
+
get connected() {
|
|
16952
|
+
return this.status().connected;
|
|
16953
|
+
}
|
|
16954
|
+
get keepsRecorderAlive() {
|
|
16955
|
+
return Boolean(this.server?.listening && Date.now() - this.lastRecordingActivity < 60 * 6e4);
|
|
16956
|
+
}
|
|
16957
|
+
claimCapture() {
|
|
16958
|
+
if (this.captureInUse) throw new Error("This tab is already recording. Stop it before starting another take.");
|
|
16959
|
+
this.captureInUse = true;
|
|
16960
|
+
return () => {
|
|
16961
|
+
this.captureInUse = false;
|
|
16962
|
+
};
|
|
16963
|
+
}
|
|
16964
|
+
async connect() {
|
|
16965
|
+
if (!this.server || this.starting) {
|
|
16966
|
+
this.starting ??= this.listen().catch((error51) => {
|
|
16967
|
+
this.server = void 0;
|
|
16968
|
+
throw error51;
|
|
16969
|
+
}).finally(() => {
|
|
16970
|
+
this.starting = void 0;
|
|
16971
|
+
});
|
|
16972
|
+
await this.starting;
|
|
16973
|
+
}
|
|
16974
|
+
return { ...this.status(), connectionUrl: `${this.address}/rill-connect#${this.token}` };
|
|
16975
|
+
}
|
|
16976
|
+
async listen() {
|
|
16977
|
+
let port = 0;
|
|
16978
|
+
if (this.options.statePath) {
|
|
16979
|
+
try {
|
|
16980
|
+
const saved = JSON.parse(await readFile7(this.options.statePath, "utf8"));
|
|
16981
|
+
if (/^[a-f0-9]{64}$/.test(saved.token) && Number.isInteger(saved.port) && saved.port > 0 && saved.port < 65536) {
|
|
16982
|
+
this.token = saved.token;
|
|
16983
|
+
port = saved.port;
|
|
16984
|
+
}
|
|
16985
|
+
} catch {
|
|
16986
|
+
}
|
|
16987
|
+
}
|
|
16988
|
+
this.server = createServer2((request3, response) => {
|
|
16989
|
+
if (this.address && request3.headers.host === new URL(this.address).host && request3.method === "GET" && request3.url === "/extension.zip" && this.options.extensionArchive) {
|
|
16990
|
+
void readFile7(this.options.extensionArchive).then((bytes) => {
|
|
16991
|
+
response.writeHead(200, { "Content-Type": "application/zip", "Content-Disposition": 'attachment; filename="rill-chrome-extension.zip"', "Cache-Control": "no-store" });
|
|
16992
|
+
response.end(bytes);
|
|
16993
|
+
}, () => {
|
|
16994
|
+
response.writeHead(404);
|
|
16995
|
+
response.end("The download is not available. Ask your agent to rebuild this Rill preview.");
|
|
16996
|
+
});
|
|
16997
|
+
return;
|
|
16998
|
+
}
|
|
16999
|
+
if (!this.address || request3.headers.host !== new URL(this.address).host || request3.url !== "/rill-connect" || request3.method !== "GET") {
|
|
17000
|
+
response.writeHead(404);
|
|
17001
|
+
response.end();
|
|
17002
|
+
return;
|
|
17003
|
+
}
|
|
17004
|
+
response.writeHead(200, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store", "Referrer-Policy": "no-referrer", "Content-Security-Policy": "default-src 'none'; style-src 'unsafe-inline'; img-src data:; font-src data:; frame-ancestors 'none'; base-uri 'none'", "X-Content-Type-Options": "nosniff" });
|
|
17005
|
+
response.end(connectionPage);
|
|
17006
|
+
});
|
|
17007
|
+
this.websocketServer = new WebSocketServer({ noServer: true, maxPayload: 6 * 1024 * 1024, perMessageDeflate: false });
|
|
17008
|
+
this.server.on("upgrade", (request3, socket, head) => {
|
|
17009
|
+
const supplied = request3.headers["sec-websocket-protocol"]?.split(",").map((value) => value.trim());
|
|
17010
|
+
const candidate = supplied?.[1] ?? "";
|
|
17011
|
+
const authorized = /^[a-f0-9]{64}$/.test(candidate) && timingSafeEqual2(Buffer.from(candidate), Buffer.from(this.token));
|
|
17012
|
+
if (!this.address || request3.headers.host !== new URL(this.address).host || request3.url !== "/capture" || !/^chrome-extension:\/\/[a-p]{32}$/.test(request3.headers.origin ?? "") || supplied?.[0] !== "rill-capture" || !authorized || this.socket) {
|
|
17013
|
+
socket.end("HTTP/1.1 403 Forbidden\r\nConnection: close\r\n\r\n");
|
|
17014
|
+
return;
|
|
17015
|
+
}
|
|
17016
|
+
this.websocketServer.handleUpgrade(request3, socket, head, (peer) => this.attach(peer));
|
|
17017
|
+
});
|
|
17018
|
+
await new Promise((resolve, reject) => {
|
|
17019
|
+
this.server.once("error", reject);
|
|
17020
|
+
this.server.listen(port, "127.0.0.1", resolve);
|
|
17021
|
+
});
|
|
17022
|
+
const address = this.server.address();
|
|
17023
|
+
if (!address || typeof address === "string") throw new Error("Could not connect Chrome. Try again.");
|
|
17024
|
+
this.address = `http://127.0.0.1:${address.port}`;
|
|
17025
|
+
if (this.options.statePath) {
|
|
17026
|
+
await mkdir7(dirname(this.options.statePath), { recursive: true, mode: 448 });
|
|
17027
|
+
await writeFile6(this.options.statePath, JSON.stringify({ token: this.token, port: address.port }), { mode: 384 });
|
|
17028
|
+
}
|
|
17029
|
+
}
|
|
17030
|
+
attach(peer) {
|
|
17031
|
+
this.socket = peer;
|
|
17032
|
+
const helloTimer = setTimeout(() => {
|
|
17033
|
+
if (!this.tab) peer.close(1008, "Choose a tab");
|
|
17034
|
+
}, 1e4);
|
|
17035
|
+
peer.on("error", () => peer.close());
|
|
17036
|
+
peer.on("message", (bytes) => {
|
|
17037
|
+
try {
|
|
17038
|
+
const message = JSON.parse(bytes.toString());
|
|
17039
|
+
if (message.type === "ping") {
|
|
17040
|
+
peer.send('{"type":"pong"}');
|
|
17041
|
+
return;
|
|
17042
|
+
}
|
|
17043
|
+
if (message.type === "tab") {
|
|
17044
|
+
if (!Number.isInteger(message.tabId) || message.tabId < 0 || !/^[a-zA-Z0-9_-]{1,128}$/.test(message.targetId) || !/^https?:\/\//.test(message.url)) throw new Error("Invalid tab");
|
|
17045
|
+
const next = { tabId: String(message.tabId), targetId: message.targetId, title: redactDiagnosticText(String(message.title ?? "").replace(/[\u0000-\u001f\u007f]/g, ""), 200), url: sanitizeUrl(message.url) };
|
|
17046
|
+
if (this.tab && this.tab.targetId !== next.targetId) throw new Error("Disconnect before choosing another tab");
|
|
17047
|
+
this.tab = next;
|
|
17048
|
+
clearTimeout(helloTimer);
|
|
17049
|
+
this.emit("connected", this.status());
|
|
17050
|
+
return;
|
|
17051
|
+
}
|
|
17052
|
+
if (message.type === "event" && this.tab && allowedEvents.has(message.method)) {
|
|
17053
|
+
this.emit(message.method, message.params);
|
|
17054
|
+
return;
|
|
17055
|
+
}
|
|
17056
|
+
if (message.type === "reply") {
|
|
17057
|
+
const pending2 = this.pending.get(message.id);
|
|
17058
|
+
if (!pending2) return;
|
|
17059
|
+
this.pending.delete(message.id);
|
|
17060
|
+
clearTimeout(pending2.timer);
|
|
17061
|
+
if (message.error) pending2.reject(new Error("Chrome could not finish recording. Keep the selected tab open and try again."));
|
|
17062
|
+
else pending2.resolve(message.result);
|
|
17063
|
+
}
|
|
17064
|
+
} catch {
|
|
17065
|
+
peer.close(1008, "Could not connect");
|
|
17066
|
+
}
|
|
17067
|
+
});
|
|
17068
|
+
peer.on("close", () => {
|
|
17069
|
+
clearTimeout(helloTimer);
|
|
17070
|
+
if (this.socket !== peer) return;
|
|
17071
|
+
this.socket = void 0;
|
|
17072
|
+
this.tab = void 0;
|
|
17073
|
+
for (const pending2 of this.pending.values()) {
|
|
17074
|
+
clearTimeout(pending2.timer);
|
|
17075
|
+
pending2.reject(new Error("browser_unavailable: Chrome disconnected. Your captured recording is being saved."));
|
|
17076
|
+
}
|
|
17077
|
+
this.pending.clear();
|
|
17078
|
+
this.emit("disconnected");
|
|
17079
|
+
});
|
|
17080
|
+
}
|
|
17081
|
+
send(method, params = {}) {
|
|
17082
|
+
if (method === "Page.startScreencast" || method === "Page.stopScreencast") this.lastRecordingActivity = Date.now();
|
|
17083
|
+
if (!allowedMethods.has(method)) return Promise.reject(new Error("This connection only records the selected tab."));
|
|
17084
|
+
if (!this.connected) return Promise.reject(new Error("browser_unavailable: Choose a tab in Rill for Chrome first."));
|
|
17085
|
+
const id = ++this.sequence;
|
|
17086
|
+
return new Promise((resolve, reject) => {
|
|
17087
|
+
const timer = setTimeout(() => {
|
|
17088
|
+
this.pending.delete(id);
|
|
17089
|
+
reject(new Error("browser_unavailable: Chrome took too long to respond. Try reconnecting your tab."));
|
|
17090
|
+
}, 1e4);
|
|
17091
|
+
this.pending.set(id, { resolve, reject, timer });
|
|
17092
|
+
this.socket.send(JSON.stringify({ id, method, params }), (error51) => {
|
|
17093
|
+
if (error51) {
|
|
17094
|
+
clearTimeout(timer);
|
|
17095
|
+
this.pending.delete(id);
|
|
17096
|
+
reject(error51);
|
|
17097
|
+
}
|
|
17098
|
+
});
|
|
17099
|
+
});
|
|
17100
|
+
}
|
|
17101
|
+
disconnect() {
|
|
17102
|
+
this.socket?.close(1e3, "Disconnected");
|
|
17103
|
+
}
|
|
17104
|
+
expireIdle() {
|
|
17105
|
+
if (Date.now() - this.lastRecordingActivity > 60 * 6e4) this.disconnect();
|
|
17106
|
+
}
|
|
17107
|
+
async close() {
|
|
17108
|
+
this.socket?.terminate();
|
|
17109
|
+
await new Promise((resolve) => this.websocketServer ? this.websocketServer.close(() => resolve()) : resolve());
|
|
17110
|
+
await new Promise((resolve) => this.server ? this.server.close(() => resolve()) : resolve());
|
|
17111
|
+
}
|
|
17112
|
+
};
|
|
17113
|
+
var connectionPage = renderChromeConnectionPage("/extension.zip", "");
|
|
17114
|
+
|
|
17115
|
+
// ../recorder/src/extension-session.ts
|
|
17116
|
+
import { mkdir as mkdir8, writeFile as writeFile7 } from "fs/promises";
|
|
17117
|
+
import { join as join10 } from "path";
|
|
17118
|
+
var ExtensionRecordingSession = class _ExtensionRecordingSession {
|
|
17119
|
+
constructor(options, connection) {
|
|
17120
|
+
this.options = options;
|
|
17121
|
+
this.connection = connection;
|
|
17122
|
+
this.targetId = connection.status().tab.targetId;
|
|
17123
|
+
}
|
|
17124
|
+
options;
|
|
17125
|
+
connection;
|
|
17126
|
+
startedAt = /* @__PURE__ */ new Date();
|
|
17127
|
+
screencast;
|
|
17128
|
+
console = [];
|
|
17129
|
+
network = /* @__PURE__ */ new Map();
|
|
17130
|
+
timeline = [];
|
|
17131
|
+
listeners = [];
|
|
17132
|
+
interrupted = false;
|
|
17133
|
+
truncated = false;
|
|
17134
|
+
stopPromise;
|
|
17135
|
+
targetId;
|
|
17136
|
+
releaseCapture;
|
|
17137
|
+
static async start(options, connection) {
|
|
17138
|
+
if (!connection.connected) throw new Error("browser_unavailable: Open Rill in Chrome and choose a tab to record.");
|
|
17139
|
+
if (options.url || options.cdpUrl || options.tabId) throw new Error("validation_failed: Choose the tab in Rill for Chrome. Recording keeps the page as it is.");
|
|
17140
|
+
if (options.encoding !== "remote") throw new Error("validation_failed: Connected Chrome currently uses the standard recording upload.");
|
|
17141
|
+
if (options.controllerTarget && options.controllerTarget !== connection.status().tab.targetId) throw new Error("browser_unavailable: Your agent is using a different tab. Select the same tab before recording.");
|
|
17142
|
+
const session = new _ExtensionRecordingSession(options, connection);
|
|
17143
|
+
session.releaseCapture = connection.claimCapture();
|
|
17144
|
+
session.screencast = new ScreencastRecorder(connection, join10(options.outputDirectory, "frames"), options.width ?? 1280, options.height ?? 720);
|
|
17145
|
+
try {
|
|
17146
|
+
await mkdir8(options.outputDirectory, { recursive: true, mode: 448 });
|
|
17147
|
+
await writeFile7(join10(options.outputDirectory, "remote-capture.json"), JSON.stringify({ expiresAt: Date.now() + 864e5 }), { mode: 384 });
|
|
17148
|
+
session.attachDiagnostics();
|
|
17149
|
+
await connection.send("Runtime.enable");
|
|
17150
|
+
await connection.send("Network.enable");
|
|
17151
|
+
await session.screencast.start();
|
|
17152
|
+
await session.screencast.waitForFirstFrame();
|
|
17153
|
+
return session;
|
|
17154
|
+
} catch (error51) {
|
|
17155
|
+
await session.screencast.stop(join10(options.outputDirectory, "recording.webm"), 0.1, true).catch(() => void 0);
|
|
17156
|
+
session.detach();
|
|
17157
|
+
session.releaseCapture?.();
|
|
17158
|
+
throw error51;
|
|
17159
|
+
}
|
|
17160
|
+
}
|
|
17161
|
+
status() {
|
|
17162
|
+
const elapsedSeconds = Math.max(0, (Date.now() - this.startedAt.getTime()) / 1e3);
|
|
17163
|
+
const maxDurationSeconds = this.options.maxDurationSeconds ?? 600;
|
|
17164
|
+
return {
|
|
17165
|
+
recordingId: this.options.recordingId,
|
|
17166
|
+
cdpUrl: "",
|
|
17167
|
+
tabId: this.targetId,
|
|
17168
|
+
browser: "existing",
|
|
17169
|
+
startedAt: this.startedAt.toISOString(),
|
|
17170
|
+
elapsedSeconds,
|
|
17171
|
+
maxDurationSeconds,
|
|
17172
|
+
remainingSeconds: this.stopPromise ? 0 : Math.max(0, maxDurationSeconds - elapsedSeconds),
|
|
17173
|
+
stopsAt: new Date(this.startedAt.getTime() + maxDurationSeconds * 1e3).toISOString(),
|
|
17174
|
+
status: this.stopPromise ? "finalizing" : "recording",
|
|
17175
|
+
interrupted: this.interrupted,
|
|
17176
|
+
captureOnly: true,
|
|
17177
|
+
controllerVerified: false,
|
|
17178
|
+
...this.interrupted ? { message: "Chrome disconnected. The captured part of your recording is being saved." } : {}
|
|
17179
|
+
};
|
|
17180
|
+
}
|
|
17181
|
+
stop(reason = "agent_request") {
|
|
17182
|
+
return this.stopPromise ??= this.finalize(reason);
|
|
17183
|
+
}
|
|
17184
|
+
on(method, listener) {
|
|
17185
|
+
this.connection.on(method, listener);
|
|
17186
|
+
this.listeners.push([method, listener]);
|
|
17187
|
+
}
|
|
17188
|
+
detach() {
|
|
17189
|
+
for (const [method, listener] of this.listeners) this.connection.off(method, listener);
|
|
17190
|
+
this.listeners = [];
|
|
17191
|
+
}
|
|
17192
|
+
offset() {
|
|
17193
|
+
return Math.max(0, Date.now() - this.startedAt.getTime());
|
|
17194
|
+
}
|
|
17195
|
+
event(type, label) {
|
|
17196
|
+
if (this.timeline.length >= 1e4) {
|
|
17197
|
+
this.truncated = true;
|
|
17198
|
+
return;
|
|
17199
|
+
}
|
|
17200
|
+
this.timeline.push({ id: `time_${this.timeline.length + 1}`, offsetMs: this.offset(), type, label });
|
|
17201
|
+
}
|
|
17202
|
+
attachDiagnostics() {
|
|
17203
|
+
this.on("Rill.coverage", () => {
|
|
17204
|
+
this.truncated = true;
|
|
17205
|
+
this.event("lifecycle", "Another tab opened. It is outside this recording.");
|
|
17206
|
+
});
|
|
17207
|
+
this.on("disconnected", () => {
|
|
17208
|
+
this.interrupted = true;
|
|
17209
|
+
this.event("lifecycle", "Chrome disconnected. Recording stopped.");
|
|
17210
|
+
});
|
|
17211
|
+
this.on("Page.frameNavigated", (value) => {
|
|
17212
|
+
if (!value.frame?.parentId) this.event("navigation", sanitizeUrl(String(value.frame?.url ?? "")));
|
|
17213
|
+
});
|
|
17214
|
+
this.on("Runtime.consoleAPICalled", (value) => {
|
|
17215
|
+
if (this.console.length >= 1e4) {
|
|
17216
|
+
this.truncated = true;
|
|
17217
|
+
return;
|
|
17218
|
+
}
|
|
17219
|
+
const severity = value.type === "error" || value.type === "assert" ? "error" : value.type === "warning" || value.type === "warn" ? "warning" : "info";
|
|
17220
|
+
const text = redactDiagnosticText((value.args ?? []).slice(0, 20).map((arg) => typeof arg.value === "string" ? arg.value : typeof arg.value === "number" || typeof arg.value === "boolean" ? String(arg.value) : "[object]").join(" "), 2e3);
|
|
17221
|
+
this.console.push({ id: `con_${this.console.length + 1}`, offsetMs: this.offset(), severity, text, stack: [] });
|
|
17222
|
+
if (severity !== "info") this.event("console", text.slice(0, 160));
|
|
17223
|
+
});
|
|
17224
|
+
this.on("Runtime.exceptionThrown", (value) => {
|
|
17225
|
+
if (this.console.length >= 1e4) {
|
|
17226
|
+
this.truncated = true;
|
|
17227
|
+
return;
|
|
17228
|
+
}
|
|
17229
|
+
const text = redactDiagnosticText(String(value.exceptionDetails?.exception?.description ?? value.exceptionDetails?.text ?? "Page error"), 2e3);
|
|
17230
|
+
this.console.push({ id: `con_${this.console.length + 1}`, offsetMs: this.offset(), severity: "error", text, stack: [] });
|
|
17231
|
+
this.event("exception", text.slice(0, 160));
|
|
17232
|
+
});
|
|
17233
|
+
this.on("Network.requestWillBeSent", (value) => {
|
|
17234
|
+
if (this.network.size >= 5e3) {
|
|
17235
|
+
this.truncated = true;
|
|
17236
|
+
return;
|
|
17237
|
+
}
|
|
17238
|
+
this.network.set(value.requestId, { id: `net_${this.network.size + 1}`, offsetMs: this.offset(), method: String(value.request?.method ?? "GET").slice(0, 20), url: sanitizeUrl(String(value.request?.url ?? "")), resourceType: String(value.type ?? "Other").slice(0, 40) });
|
|
17239
|
+
});
|
|
17240
|
+
this.on("Network.responseReceived", (value) => {
|
|
17241
|
+
const event = this.network.get(value.requestId);
|
|
17242
|
+
if (event && Number.isInteger(value.response?.status)) event.status = value.response.status;
|
|
17243
|
+
});
|
|
17244
|
+
this.on("Network.loadingFinished", (value) => {
|
|
17245
|
+
const event = this.network.get(value.requestId);
|
|
17246
|
+
if (event) {
|
|
17247
|
+
event.completedOffsetMs = this.offset();
|
|
17248
|
+
event.durationMs = Math.max(0, event.completedOffsetMs - event.offsetMs);
|
|
17249
|
+
}
|
|
17250
|
+
});
|
|
17251
|
+
this.on("Network.loadingFailed", (value) => {
|
|
17252
|
+
const event = this.network.get(value.requestId);
|
|
17253
|
+
if (event) event.failureReason = redactDiagnosticText(String(value.errorText ?? "Request failed"), 200);
|
|
17254
|
+
});
|
|
17255
|
+
}
|
|
17256
|
+
async finalize(reason) {
|
|
17257
|
+
const durationSeconds = Math.min(this.options.maxDurationSeconds ?? 600, Math.max(0.1, this.offset() / 1e3));
|
|
17258
|
+
const videoPath = join10(this.options.outputDirectory, "recording.webm");
|
|
17259
|
+
const diagnosticsPath = join10(this.options.outputDirectory, "diagnostics-v1.ndjson.gz");
|
|
17260
|
+
const network = [...this.network.values()];
|
|
17261
|
+
try {
|
|
17262
|
+
await this.screencast.stop(videoPath, durationSeconds, true);
|
|
17263
|
+
const bundle = {
|
|
17264
|
+
schemaVersion: 1,
|
|
17265
|
+
capturePolicyVersion: "safe-default-v1",
|
|
17266
|
+
recordingId: this.options.recordingId,
|
|
17267
|
+
startedAt: this.startedAt.toISOString(),
|
|
17268
|
+
durationMs: Math.round(durationSeconds * 1e3),
|
|
17269
|
+
videoStartOffsetMs: (this.screencast.videoStartedAtMs ?? this.startedAt.getTime()) - this.startedAt.getTime(),
|
|
17270
|
+
bodyCaptureEnabled: false,
|
|
17271
|
+
bodyCaptureRedacted: false,
|
|
17272
|
+
truncated: this.truncated || this.interrupted,
|
|
17273
|
+
console: this.console,
|
|
17274
|
+
network,
|
|
17275
|
+
timeline: this.timeline
|
|
17276
|
+
};
|
|
17277
|
+
await writeBundle(bundle, diagnosticsPath);
|
|
17278
|
+
} finally {
|
|
17279
|
+
this.detach();
|
|
17280
|
+
this.releaseCapture?.();
|
|
17281
|
+
}
|
|
17282
|
+
return {
|
|
17283
|
+
recordingId: this.options.recordingId,
|
|
17284
|
+
videoPath,
|
|
17285
|
+
frameDirectory: join10(this.options.outputDirectory, "frames"),
|
|
17286
|
+
diagnosticsPath,
|
|
17287
|
+
durationSeconds,
|
|
17288
|
+
stoppedReason: this.interrupted ? "browser_closed" : reason,
|
|
17289
|
+
diagnostics: { consoleErrors: this.console.filter((event) => event.severity === "error").length, failedRequests: network.filter((event) => event.failureReason).length, httpErrors: network.filter((event) => (event.status ?? 0) >= 400).length, truncated: this.truncated || this.interrupted, bodyCaptureEnabled: false }
|
|
17290
|
+
};
|
|
17291
|
+
}
|
|
17292
|
+
};
|
|
17293
|
+
|
|
16621
17294
|
// ../recorder/src/daemon.ts
|
|
16622
17295
|
var sessions = /* @__PURE__ */ new Map();
|
|
16623
17296
|
var lastActivity = Date.now();
|
|
17297
|
+
var browserConnection = new BrowserConnection({ statePath: join11(dirname2(daemonStatePath()), "chrome-connection.json"), extensionArchive: process.argv[1] ? join11(dirname2(process.argv[1]), "rill-chrome-extension.zip") : void 0 });
|
|
16624
17298
|
async function runDaemon() {
|
|
16625
17299
|
if (process.platform === "win32" && process.env.RILL_SOCKET) throw new Error("RILL_SOCKET overrides are not supported by authenticated Windows IPC.");
|
|
16626
|
-
const
|
|
16627
|
-
const socketPath =
|
|
16628
|
-
await
|
|
16629
|
-
await
|
|
17300
|
+
const identity2 = process.platform === "win32" ? await windowsIpcIdentity() : void 0;
|
|
17301
|
+
const socketPath = identity2?.pipe ?? daemonSocketPath();
|
|
17302
|
+
await mkdir9(identity2 ? dirname2(daemonStatePath()) : dirname2(socketPath), { recursive: true, mode: 448 });
|
|
17303
|
+
await mkdir9(recordingTempRoot(), { recursive: true, mode: 448 });
|
|
16630
17304
|
await restoreRecoverableSessions();
|
|
16631
17305
|
await purgeExpiredCaptures();
|
|
16632
|
-
if (
|
|
17306
|
+
if (await stat4(join11(dirname2(daemonStatePath()), "chrome-connection.json")).catch(() => null)) await browserConnection.connect();
|
|
17307
|
+
browserConnection.on("disconnected", () => {
|
|
17308
|
+
for (const entry of sessions.values()) if (entry.state === "recording" && entry.session instanceof ExtensionRecordingSession) void finalizeEntry(entry, "browser_closed");
|
|
17309
|
+
});
|
|
17310
|
+
if (!identity2) await unlink(socketPath).catch((error51) => {
|
|
16633
17311
|
if (error51.code !== "ENOENT") throw error51;
|
|
16634
17312
|
});
|
|
16635
|
-
const server =
|
|
16636
|
-
if (
|
|
17313
|
+
const server = createServer3((request3, response) => {
|
|
17314
|
+
if (identity2 && !acceptsWindowsIpcToken(identity2.token, request3.headers.authorization)) {
|
|
16637
17315
|
request3.resume();
|
|
16638
17316
|
json2(response, 401, { error: { code: "authentication_required", message: "Recorder authentication required." } });
|
|
16639
17317
|
return;
|
|
@@ -16643,7 +17321,7 @@ async function runDaemon() {
|
|
|
16643
17321
|
await new Promise((resolve, reject) => {
|
|
16644
17322
|
server.once("error", reject);
|
|
16645
17323
|
server.listen(socketPath, () => {
|
|
16646
|
-
if (
|
|
17324
|
+
if (identity2) resolve();
|
|
16647
17325
|
else void chmod(socketPath, 384).then(resolve, reject);
|
|
16648
17326
|
});
|
|
16649
17327
|
});
|
|
@@ -16652,14 +17330,16 @@ async function runDaemon() {
|
|
|
16652
17330
|
await purgeExpiredCaptures();
|
|
16653
17331
|
const hasActiveSession = [...sessions.values()].some((entry) => entry.state === "recording" || entry.state === "finalizing");
|
|
16654
17332
|
const hasRetainedCapture = await hasRetainedRemoteCaptures(recordingTempRoot());
|
|
16655
|
-
if (!hasActiveSession
|
|
17333
|
+
if (!hasActiveSession) browserConnection.expireIdle();
|
|
17334
|
+
if (!hasActiveSession && !hasRetainedCapture && !browserConnection.keepsRecorderAlive && Date.now() - lastActivity > 5 * 6e4) server.close(() => process.exit(0));
|
|
16656
17335
|
})().catch(() => void 0);
|
|
16657
17336
|
}, 3e4);
|
|
16658
17337
|
idleTimer.unref();
|
|
16659
17338
|
const close = () => {
|
|
16660
17339
|
clearInterval(idleTimer);
|
|
16661
17340
|
server.close();
|
|
16662
|
-
|
|
17341
|
+
void browserConnection.close();
|
|
17342
|
+
if (!identity2) void unlink(socketPath).catch(() => void 0);
|
|
16663
17343
|
};
|
|
16664
17344
|
process.once("SIGTERM", close);
|
|
16665
17345
|
process.once("SIGINT", close);
|
|
@@ -16667,14 +17347,27 @@ async function runDaemon() {
|
|
|
16667
17347
|
async function route(request3, response) {
|
|
16668
17348
|
lastActivity = Date.now();
|
|
16669
17349
|
const url2 = new URL(request3.url ?? "/", "http://daemon.local");
|
|
16670
|
-
if (request3.method === "GET" && url2.pathname === "/health") return json2(response, 200, { ok: true, pid: process.pid, capabilities: ["tab-selection-v1"] });
|
|
17350
|
+
if (request3.method === "GET" && url2.pathname === "/health") return json2(response, 200, { ok: true, pid: process.pid, capabilities: ["tab-selection-v1", "chrome-connection-v1"] });
|
|
17351
|
+
if (request3.method === "POST" && url2.pathname === "/browser/connect") {
|
|
17352
|
+
try {
|
|
17353
|
+
return json2(response, 200, await browserConnection.connect());
|
|
17354
|
+
} catch {
|
|
17355
|
+
return json2(response, 500, { error: { code: "browser_unavailable", message: "Could not connect Chrome. Try again." } });
|
|
17356
|
+
}
|
|
17357
|
+
}
|
|
17358
|
+
if (request3.method === "GET" && url2.pathname === "/browser/status") return json2(response, 200, browserConnection.status());
|
|
17359
|
+
if (request3.method === "POST" && url2.pathname === "/browser/disconnect") {
|
|
17360
|
+
browserConnection.disconnect();
|
|
17361
|
+
return json2(response, 200, { connected: false });
|
|
17362
|
+
}
|
|
16671
17363
|
if (request3.method === "POST" && url2.pathname === "/sessions") {
|
|
16672
17364
|
if ([...sessions.values()].filter((entry) => entry.state === "recording").length >= 2) return json2(response, 429, { error: { code: "quota_exceeded", message: "This recorder already has two active sessions." } });
|
|
16673
17365
|
try {
|
|
16674
17366
|
const input = await readJson(request3);
|
|
16675
17367
|
if (!/^rec_[a-zA-Z0-9_-]+$/.test(input.recordingId)) return json2(response, 400, { error: { code: "validation_failed", message: "recordingId is required." } });
|
|
16676
|
-
const outputDirectory =
|
|
16677
|
-
|
|
17368
|
+
const outputDirectory = join11(recordingTempRoot(), input.recordingId);
|
|
17369
|
+
if (input.browser === "existing" && [...sessions.values()].some((entry2) => entry2.state !== "ready" && entry2.state !== "failed" && entry2.session instanceof ExtensionRecordingSession)) throw new Error("This tab is already recording. Stop it before starting another take.");
|
|
17370
|
+
const session = input.browser === "existing" ? await ExtensionRecordingSession.start({ ...input, outputDirectory }, browserConnection) : await RecordingSession.start({ ...input, outputDirectory });
|
|
16678
17371
|
const status = session.status();
|
|
16679
17372
|
const entry = { session, base: { recordingId: status.recordingId, cdpUrl: status.cdpUrl, tabId: status.tabId, startedAt: status.startedAt }, state: "recording" };
|
|
16680
17373
|
sessions.set(input.recordingId, entry);
|
|
@@ -16724,7 +17417,7 @@ function serializeEntry(entry) {
|
|
|
16724
17417
|
}
|
|
16725
17418
|
async function restoreRecoverableSessions() {
|
|
16726
17419
|
try {
|
|
16727
|
-
const value = JSON.parse(await
|
|
17420
|
+
const value = JSON.parse(await readFile8(daemonStatePath(), "utf8"));
|
|
16728
17421
|
for (const [id, entry] of Object.entries(value)) {
|
|
16729
17422
|
if (entry.status !== "ready" && entry.status !== "failed") continue;
|
|
16730
17423
|
sessions.set(id, { base: { recordingId: entry.recordingId, cdpUrl: entry.cdpUrl, tabId: entry.tabId, startedAt: entry.startedAt }, state: entry.status, result: entry.result, error: entry.error });
|
|
@@ -16735,7 +17428,7 @@ async function restoreRecoverableSessions() {
|
|
|
16735
17428
|
async function persistState() {
|
|
16736
17429
|
const state = Object.fromEntries([...sessions.entries()].map(([id, entry]) => [id, serializeEntry(entry)]));
|
|
16737
17430
|
const temporary = daemonStatePath() + "." + crypto.randomUUID() + ".tmp";
|
|
16738
|
-
await
|
|
17431
|
+
await writeFile8(temporary, `${JSON.stringify(state, null, 2)}
|
|
16739
17432
|
`, { mode: 384 });
|
|
16740
17433
|
await rename2(temporary, daemonStatePath());
|
|
16741
17434
|
}
|
|
@@ -16803,7 +17496,7 @@ function emit(value, pretty = false) {
|
|
|
16803
17496
|
function failureValue(error51) {
|
|
16804
17497
|
const message = error51 instanceof Error ? error51.message : String(error51);
|
|
16805
17498
|
const [candidate] = message.split(":", 1);
|
|
16806
|
-
const knownCodes = /* @__PURE__ */ new Set(["authentication_required", "authorization_not_found", "expired_token", "access_denied", "unsupported_platform", "scope_denied", "quota_exceeded", "browser_unavailable", "local_capture_failed", "recording_not_found", "upload_interrupted", "processing_pending", "processing_failed", "share_revoked", "validation_failed"]);
|
|
17499
|
+
const knownCodes = /* @__PURE__ */ new Set(["authentication_required", "authorization_not_found", "expired_token", "access_denied", "unsupported_platform", "unsupported_node_version", "scope_denied", "quota_exceeded", "browser_unavailable", "local_capture_failed", "recording_not_found", "upload_interrupted", "processing_pending", "processing_failed", "share_revoked", "validation_failed"]);
|
|
16807
17500
|
const structured = error51 && typeof error51 === "object" ? error51 : {};
|
|
16808
17501
|
const code = structured.code && knownCodes.has(structured.code) ? structured.code : knownCodes.has(candidate) ? candidate : "internal_error";
|
|
16809
17502
|
return { error: { code, message, ...structured.recovery ? { recovery: structured.recovery } : {}, ...structured.details ? { details: structured.details } : {} } };
|
|
@@ -16817,17 +17510,17 @@ function fail(error51) {
|
|
|
16817
17510
|
function terminalColor(enabled = Boolean(process.stdout.isTTY) && !("NO_COLOR" in process.env) && process.env.TERM !== "dumb") {
|
|
16818
17511
|
return (text, code) => enabled ? `\x1B[${code}m${text}\x1B[0m` : text;
|
|
16819
17512
|
}
|
|
16820
|
-
function formatIdentity(
|
|
16821
|
-
if (
|
|
17513
|
+
function formatIdentity(identity2, color = terminalColor()) {
|
|
17514
|
+
if (identity2.status === "not_connected") return `${color("! Not connected to Rill", 33)}
|
|
16822
17515
|
|
|
16823
|
-
Sign in: ${cliCommand(
|
|
17516
|
+
Sign in: ${cliCommand(identity2.apiUrl, "login")}
|
|
16824
17517
|
`;
|
|
16825
|
-
if (
|
|
17518
|
+
if (identity2.status === "anonymous") return `${color("\u2713 Using Rill anonymously", 32)}
|
|
16826
17519
|
|
|
16827
|
-
Connect your account: ${cliCommand(
|
|
17520
|
+
Connect your account: ${cliCommand(identity2.apiUrl, "connect", "--email", "<your-email>")}
|
|
16828
17521
|
`;
|
|
16829
17522
|
return `${color("\u2713 Connected to Rill", 32)}
|
|
16830
|
-
Account: ${
|
|
17523
|
+
Account: ${identity2.email ?? "Email unavailable"}
|
|
16831
17524
|
`;
|
|
16832
17525
|
}
|
|
16833
17526
|
function formatDoctor(result, color = terminalColor()) {
|
|
@@ -16858,8 +17551,11 @@ function formatRecording(value, apiUrl, color = terminalColor()) {
|
|
|
16858
17551
|
const duration3 = value.durationSeconds ?? value.duration_seconds ?? artifact.durationSeconds;
|
|
16859
17552
|
if (typeof duration3 === "number" && Number.isFinite(duration3)) lines.push(`Duration: ${Math.floor(duration3 / 60)}m ${Math.floor(duration3 % 60)}s`);
|
|
16860
17553
|
if (typeof value.remainingSeconds === "number") lines.push(`Time remaining: ${Math.max(0, Math.ceil(value.remainingSeconds))}s`);
|
|
16861
|
-
if (
|
|
16862
|
-
|
|
17554
|
+
if (value.browser === "existing") lines.push("Recording your selected Chrome tab.");
|
|
17555
|
+
else {
|
|
17556
|
+
if (typeof value.cdpUrl === "string" && value.cdpUrl) lines.push(`Browser connection: ${value.cdpUrl}`);
|
|
17557
|
+
if (typeof value.tabId === "string") lines.push(`Recorded tab: ${value.tabId}`);
|
|
17558
|
+
}
|
|
16863
17559
|
if (typeof value.shareUrl === "string" && value.shareUrl) lines.push("", `Share: ${value.shareUrl}`);
|
|
16864
17560
|
else if (status === "ready") lines.push("", "Ready. No share link returned; open this recording in your Rill account.");
|
|
16865
17561
|
const message = value.message ?? value.error;
|
|
@@ -16937,6 +17633,31 @@ async function withRunFeedback(session, start) {
|
|
|
16937
17633
|
|
|
16938
17634
|
// src/doctor.ts
|
|
16939
17635
|
import { spawn as spawn7 } from "child_process";
|
|
17636
|
+
|
|
17637
|
+
// src/node-runtime.ts
|
|
17638
|
+
var SUPPORTED_NODE_RANGE = "^22.23.4 || ^24.22.1 || >=26.12.1";
|
|
17639
|
+
var NODE_UPGRADE_RECOVERY = "Install Node.js 24.22.1 LTS or a newer patched release (22.23.4+, 24.22.1+, or 26.12.1+), then rerun the command. Update Chrome/Chromium separately for local recording.";
|
|
17640
|
+
function supportsNodeRuntime(version3) {
|
|
17641
|
+
const match = /^v?(\d+)\.(\d+)\.(\d+)(?:\+[\w.-]+)?$/.exec(version3);
|
|
17642
|
+
if (!match) return false;
|
|
17643
|
+
const [major, minor, patch] = match.slice(1).map(Number);
|
|
17644
|
+
const minimum = major === 22 ? [23, 4] : major === 24 ? [22, 1] : major === 26 ? [12, 1] : null;
|
|
17645
|
+
if (!minimum) return major > 26;
|
|
17646
|
+
return minor > minimum[0] || minor === minimum[0] && patch >= minimum[1];
|
|
17647
|
+
}
|
|
17648
|
+
var UnsupportedNodeRuntimeError = class extends Error {
|
|
17649
|
+
code = "unsupported_node_version";
|
|
17650
|
+
recovery = NODE_UPGRADE_RECOVERY;
|
|
17651
|
+
constructor(version3) {
|
|
17652
|
+
super(`Node.js ${version3} does not meet Rill's patched runtime requirement for the eMudhra certificate compromise. Supported versions: ${SUPPORTED_NODE_RANGE}.`);
|
|
17653
|
+
this.name = "UnsupportedNodeRuntimeError";
|
|
17654
|
+
}
|
|
17655
|
+
};
|
|
17656
|
+
function assertNodeRuntime(version3 = process.version) {
|
|
17657
|
+
if (!supportsNodeRuntime(version3)) throw new UnsupportedNodeRuntimeError(version3);
|
|
17658
|
+
}
|
|
17659
|
+
|
|
17660
|
+
// src/doctor.ts
|
|
16940
17661
|
var defaultDependencies = {
|
|
16941
17662
|
platform: process.platform,
|
|
16942
17663
|
encoding: "remote",
|
|
@@ -16955,10 +17676,12 @@ async function runDoctor(apiUrl, overrides = {}) {
|
|
|
16955
17676
|
const checks = [];
|
|
16956
17677
|
let feedbackEnabled = false;
|
|
16957
17678
|
checks.push(recorderTransportCheck(dependencies.platform));
|
|
16958
|
-
if (
|
|
17679
|
+
if (supportsNodeRuntime(dependencies.nodeVersion)) {
|
|
16959
17680
|
checks.push({ name: "node", status: "pass", message: `Node.js ${dependencies.nodeVersion} is supported.`, version: dependencies.nodeVersion });
|
|
16960
17681
|
} else {
|
|
16961
|
-
|
|
17682
|
+
const error51 = new UnsupportedNodeRuntimeError(dependencies.nodeVersion);
|
|
17683
|
+
checks.push({ name: "node", status: "fail", message: error51.message, version: dependencies.nodeVersion, recovery: error51.recovery });
|
|
17684
|
+
return { schemaVersion: 1, status: "error", apiUrl, environment: { platform: dependencies.platform }, checks };
|
|
16962
17685
|
}
|
|
16963
17686
|
let health = null;
|
|
16964
17687
|
try {
|
|
@@ -17025,8 +17748,8 @@ async function runDoctor(apiUrl, overrides = {}) {
|
|
|
17025
17748
|
}
|
|
17026
17749
|
async function executableCheck(name, path, args, run2, recovery) {
|
|
17027
17750
|
try {
|
|
17028
|
-
const
|
|
17029
|
-
return { name, status: "pass", message: `${name === "chrome" ? "Chrome/Chromium" : "ffmpeg"} is available.`, version:
|
|
17751
|
+
const version3 = firstLine(await run2(path, args));
|
|
17752
|
+
return { name, status: "pass", message: `${name === "chrome" ? "Chrome/Chromium" : "ffmpeg"} is available.`, version: version3, path };
|
|
17030
17753
|
} catch (error51) {
|
|
17031
17754
|
return { name, status: "fail", message: `${name === "chrome" ? "Chrome/Chromium" : "ffmpeg"} is unavailable: ${errorMessage(error51)}`, path, recovery };
|
|
17032
17755
|
}
|
|
@@ -17055,8 +17778,8 @@ ${Buffer.concat(stderr).toString("utf8")}`.trim();
|
|
|
17055
17778
|
});
|
|
17056
17779
|
});
|
|
17057
17780
|
}
|
|
17058
|
-
function normalizeVersion(
|
|
17059
|
-
return
|
|
17781
|
+
function normalizeVersion(version3) {
|
|
17782
|
+
return version3.replace(/^v/, "").split("-")[0];
|
|
17060
17783
|
}
|
|
17061
17784
|
function compareVersions(left, right) {
|
|
17062
17785
|
const leftParts = normalizeVersion(left).split(".").map(Number);
|
|
@@ -17196,7 +17919,7 @@ function controlPlaneUrl(environmentUrl) {
|
|
|
17196
17919
|
async function removeLocalCapture(id) {
|
|
17197
17920
|
await ensureDaemon(process.argv[1]);
|
|
17198
17921
|
const local = await recordingStatus(id).catch(() => null);
|
|
17199
|
-
if (local?.result?.frameDirectory) await rm7(
|
|
17922
|
+
if (local?.result?.frameDirectory) await rm7(dirname3(local.result.videoPath), { recursive: true, force: true });
|
|
17200
17923
|
}
|
|
17201
17924
|
function watchEncoding(apiUrl, id) {
|
|
17202
17925
|
const child = spawn8(process.execPath, [process.argv[1], "--api-url", apiUrl, "encoding-watch", id], { detached: true, stdio: "ignore", env: { ...process.env }, windowsHide: true });
|
|
@@ -17206,9 +17929,35 @@ function watchEncoding(apiUrl, id) {
|
|
|
17206
17929
|
var program = new Command();
|
|
17207
17930
|
program.name("rill").description("Record, inspect, and share agent browser runs.").version(CLI_VERSION).option("--api-url <url>", "Rill control plane", controlPlaneUrl(process.env.RILL_API_URL)).option("--pretty", "Pretty-print JSON output", false).option("--json", "Output machine-readable JSON (including whoami)", false).option("--human", "Readable doctor, login, whoami and recording start/stop/status/retry output", false);
|
|
17208
17931
|
program.configureOutput({ outputError: () => void 0 }).exitOverride();
|
|
17209
|
-
|
|
17932
|
+
var browserCommands = program.command("browser").description("Connect the Chrome tab you already use.");
|
|
17933
|
+
browserCommands.command("connect").action(async () => {
|
|
17934
|
+
await ensureDaemon(process.argv[1], false, true);
|
|
17935
|
+
const result = await connectBrowser();
|
|
17936
|
+
if (program.opts().json || program.opts().pretty) emit(result, program.opts().pretty);
|
|
17937
|
+
else process.stdout.write(result.connected ? `Chrome is connected. You can start another recording.
|
|
17938
|
+
` : `Connect Chrome
|
|
17939
|
+
|
|
17940
|
+
Open this link in Chrome, then choose your app tab in Rill:
|
|
17941
|
+
${result.connectionUrl}
|
|
17942
|
+
`);
|
|
17943
|
+
});
|
|
17944
|
+
browserCommands.command("status").action(async () => {
|
|
17945
|
+
await ensureDaemon(process.argv[1], false, true);
|
|
17946
|
+
const result = await browserConnectionStatus();
|
|
17947
|
+
if (program.opts().json || program.opts().pretty) emit(result, program.opts().pretty);
|
|
17948
|
+
else process.stdout.write(result.connected ? `Chrome is connected.
|
|
17949
|
+
Tab: ${result.tab?.title}
|
|
17950
|
+
` : "Chrome is not connected. Run rill browser connect to get started.\n");
|
|
17951
|
+
});
|
|
17952
|
+
browserCommands.command("disconnect").action(async () => {
|
|
17953
|
+
await ensureDaemon(process.argv[1], false, true);
|
|
17954
|
+
const result = await disconnectBrowser();
|
|
17955
|
+
if (program.opts().json || program.opts().pretty) emit(result, program.opts().pretty);
|
|
17956
|
+
else process.stdout.write("Chrome disconnected. Your tab stays open.\n");
|
|
17957
|
+
});
|
|
17958
|
+
function humanOutput(identity2 = false) {
|
|
17210
17959
|
const options = program.opts();
|
|
17211
|
-
return !options.json && !options.pretty && (
|
|
17960
|
+
return !options.json && !options.pretty && (identity2 || options.human);
|
|
17212
17961
|
}
|
|
17213
17962
|
function emitRecording(value, pretty) {
|
|
17214
17963
|
if (humanOutput()) process.stdout.write(formatRecording(value, program.opts().apiUrl));
|
|
@@ -17251,10 +18000,10 @@ async function authorize(quiet = false) {
|
|
|
17251
18000
|
}
|
|
17252
18001
|
program.command("whoami").description("Show the current verified identity without starting sign-in.").addHelpText("after", "\nShows a short account summary by default. Use rill whoami --json for agents, or rill --pretty whoami for indented JSON.").action(async () => {
|
|
17253
18002
|
const options = program.opts();
|
|
17254
|
-
const
|
|
17255
|
-
if (humanOutput(true)) process.stdout.write(formatIdentity(
|
|
17256
|
-
else emit(
|
|
17257
|
-
if (!
|
|
18003
|
+
const identity2 = await whoami(options.apiUrl);
|
|
18004
|
+
if (humanOutput(true)) process.stdout.write(formatIdentity(identity2));
|
|
18005
|
+
else emit(identity2, options.pretty);
|
|
18006
|
+
if (!identity2.authenticated) process.exitCode = 1;
|
|
17258
18007
|
});
|
|
17259
18008
|
var connect = program.command("connect").description("Connect this agent once, with renewable account access.").option("--email <email>", "Email of the Rill account approving access").option("--no-wait", "Return approval instructions immediately; poll with connect status").action(async (input) => {
|
|
17260
18009
|
if (!input.email) throw new Error("validation_failed: Supply --email <email> to start a connection.");
|
|
@@ -17306,10 +18055,14 @@ record2.command("tabs").requiredOption("--cdp-url <url>", "Existing Chromium CDP
|
|
|
17306
18055
|
const options = program.opts();
|
|
17307
18056
|
emit({ schemaVersion: 1, tabs: await listBrowserTabs(commandOptions.cdpUrl) }, options.pretty);
|
|
17308
18057
|
});
|
|
17309
|
-
record2.command("start").option("--encoding <mode>", "remote or local", "remote").option("--url <url>").option("--title <title>").option("--description <description>", "").option("--headed").option("--cdp-url <url>").option("--tab-id <id>", "Record this existing tab from record tabs").option("--width <pixels>", "", "1280").option("--height <pixels>", "", "720").option("--max-duration <seconds>", "", "600").action(async (commandOptions) => {
|
|
18058
|
+
record2.command("start").option("--browser <choice>", "existing or new", "new").option("--operator <who>", "human or agent", "human").option("--controller-target <id>", "Target selected by your agent controller").option("--encoding <mode>", "remote or local", "remote").option("--url <url>").option("--title <title>").option("--description <description>", "").option("--headed").option("--cdp-url <url>").option("--tab-id <id>", "Record this existing tab from record tabs").option("--width <pixels>", "", "1280").option("--height <pixels>", "", "720").option("--max-duration <seconds>", "", "600").action(async (commandOptions) => {
|
|
17310
18059
|
const options = program.opts();
|
|
17311
18060
|
assertRecorderTransport();
|
|
17312
18061
|
validateTabSelection(commandOptions);
|
|
18062
|
+
const connectedBrowser = await prepareBrowserRecording(commandOptions, async () => {
|
|
18063
|
+
await ensureDaemon(process.argv[1], false, true);
|
|
18064
|
+
return browserConnectionStatus();
|
|
18065
|
+
});
|
|
17313
18066
|
await ensureAnonymousCredential(options.apiUrl);
|
|
17314
18067
|
const client = new ControlPlaneClient(options.apiUrl, () => loadCredential(options.apiUrl));
|
|
17315
18068
|
const result = await withRunFeedback(() => client.agentSession(), async (rememberRecording) => {
|
|
@@ -17318,7 +18071,7 @@ record2.command("start").option("--encoding <mode>", "remote or local", "remote"
|
|
|
17318
18071
|
if (!Number.isFinite(Number(commandOptions.maxDuration)) || Number(commandOptions.maxDuration) < 1 || Number(commandOptions.maxDuration) > 600) throw new Error("Recording duration must be between 1 and 600 seconds.");
|
|
17319
18072
|
const width = Number(commandOptions.width);
|
|
17320
18073
|
const height = Number(commandOptions.height);
|
|
17321
|
-
const metadata = { requestId: crypto.randomUUID(), title: commandOptions.title ?? `Browser recording \xB7 ${(/* @__PURE__ */ new Date()).toISOString()}`, description: commandOptions.description, sourceUrl: commandOptions.url ?? null, width, height };
|
|
18074
|
+
const metadata = { requestId: crypto.randomUUID(), title: commandOptions.title ?? `Browser recording \xB7 ${(/* @__PURE__ */ new Date()).toISOString()}`, description: commandOptions.description, sourceUrl: commandOptions.url ?? connectedBrowser?.tab?.url ?? null, width, height, ...connectedBrowser ? { recordingSource: "chrome_cli" } : {} };
|
|
17322
18075
|
const remote = await createRecordingWithRecovery({
|
|
17323
18076
|
createRemote: () => client.createRecording(metadata),
|
|
17324
18077
|
localStatus: recordingStatus,
|
|
@@ -17331,8 +18084,8 @@ record2.command("start").option("--encoding <mode>", "remote or local", "remote"
|
|
|
17331
18084
|
rememberRecording(remote.recordingId);
|
|
17332
18085
|
const local = await startRecordingWithCompensation(remote, {
|
|
17333
18086
|
startLocal: async () => {
|
|
17334
|
-
await ensureDaemon(process.argv[1], Boolean(commandOptions.cdpUrl));
|
|
17335
|
-
return startRecording({ encoding: commandOptions.encoding, recordingId: remote.recordingId, url: commandOptions.url, cdpUrl: commandOptions.cdpUrl, tabId: commandOptions.tabId, headed: Boolean(commandOptions.headed), width, height, maxDurationSeconds: Math.min(Number(commandOptions.maxDuration), remote.maxDurationSeconds ?? 600), captureNetworkBodies: remote.bodyCaptureEnabled, cursorAppearance: remote.cursorAppearance });
|
|
18087
|
+
await ensureDaemon(process.argv[1], Boolean(commandOptions.cdpUrl), Boolean(connectedBrowser));
|
|
18088
|
+
return startRecording({ ...connectedBrowser ? { browser: "existing", controllerTarget: commandOptions.controllerTarget } : {}, encoding: commandOptions.encoding, recordingId: remote.recordingId, url: commandOptions.url, cdpUrl: commandOptions.cdpUrl, tabId: commandOptions.tabId, headed: Boolean(commandOptions.headed), width, height, maxDurationSeconds: Math.min(Number(commandOptions.maxDuration), remote.maxDurationSeconds ?? 600), captureNetworkBodies: remote.bodyCaptureEnabled, cursorAppearance: remote.cursorAppearance });
|
|
17336
18089
|
},
|
|
17337
18090
|
cancelRemote: (recordingId, reason) => client.cancelRecording(recordingId, reason)
|
|
17338
18091
|
}, options.apiUrl);
|
|
@@ -17341,9 +18094,9 @@ record2.command("start").option("--encoding <mode>", "remote or local", "remote"
|
|
|
17341
18094
|
os: process.platform,
|
|
17342
18095
|
arch: process.arch,
|
|
17343
18096
|
headed: Boolean(commandOptions.headed),
|
|
17344
|
-
attached: Boolean(commandOptions.cdpUrl)
|
|
18097
|
+
attached: Boolean(commandOptions.cdpUrl || connectedBrowser)
|
|
17345
18098
|
}).catch(() => void 0);
|
|
17346
|
-
return { schemaVersion: 1, recordingId: remote.recordingId, status: local.status,
|
|
18099
|
+
return { schemaVersion: 1, recordingId: remote.recordingId, status: local.status, ...connectedBrowser ? { browser: "existing", browserAttachment: { protocol: "extension", tab: connectedBrowser.tab, captureOnly: true, controllerVerified: false, instructions: "Use your existing controller on this exact target, or perform the steps yourself. Rill records the selected tab and does not provide browser control. Popups and other tabs are outside this recording." } } : { cdpUrl: local.cdpUrl, browserAttachment: browserAttachment(local.cdpUrl, local.tabId) }, ...local.tabId ? { tabId: local.tabId } : {}, startedAt: local.startedAt, stopsAt: local.stopsAt, elapsedSeconds: local.elapsedSeconds, remainingSeconds: local.remainingSeconds, bodyCaptureEnabled: connectedBrowser ? false : remote.bodyCaptureEnabled, anonymous: remote.anonymous ?? false, expiresAt: remote.expiresAt ?? null, maxDurationSeconds: Math.min(Number(commandOptions.maxDuration), remote.maxDurationSeconds ?? 600) };
|
|
17347
18100
|
});
|
|
17348
18101
|
emitRecording(result, options.pretty);
|
|
17349
18102
|
});
|
|
@@ -17368,7 +18121,7 @@ record2.command("stop").argument("<recording-id>").option("--no-wait").option("-
|
|
|
17368
18121
|
const accepted = await submitFrameCapture(client, recordingId, local.result.frameDirectory);
|
|
17369
18122
|
const result2 = commandOptions.wait && record2.opts().wait !== false ? await waitForEncoding(client, recordingId) : accepted;
|
|
17370
18123
|
emitRecording({ schemaVersion: 1, ...result2, recordingId, ...recordingNextStep(options.apiUrl, recordingId, result2.status, { resumableFrames: true }), ...recordingFeedbackFields(recordingId, stopped?.feedbackRequested ?? false) }, options.pretty);
|
|
17371
|
-
if (result2.status === "ready") await rm7(
|
|
18124
|
+
if (result2.status === "ready") await rm7(dirname3(local.result.videoPath), { recursive: true, force: true });
|
|
17372
18125
|
else watchEncoding(options.apiUrl, recordingId);
|
|
17373
18126
|
return;
|
|
17374
18127
|
}
|
|
@@ -17390,7 +18143,7 @@ record2.command("stop").argument("<recording-id>").option("--no-wait").option("-
|
|
|
17390
18143
|
...recordingFeedbackFields(recordingId, stopped?.feedbackRequested ?? false)
|
|
17391
18144
|
};
|
|
17392
18145
|
emitRecording(result, options.pretty);
|
|
17393
|
-
if (remote) await rm7(
|
|
18146
|
+
if (remote) await rm7(dirname3(local.result.videoPath), { recursive: true, force: true });
|
|
17394
18147
|
});
|
|
17395
18148
|
record2.command("retry").argument("<recording-id>").option("--no-wait").action(async (recordingId, commandOptions) => {
|
|
17396
18149
|
const options = program.opts();
|
|
@@ -17422,7 +18175,7 @@ program.command("encoding-watch", { hidden: true }).argument("<recording-id>").a
|
|
|
17422
18175
|
if (result.status === "ready") {
|
|
17423
18176
|
await ensureDaemon(process.argv[1]);
|
|
17424
18177
|
const local = await recordingStatus(id).catch(() => null);
|
|
17425
|
-
if (local?.result?.frameDirectory) await rm7(
|
|
18178
|
+
if (local?.result?.frameDirectory) await rm7(dirname3(local.result.videoPath), { recursive: true, force: true });
|
|
17426
18179
|
}
|
|
17427
18180
|
});
|
|
17428
18181
|
var feedback = program.command("feedback").description("Submit a short Rill product review for a recorded run.");
|
|
@@ -17506,17 +18259,17 @@ comments.command("edit").argument("<share-url>").requiredOption("--comment <id>"
|
|
|
17506
18259
|
var drafts = program.command("drafts");
|
|
17507
18260
|
drafts.command("list").action(async () => {
|
|
17508
18261
|
const options = program.opts();
|
|
17509
|
-
const root = process.env.RILL_TEMP ??
|
|
18262
|
+
const root = process.env.RILL_TEMP ?? join12(tmpdir3(), "rill-recordings");
|
|
17510
18263
|
const names = await readdir2(root).catch(() => []);
|
|
17511
18264
|
emit({ schemaVersion: 1, drafts: names }, options.pretty);
|
|
17512
18265
|
});
|
|
17513
18266
|
drafts.command("purge").action(async () => {
|
|
17514
18267
|
const options = program.opts();
|
|
17515
|
-
const root = process.env.RILL_TEMP ??
|
|
18268
|
+
const root = process.env.RILL_TEMP ?? join12(tmpdir3(), "rill-recordings");
|
|
17516
18269
|
const names = await readdir2(root).catch(() => []);
|
|
17517
18270
|
let purged = 0;
|
|
17518
18271
|
for (const name of names) {
|
|
17519
|
-
const path =
|
|
18272
|
+
const path = join12(root, name);
|
|
17520
18273
|
const metadata = await stat5(path);
|
|
17521
18274
|
if (Date.now() - metadata.mtimeMs > 24 * 60 * 60 * 1e3) {
|
|
17522
18275
|
await rm7(path, { recursive: true, force: true });
|
|
@@ -17526,6 +18279,10 @@ drafts.command("purge").action(async () => {
|
|
|
17526
18279
|
emit({ schemaVersion: 1, purged }, options.pretty);
|
|
17527
18280
|
});
|
|
17528
18281
|
program.hook("preAction", async (_command, actionCommand) => {
|
|
18282
|
+
if (!supportsNodeRuntime(process.version)) {
|
|
18283
|
+
if (commandPath(actionCommand).join(" ") === "doctor") return;
|
|
18284
|
+
assertNodeRuntime();
|
|
18285
|
+
}
|
|
17529
18286
|
if (commandPath(actionCommand).join(" ") === "record start") assertRecorderTransport();
|
|
17530
18287
|
const command = cliCommandCategory(commandPath(actionCommand));
|
|
17531
18288
|
if (!command) return;
|