@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/dist/cli.js CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  var __defProp = Object.defineProperty;
3
- var __export = (target, all) => {
3
+ var __export = (target2, all) => {
4
4
  for (var name in all)
5
- __defProp(target, name, { get: all[name], enumerable: true });
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(target, prop, value) {
1394
- Object.defineProperty(target, prop, {
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 target;
1577
+ let target2;
1578
1578
  return new Proxy({}, {
1579
1579
  get(_, prop, receiver) {
1580
- target ?? (target = getter());
1581
- return Reflect.get(target, prop, receiver);
1580
+ target2 ?? (target2 = getter());
1581
+ return Reflect.get(target2, prop, receiver);
1582
1582
  },
1583
1583
  set(_, prop, value, receiver) {
1584
- target ?? (target = getter());
1585
- return Reflect.set(target, prop, value, receiver);
1584
+ target2 ?? (target2 = getter());
1585
+ return Reflect.set(target2, prop, value, receiver);
1586
1586
  },
1587
1587
  has(_, prop) {
1588
- target ?? (target = getter());
1589
- return Reflect.has(target, prop);
1588
+ target2 ?? (target2 = getter());
1589
+ return Reflect.has(target2, prop);
1590
1590
  },
1591
1591
  deleteProperty(_, prop) {
1592
- target ?? (target = getter());
1593
- return Reflect.deleteProperty(target, prop);
1592
+ target2 ?? (target2 = getter());
1593
+ return Reflect.deleteProperty(target2, prop);
1594
1594
  },
1595
1595
  ownKeys(_) {
1596
- target ?? (target = getter());
1597
- return Reflect.ownKeys(target);
1596
+ target2 ?? (target2 = getter());
1597
+ return Reflect.ownKeys(target2);
1598
1598
  },
1599
1599
  getOwnPropertyDescriptor(_, prop) {
1600
- target ?? (target = getter());
1601
- return Reflect.getOwnPropertyDescriptor(target, prop);
1600
+ target2 ?? (target2 = getter());
1601
+ return Reflect.getOwnPropertyDescriptor(target2, prop);
1602
1602
  },
1603
1603
  defineProperty(_, prop, descriptor) {
1604
- target ?? (target = getter());
1605
- return Reflect.defineProperty(target, prop, descriptor);
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 = (version2) => {
2241
- if (!version2)
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}-${version2}[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$`);
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, element, params) {
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 target = params?.target ?? "draft-2020-12";
12058
- if (target === "draft-4")
12059
- target = "draft-04";
12060
- if (target === "draft-7")
12061
- target = "draft-07";
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(target) {
13468
- return pipe(this, target);
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(element, params) {
13990
- return _array(ZodArray, element, params);
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 element = convertSchema(items, ctx);
15028
- let arraySchema = z.array(element);
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 version2 = detectVersion(normalized, params?.defaultTarget);
15119
+ const version3 = detectVersion(normalized, params?.defaultTarget);
15120
15120
  const defs = normalized.$defs || normalized.definitions || {};
15121
15121
  const ctx = {
15122
- version: version2,
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(600),
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(36e3)
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 dirname2, join as join11 } from "path";
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 identity = { version: 1, pipe: "\\\\.\\pipe\\rill-v3-" + randomBytes2(32).toString("hex"), token: randomBytes2(32).toString("hex") };
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(identity), { mode: 384, flag: "wx" });
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 context of browser.contexts()) for (const page of context.pages()) {
15438
+ for (const context2 of browser.contexts()) for (const page of context2.pages()) {
15432
15439
  if (page.isClosed()) continue;
15433
- const session = await context.newCDPSession(page).catch(() => null);
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 identity = process.platform === "win32" ? await (windowsIdentityPromise ??= windowsIpcIdentity().catch((error51) => {
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: identity?.pipe ?? daemonSocketPath(), method, path, headers: { ...payload ? { "Content-Type": "application/json", "Content-Length": payload.length } : {}, ...identity ? { Authorization: "Bearer " + identity.token } : {} } }, (incoming) => {
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 createServer2 } from "http";
15578
- import { mkdir as mkdir7, readFile as readFile7, unlink, writeFile as writeFile6, rename as rename2, stat as stat4, chmod } from "fs/promises";
15579
- import { dirname, join as join10 } from "path";
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 target = window;
15611
- const listener = target[key2 + "_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 target[key2 + "_listener"];
15614
- delete target[key2];
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 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() }).strict();
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 target = event.target;
15998
- if (target !== document && !(target instanceof Element)) return;
15999
- const position = target === document ? { x: window.scrollX, y: window.scrollY } : { x: target.scrollLeft, y: target.scrollTop };
16000
- const previous = positions.get(target);
16001
- positions.set(target, position);
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 = (element, frames, duration3, easing) => {
16033
- root.insertBefore(element, pointer);
16034
- const animation = element.animate(frames, { duration: duration3, easing });
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
- element.remove();
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
- for (let i = 0; i < manifest.frames.length; i++) {
16198
- const next = manifest.frames[i + 1];
16199
- const duration3 = next ? next.timestamp - manifest.frames[i].timestamp : manifest.durationSeconds - (manifest.frames[i].timestamp - manifest.frames[0].timestamp);
16200
- lines.push(`file 'frame-${String(i).padStart(6, "0")}.jpg'`, `duration ${Math.max(1 / 60, duration3).toFixed(6)}`);
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 dimensions = `scale=${manifest.width}:${manifest.height}:force_original_aspect_ratio=decrease,pad=${manifest.width}:${manifest.height}:(ow-iw)/2:(oh-ih)/2,setsar=1`;
16208
- const filters = output.endsWith(".mp4") ? `${dimensions},fps=30,tpad=stop_mode=clone:stop_duration=1` : dimensions;
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.session.on("Page.screencastFrame", (payload) => {
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: payload.metadata.timestamp ?? Date.now() / 1e3 });
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
- await this.session.send("Page.startScreencast", { format: "jpeg", quality: 82, maxWidth: this.width, maxHeight: this.height, everyNthFrame: 1 });
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 context = options.blockServiceWorkers ? await browser.newContext({ serviceWorkers: "block" }) : browser.contexts()[0];
16577
- const page = context.pages()[0] ?? await context.newPage();
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 identity = process.platform === "win32" ? await windowsIpcIdentity() : void 0;
16627
- const socketPath = identity?.pipe ?? daemonSocketPath();
16628
- await mkdir7(identity ? dirname(daemonStatePath()) : dirname(socketPath), { recursive: true, mode: 448 });
16629
- await mkdir7(recordingTempRoot(), { recursive: true, mode: 448 });
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 (!identity) await unlink(socketPath).catch((error51) => {
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 = createServer2((request3, response) => {
16636
- if (identity && !acceptsWindowsIpcToken(identity.token, request3.headers.authorization)) {
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 (identity) resolve();
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 && !hasRetainedCapture && Date.now() - lastActivity > 5 * 6e4) server.close(() => process.exit(0));
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
- if (!identity) void unlink(socketPath).catch(() => void 0);
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 = join10(recordingTempRoot(), input.recordingId);
16677
- const session = await RecordingSession.start({ ...input, outputDirectory });
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 readFile7(daemonStatePath(), "utf8"));
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 writeFile6(temporary, `${JSON.stringify(state, null, 2)}
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(identity, color = terminalColor()) {
16821
- if (identity.status === "not_connected") return `${color("! Not connected to Rill", 33)}
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(identity.apiUrl, "login")}
17516
+ Sign in: ${cliCommand(identity2.apiUrl, "login")}
16824
17517
  `;
16825
- if (identity.status === "anonymous") return `${color("\u2713 Using Rill anonymously", 32)}
17518
+ if (identity2.status === "anonymous") return `${color("\u2713 Using Rill anonymously", 32)}
16826
17519
 
16827
- Connect your account: ${cliCommand(identity.apiUrl, "connect", "--email", "<your-email>")}
17520
+ Connect your account: ${cliCommand(identity2.apiUrl, "connect", "--email", "<your-email>")}
16828
17521
  `;
16829
17522
  return `${color("\u2713 Connected to Rill", 32)}
16830
- Account: ${identity.email ?? "Email unavailable"}
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 (typeof value.cdpUrl === "string") lines.push(`Browser connection: ${value.cdpUrl}`);
16862
- if (typeof value.tabId === "string") lines.push(`Recorded tab: ${value.tabId}`);
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 (compareVersions(normalizeVersion(dependencies.nodeVersion), "22.0.0") >= 0) {
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
- checks.push({ name: "node", status: "fail", message: `Node.js ${dependencies.nodeVersion} is unsupported.`, version: dependencies.nodeVersion, recovery: "Install Node.js 22 or newer, then reinstall the Rill CLI." });
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 version2 = firstLine(await run2(path, args));
17029
- return { name, status: "pass", message: `${name === "chrome" ? "Chrome/Chromium" : "ffmpeg"} is available.`, version: version2, path };
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(version2) {
17059
- return version2.replace(/^v/, "").split("-")[0];
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(dirname2(local.result.videoPath), { recursive: true, force: true });
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
- function humanOutput(identity = false) {
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 && (identity || options.human);
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 identity = await whoami(options.apiUrl);
17255
- if (humanOutput(true)) process.stdout.write(formatIdentity(identity));
17256
- else emit(identity, options.pretty);
17257
- if (!identity.authenticated) process.exitCode = 1;
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, cdpUrl: local.cdpUrl, ...local.tabId ? { tabId: local.tabId } : {}, browserAttachment: browserAttachment(local.cdpUrl, local.tabId), startedAt: local.startedAt, stopsAt: local.stopsAt, elapsedSeconds: local.elapsedSeconds, remainingSeconds: local.remainingSeconds, bodyCaptureEnabled: remote.bodyCaptureEnabled, anonymous: remote.anonymous ?? false, expiresAt: remote.expiresAt ?? null, maxDurationSeconds: Math.min(Number(commandOptions.maxDuration), remote.maxDurationSeconds ?? 600) };
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(dirname2(local.result.videoPath), { recursive: true, force: true });
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(dirname2(local.result.videoPath), { recursive: true, force: true });
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(dirname2(local.result.videoPath), { recursive: true, force: true });
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 ?? join11(tmpdir3(), "rill-recordings");
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 ?? join11(tmpdir3(), "rill-recordings");
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 = join11(root, name);
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;