@hasna/browser 0.1.0 → 0.2.0

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/index.js CHANGED
@@ -2358,6 +2358,54 @@ function runMigrations(db) {
2358
2358
  CREATE INDEX IF NOT EXISTS idx_auth_flows_domain ON auth_flows(domain);
2359
2359
  CREATE INDEX IF NOT EXISTS idx_auth_flows_name ON auth_flows(name);
2360
2360
  `
2361
+ },
2362
+ {
2363
+ version: 7,
2364
+ sql: `
2365
+ CREATE TABLE IF NOT EXISTS workflows (
2366
+ id TEXT PRIMARY KEY,
2367
+ name TEXT NOT NULL UNIQUE,
2368
+ description TEXT,
2369
+ steps TEXT NOT NULL DEFAULT '[]',
2370
+ start_url TEXT,
2371
+ last_run TEXT,
2372
+ last_heal TEXT,
2373
+ heal_count INTEGER DEFAULT 0,
2374
+ run_count INTEGER DEFAULT 0,
2375
+ created_at TEXT DEFAULT (datetime('now')),
2376
+ updated_at TEXT DEFAULT (datetime('now'))
2377
+ );
2378
+ `
2379
+ },
2380
+ {
2381
+ version: 8,
2382
+ sql: `
2383
+ CREATE TABLE IF NOT EXISTS datasets (
2384
+ id TEXT PRIMARY KEY,
2385
+ name TEXT NOT NULL UNIQUE,
2386
+ source_url TEXT,
2387
+ source_type TEXT NOT NULL DEFAULT 'page',
2388
+ data TEXT NOT NULL DEFAULT '[]',
2389
+ schema TEXT,
2390
+ row_count INTEGER DEFAULT 0,
2391
+ last_refresh TEXT,
2392
+ created_at TEXT DEFAULT (datetime('now')),
2393
+ updated_at TEXT DEFAULT (datetime('now'))
2394
+ );
2395
+
2396
+ CREATE TABLE IF NOT EXISTS api_endpoints (
2397
+ id TEXT PRIMARY KEY,
2398
+ session_id TEXT,
2399
+ url TEXT NOT NULL,
2400
+ method TEXT DEFAULT 'GET',
2401
+ response_schema TEXT,
2402
+ sample_response TEXT,
2403
+ status_code INTEGER,
2404
+ content_type TEXT,
2405
+ discovered_at TEXT DEFAULT (datetime('now'))
2406
+ );
2407
+ CREATE INDEX IF NOT EXISTS idx_api_endpoints_session ON api_endpoints(session_id);
2408
+ `
2361
2409
  }
2362
2410
  ];
2363
2411
  for (const m of migrations) {
@@ -3322,14 +3370,14 @@ function enableConsoleCapture(page, sessionId) {
3322
3370
  warning: "warn"
3323
3371
  };
3324
3372
  const level = levelMap[msg.type()] ?? "log";
3325
- const location = msg.location();
3373
+ const location2 = msg.location();
3326
3374
  try {
3327
3375
  logConsoleMessage({
3328
3376
  session_id: sessionId,
3329
3377
  level,
3330
3378
  message: msg.text(),
3331
- source: location.url || undefined,
3332
- line_number: location.lineNumber || undefined
3379
+ source: location2.url || undefined,
3380
+ line_number: location2.lineNumber || undefined
3333
3381
  });
3334
3382
  } catch {}
3335
3383
  };
@@ -12116,6 +12164,743 @@ var init_recorder = __esm(() => {
12116
12164
  activeRecordings = new Map;
12117
12165
  });
12118
12166
 
12167
+ // src/lib/env-detector.ts
12168
+ var exports_env_detector = {};
12169
+ __export(exports_env_detector, {
12170
+ detectEnvironment: () => detectEnvironment
12171
+ });
12172
+ async function detectEnvironment(page) {
12173
+ const url = page.url();
12174
+ const signals = [];
12175
+ let score = { local: 0, dev: 0, staging: 0, prod: 0 };
12176
+ try {
12177
+ const u = new URL(url);
12178
+ if (u.hostname === "localhost" || u.hostname === "127.0.0.1" || u.hostname === "0.0.0.0" || u.hostname.endsWith(".local")) {
12179
+ score.local += 5;
12180
+ signals.push(`URL hostname: ${u.hostname} \u2192 local`);
12181
+ } else if (u.hostname.match(/^(dev|development)\./i) || u.port !== "") {
12182
+ score.dev += 4;
12183
+ signals.push(`URL pattern: ${u.hostname}:${u.port} \u2192 dev`);
12184
+ } else if (u.hostname.match(/^(staging|stg|stage|preprod|uat)\./i)) {
12185
+ score.staging += 4;
12186
+ signals.push(`URL pattern: ${u.hostname} \u2192 staging`);
12187
+ } else {
12188
+ score.prod += 2;
12189
+ signals.push(`URL looks production: ${u.hostname}`);
12190
+ }
12191
+ if (u.port && !["80", "443", ""].includes(u.port)) {
12192
+ score.dev += 2;
12193
+ signals.push(`Non-standard port: ${u.port}`);
12194
+ }
12195
+ if (u.protocol === "https:") {
12196
+ score.prod += 1;
12197
+ signals.push("HTTPS \u2192 likely prod");
12198
+ } else {
12199
+ score.dev += 2;
12200
+ signals.push("HTTP \u2192 likely dev/local");
12201
+ }
12202
+ } catch {}
12203
+ try {
12204
+ const pageSignals = await page.evaluate(() => {
12205
+ const s = [];
12206
+ const w = window;
12207
+ const envVars = ["__ENV__", "__NEXT_DATA__", "__NUXT__", "process"];
12208
+ for (const v of envVars) {
12209
+ if (w[v]) {
12210
+ const env2 = w[v]?.env?.NODE_ENV ?? w[v]?.runtimeConfig?.public?.env ?? w[v]?.props?.pageProps?.env;
12211
+ if (env2)
12212
+ s.push(`window.${v}: ${env2}`);
12213
+ }
12214
+ }
12215
+ if (w.__REACT_DEVTOOLS_GLOBAL_HOOK__?.renderers?.size > 0) {
12216
+ const fiber = document.querySelector("[data-reactroot]") || document.getElementById("__next") || document.getElementById("root");
12217
+ if (fiber)
12218
+ s.push("React app detected");
12219
+ }
12220
+ const envMeta = document.querySelector('meta[name="environment"], meta[name="env"], meta[name="deploy-env"]');
12221
+ if (envMeta)
12222
+ s.push(`meta[environment]: ${envMeta.getAttribute("content")}`);
12223
+ const scripts = document.querySelectorAll("script[src]");
12224
+ let minified = 0, unminified = 0;
12225
+ scripts.forEach((s2) => {
12226
+ const src = s2.getAttribute("src") ?? "";
12227
+ if (src.includes(".min.") || src.match(/\.[a-f0-9]{8,}\./))
12228
+ minified++;
12229
+ else if (src.endsWith(".js") && !src.includes("chunk"))
12230
+ unminified++;
12231
+ });
12232
+ if (unminified > minified && unminified > 2)
12233
+ s.push(`Unminified scripts (${unminified}/${minified + unminified}) \u2192 likely dev`);
12234
+ else if (minified > 0)
12235
+ s.push(`Minified/hashed scripts (${minified}/${minified + unminified}) \u2192 likely prod`);
12236
+ if (document.querySelector("[data-testid]"))
12237
+ s.push("data-testid attributes present \u2192 dev/staging");
12238
+ if ("serviceWorker" in navigator && navigator.serviceWorker.controller) {
12239
+ s.push("Service worker active \u2192 likely prod");
12240
+ }
12241
+ if (w.Sentry)
12242
+ s.push("Sentry SDK loaded \u2192 prod monitoring");
12243
+ if (w.__DATADOG_SYNTHETICS_INLINED_SCRIPT)
12244
+ s.push("Datadog loaded \u2192 prod monitoring");
12245
+ if (w.LogRocket)
12246
+ s.push("LogRocket loaded \u2192 prod monitoring");
12247
+ if (w._lr_loaded)
12248
+ s.push("LogRocket loaded \u2192 prod monitoring");
12249
+ if (w.gtag || w.ga)
12250
+ s.push("Google Analytics loaded \u2192 likely prod");
12251
+ if (w.posthog || w._ph)
12252
+ s.push("PostHog loaded \u2192 prod analytics");
12253
+ if (w.mixpanel)
12254
+ s.push("Mixpanel loaded \u2192 prod analytics");
12255
+ const robots = document.querySelector('meta[name="robots"]');
12256
+ if (robots) {
12257
+ const content = robots.getAttribute("content") ?? "";
12258
+ if (content.includes("noindex"))
12259
+ s.push(`robots: noindex \u2192 staging/dev`);
12260
+ }
12261
+ return s;
12262
+ });
12263
+ for (const signal of pageSignals) {
12264
+ signals.push(signal);
12265
+ if (signal.includes("development") || signal.includes("\u2192 dev") || signal.includes("\u2192 likely dev"))
12266
+ score.dev += 2;
12267
+ if (signal.includes("production") || signal.includes("\u2192 prod") || signal.includes("\u2192 likely prod"))
12268
+ score.prod += 2;
12269
+ if (signal.includes("staging") || signal.includes("\u2192 staging"))
12270
+ score.staging += 2;
12271
+ if (signal.includes("monitoring") || signal.includes("analytics"))
12272
+ score.prod += 1;
12273
+ if (signal.includes("noindex")) {
12274
+ score.staging += 2;
12275
+ score.dev += 1;
12276
+ }
12277
+ }
12278
+ } catch {}
12279
+ const entries = Object.entries(score);
12280
+ entries.sort((a, b) => b[1] - a[1]);
12281
+ const [env, topScore] = entries[0];
12282
+ const [, secondScore] = entries[1];
12283
+ const confidence = topScore >= 5 ? "high" : topScore > secondScore + 1 ? "medium" : "low";
12284
+ return { env, confidence, signals };
12285
+ }
12286
+
12287
+ // src/lib/deep-performance.ts
12288
+ var exports_deep_performance = {};
12289
+ __export(exports_deep_performance, {
12290
+ getDeepPerformance: () => getDeepPerformance
12291
+ });
12292
+ function categorizeThirdParty(domain) {
12293
+ for (const [pattern, category] of Object.entries(THIRD_PARTY_CATEGORIES)) {
12294
+ if (domain.includes(pattern))
12295
+ return category;
12296
+ }
12297
+ return "other";
12298
+ }
12299
+ async function getDeepPerformance(page) {
12300
+ return page.evaluate(() => {
12301
+ const perf = performance;
12302
+ const entries = perf.getEntriesByType("resource");
12303
+ const navEntry = perf.getEntriesByType("navigation")[0];
12304
+ const paintEntries = perf.getEntriesByType("paint");
12305
+ const fcp = paintEntries.find((e) => e.name === "first-contentful-paint")?.startTime;
12306
+ const ttfb = navEntry?.responseStart;
12307
+ const web_vitals = { fcp, ttfb };
12308
+ try {
12309
+ const lcpEntries = perf.getEntriesByType("largest-contentful-paint");
12310
+ if (lcpEntries.length > 0)
12311
+ web_vitals.lcp = lcpEntries[lcpEntries.length - 1].startTime;
12312
+ } catch {}
12313
+ const byType = {};
12314
+ let totalBytes = 0;
12315
+ const resourceList = [];
12316
+ const pageDomain = location.hostname;
12317
+ const thirdPartyMap = new Map;
12318
+ for (const entry of entries) {
12319
+ const size = entry.transferSize || entry.encodedBodySize || 0;
12320
+ totalBytes += size;
12321
+ let type2 = entry.initiatorType || "other";
12322
+ if (type2 === "xmlhttprequest" || type2 === "fetch")
12323
+ type2 = "xhr";
12324
+ if (type2 === "link" && entry.name.match(/\.css/))
12325
+ type2 = "css";
12326
+ if (type2 === "img" || entry.name.match(/\.(png|jpg|jpeg|gif|svg|webp|avif|ico)/i))
12327
+ type2 = "image";
12328
+ if (type2 === "script" || entry.name.match(/\.js/))
12329
+ type2 = "script";
12330
+ if (entry.name.match(/\.(woff2?|ttf|otf|eot)/i))
12331
+ type2 = "font";
12332
+ if (!byType[type2])
12333
+ byType[type2] = { count: 0, size_bytes: 0 };
12334
+ byType[type2].count++;
12335
+ byType[type2].size_bytes += size;
12336
+ resourceList.push({ url: entry.name, size_bytes: size, type: type2 });
12337
+ try {
12338
+ const domain = new URL(entry.name).hostname;
12339
+ if (domain !== pageDomain && !domain.endsWith(`.${pageDomain}`)) {
12340
+ if (!thirdPartyMap.has(domain))
12341
+ thirdPartyMap.set(domain, { scripts: 0, total_bytes: 0 });
12342
+ const tp = thirdPartyMap.get(domain);
12343
+ tp.scripts++;
12344
+ tp.total_bytes += size;
12345
+ }
12346
+ } catch {}
12347
+ }
12348
+ resourceList.sort((a, b) => b.size_bytes - a.size_bytes);
12349
+ const largest = resourceList.slice(0, 10).map((r) => ({
12350
+ url: r.url.length > 120 ? r.url.slice(0, 117) + "..." : r.url,
12351
+ size_bytes: r.size_bytes,
12352
+ type: r.type
12353
+ }));
12354
+ const third_party = Array.from(thirdPartyMap.entries()).map(([domain, data]) => ({ domain, ...data, category: "" })).sort((a, b) => b.total_bytes - a.total_bytes).slice(0, 15);
12355
+ const allNodes = document.querySelectorAll("*");
12356
+ let maxDepth = 0;
12357
+ let textNodes = 0;
12358
+ const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_ALL);
12359
+ let node = walker.currentNode;
12360
+ while (node) {
12361
+ if (node.nodeType === Node.TEXT_NODE && node.textContent?.trim())
12362
+ textNodes++;
12363
+ let depth = 0;
12364
+ let parent = node.parentNode;
12365
+ while (parent) {
12366
+ depth++;
12367
+ parent = parent.parentNode;
12368
+ }
12369
+ if (depth > maxDepth)
12370
+ maxDepth = depth;
12371
+ node = walker.nextNode();
12372
+ }
12373
+ const mem = performance.memory;
12374
+ const memory = {
12375
+ js_heap_used_mb: mem ? Math.round(mem.usedJSHeapSize / 1024 / 1024 * 100) / 100 : 0,
12376
+ js_heap_total_mb: mem ? Math.round(mem.totalJSHeapSize / 1024 / 1024 * 100) / 100 : 0,
12377
+ js_heap_limit_mb: mem ? Math.round(mem.jsHeapSizeLimit / 1024 / 1024 * 100) / 100 : 0
12378
+ };
12379
+ return {
12380
+ web_vitals,
12381
+ resources: { total_transfer_bytes: totalBytes, total_resources: entries.length, by_type: byType, largest },
12382
+ third_party,
12383
+ dom: { node_count: document.all.length, max_depth: maxDepth, element_count: allNodes.length, text_node_count: textNodes },
12384
+ main_thread: { long_tasks: 0, total_blocking_ms: 0 },
12385
+ memory
12386
+ };
12387
+ }).then((result) => {
12388
+ for (const tp of result.third_party) {
12389
+ tp.category = categorizeThirdParty(tp.domain);
12390
+ }
12391
+ return result;
12392
+ });
12393
+ }
12394
+ var THIRD_PARTY_CATEGORIES;
12395
+ var init_deep_performance = __esm(() => {
12396
+ THIRD_PARTY_CATEGORIES = {
12397
+ "google-analytics.com": "analytics",
12398
+ "googletagmanager.com": "analytics",
12399
+ gtag: "analytics",
12400
+ "facebook.net": "social",
12401
+ "connect.facebook": "social",
12402
+ "stripe.com": "payment",
12403
+ "js.stripe.com": "payment",
12404
+ "sentry.io": "monitoring",
12405
+ "sentry-cdn": "monitoring",
12406
+ "posthog.com": "analytics",
12407
+ "ph.": "analytics",
12408
+ "intercom.io": "chat",
12409
+ "crisp.chat": "chat",
12410
+ "hotjar.com": "analytics",
12411
+ "clarity.ms": "analytics",
12412
+ "cdn.jsdelivr.net": "cdn",
12413
+ "cdnjs.cloudflare.com": "cdn",
12414
+ "unpkg.com": "cdn",
12415
+ "fonts.googleapis.com": "fonts",
12416
+ "fonts.gstatic.com": "fonts"
12417
+ };
12418
+ });
12419
+
12420
+ // src/lib/api-detector.ts
12421
+ var exports_api_detector = {};
12422
+ __export(exports_api_detector, {
12423
+ listDiscoveredAPIs: () => listDiscoveredAPIs,
12424
+ detectAPIs: () => detectAPIs
12425
+ });
12426
+ import { randomUUID as randomUUID9 } from "crypto";
12427
+ function detectAPIs(sessionId) {
12428
+ const db = getDatabase();
12429
+ const requests = db.query(`SELECT method, url, status_code, response_headers, body_size
12430
+ FROM network_log
12431
+ WHERE session_id = ?
12432
+ AND (response_headers LIKE '%application/json%' OR response_headers LIKE '%text/json%')
12433
+ AND status_code >= 200 AND status_code < 400
12434
+ ORDER BY timestamp DESC`).all(sessionId);
12435
+ const seen = new Map;
12436
+ for (const req of requests) {
12437
+ try {
12438
+ const urlObj = new URL(req.url);
12439
+ if (urlObj.pathname.match(/\.(js|css|png|jpg|svg|woff|ico)$/))
12440
+ continue;
12441
+ if (urlObj.hostname.includes("googleapis.com/identitytoolkit"))
12442
+ continue;
12443
+ if (urlObj.hostname.includes("posthog"))
12444
+ continue;
12445
+ if (urlObj.hostname.includes("sentry"))
12446
+ continue;
12447
+ const key = `${req.method} ${urlObj.origin}${urlObj.pathname}`;
12448
+ if (!seen.has(key)) {
12449
+ seen.set(key, {
12450
+ url: `${urlObj.origin}${urlObj.pathname}`,
12451
+ method: req.method,
12452
+ status_code: req.status_code,
12453
+ content_type: "application/json",
12454
+ response_schema: {},
12455
+ sample_size: req.body_size ?? 0
12456
+ });
12457
+ }
12458
+ } catch {}
12459
+ }
12460
+ const apis = Array.from(seen.values());
12461
+ for (const api of apis) {
12462
+ const id = randomUUID9();
12463
+ db.prepare("INSERT OR IGNORE INTO api_endpoints (id, session_id, url, method, status_code, content_type) VALUES (?, ?, ?, ?, ?, ?)").run(id, sessionId, api.url, api.method, api.status_code, api.content_type);
12464
+ }
12465
+ return apis;
12466
+ }
12467
+ function listDiscoveredAPIs(sessionId) {
12468
+ const db = getDatabase();
12469
+ if (sessionId) {
12470
+ return db.query("SELECT * FROM api_endpoints WHERE session_id = ? ORDER BY discovered_at DESC").all(sessionId);
12471
+ }
12472
+ return db.query("SELECT * FROM api_endpoints ORDER BY discovered_at DESC LIMIT 100").all();
12473
+ }
12474
+ var init_api_detector = __esm(() => {
12475
+ init_schema();
12476
+ });
12477
+
12478
+ // src/lib/structured-extract.ts
12479
+ var exports_structured_extract = {};
12480
+ __export(exports_structured_extract, {
12481
+ extractStructuredData: () => extractStructuredData
12482
+ });
12483
+ async function extractStructuredData(page) {
12484
+ return page.evaluate(() => {
12485
+ const result = { tables: [], lists: [], jsonLd: [], openGraph: {}, metaTags: {}, repeatedElements: [] };
12486
+ document.querySelectorAll("table").forEach((table, idx) => {
12487
+ const headers = [];
12488
+ table.querySelectorAll("thead th, thead td, tr:first-child th").forEach((th) => {
12489
+ headers.push(th.textContent?.trim() ?? "");
12490
+ });
12491
+ const rows = [];
12492
+ table.querySelectorAll("tbody tr, tr:not(:first-child)").forEach((tr) => {
12493
+ const row = [];
12494
+ tr.querySelectorAll("td, th").forEach((td) => {
12495
+ row.push(td.textContent?.trim() ?? "");
12496
+ });
12497
+ if (row.length > 0 && row.some((c) => c !== ""))
12498
+ rows.push(row);
12499
+ });
12500
+ if (rows.length > 0) {
12501
+ result.tables.push({ headers, rows, selector: `table:nth-of-type(${idx + 1})` });
12502
+ }
12503
+ });
12504
+ document.querySelectorAll("ul, ol").forEach((list, idx) => {
12505
+ const items = [];
12506
+ list.querySelectorAll(":scope > li").forEach((li) => {
12507
+ const text = li.textContent?.trim() ?? "";
12508
+ if (text)
12509
+ items.push(text);
12510
+ });
12511
+ if (items.length >= 3) {
12512
+ const tag = list.tagName.toLowerCase();
12513
+ result.lists.push({ items, selector: `${tag}:nth-of-type(${idx + 1})` });
12514
+ }
12515
+ });
12516
+ document.querySelectorAll('script[type="application/ld+json"]').forEach((script) => {
12517
+ try {
12518
+ result.jsonLd.push(JSON.parse(script.textContent ?? ""));
12519
+ } catch {}
12520
+ });
12521
+ document.querySelectorAll('meta[property^="og:"]').forEach((meta) => {
12522
+ const prop = meta.getAttribute("property")?.replace("og:", "") ?? "";
12523
+ result.openGraph[prop] = meta.getAttribute("content") ?? "";
12524
+ });
12525
+ document.querySelectorAll("meta[name]").forEach((meta) => {
12526
+ const name = meta.getAttribute("name") ?? "";
12527
+ if (name)
12528
+ result.metaTags[name] = meta.getAttribute("content") ?? "";
12529
+ });
12530
+ const classCounts = new Map;
12531
+ document.querySelectorAll("[class]").forEach((el) => {
12532
+ const cls = el.className.toString().trim();
12533
+ if (cls && cls.length > 5 && cls.length < 100) {
12534
+ if (!classCounts.has(cls))
12535
+ classCounts.set(cls, []);
12536
+ classCounts.get(cls).push(el);
12537
+ }
12538
+ });
12539
+ for (const [cls, elements] of classCounts) {
12540
+ if (elements.length >= 3 && elements.length <= 200) {
12541
+ const sample = elements.slice(0, 3).map((el) => el.textContent?.trim().slice(0, 100) ?? "");
12542
+ if (sample.some((s) => s.length > 10)) {
12543
+ result.repeatedElements.push({
12544
+ selector: `.${cls.split(" ")[0]}`,
12545
+ count: elements.length,
12546
+ sample
12547
+ });
12548
+ }
12549
+ }
12550
+ }
12551
+ result.repeatedElements.sort((a, b) => b.count - a.count);
12552
+ result.repeatedElements = result.repeatedElements.slice(0, 10);
12553
+ return result;
12554
+ });
12555
+ }
12556
+
12557
+ // src/lib/gallery-diff.ts
12558
+ var exports_gallery_diff = {};
12559
+ __export(exports_gallery_diff, {
12560
+ diffImages: () => diffImages
12561
+ });
12562
+ import { join as join5 } from "path";
12563
+ import { mkdirSync as mkdirSync5 } from "fs";
12564
+ import { homedir as homedir5 } from "os";
12565
+ async function diffImages(path1, path2) {
12566
+ const img1 = import_sharp2.default(path1);
12567
+ const img2 = import_sharp2.default(path2);
12568
+ const [meta1, meta2] = await Promise.all([img1.metadata(), img2.metadata()]);
12569
+ const w = Math.min(meta1.width ?? 1280, meta2.width ?? 1280);
12570
+ const h = Math.min(meta1.height ?? 720, meta2.height ?? 720);
12571
+ const [raw1, raw2] = await Promise.all([
12572
+ import_sharp2.default(path1).resize(w, h, { fit: "fill" }).raw().toBuffer(),
12573
+ import_sharp2.default(path2).resize(w, h, { fit: "fill" }).raw().toBuffer()
12574
+ ]);
12575
+ const totalPixels = w * h;
12576
+ const channels = 3;
12577
+ const diffBuffer = Buffer.alloc(raw1.length);
12578
+ let changedPixels = 0;
12579
+ for (let i = 0;i < raw1.length; i += channels) {
12580
+ const dr = Math.abs(raw1[i] - raw2[i]);
12581
+ const dg = Math.abs(raw1[i + 1] - raw2[i + 1]);
12582
+ const db = Math.abs(raw1[i + 2] - raw2[i + 2]);
12583
+ const diff = (dr + dg + db) / 3;
12584
+ if (diff > 10) {
12585
+ changedPixels++;
12586
+ diffBuffer[i] = 255;
12587
+ diffBuffer[i + 1] = 0;
12588
+ diffBuffer[i + 2] = 0;
12589
+ } else {
12590
+ diffBuffer[i] = Math.round(raw1[i] * 0.4);
12591
+ diffBuffer[i + 1] = Math.round(raw1[i + 1] * 0.4);
12592
+ diffBuffer[i + 2] = Math.round(raw1[i + 2] * 0.4);
12593
+ }
12594
+ }
12595
+ const dataDir = process.env["BROWSER_DATA_DIR"] ?? join5(homedir5(), ".browser");
12596
+ const diffDir = join5(dataDir, "diffs");
12597
+ mkdirSync5(diffDir, { recursive: true });
12598
+ const diffPath = join5(diffDir, `diff-${Date.now()}.webp`);
12599
+ const diffImageBuffer = await import_sharp2.default(diffBuffer, { raw: { width: w, height: h, channels } }).webp({ quality: 85 }).toBuffer();
12600
+ await Bun.write(diffPath, diffImageBuffer);
12601
+ return {
12602
+ diff_path: diffPath,
12603
+ diff_base64: diffImageBuffer.toString("base64"),
12604
+ changed_pixels: changedPixels,
12605
+ total_pixels: totalPixels,
12606
+ changed_percent: changedPixels / totalPixels * 100
12607
+ };
12608
+ }
12609
+ var import_sharp2;
12610
+ var init_gallery_diff = __esm(() => {
12611
+ import_sharp2 = __toESM(require_lib(), 1);
12612
+ });
12613
+
12614
+ // src/lib/profiles.ts
12615
+ var exports_profiles = {};
12616
+ __export(exports_profiles, {
12617
+ saveProfile: () => saveProfile,
12618
+ loadProfile: () => loadProfile,
12619
+ listProfiles: () => listProfiles,
12620
+ deleteProfile: () => deleteProfile,
12621
+ applyProfile: () => applyProfile
12622
+ });
12623
+ import { mkdirSync as mkdirSync6, existsSync as existsSync3, readdirSync as readdirSync2, rmSync, readFileSync, writeFileSync } from "fs";
12624
+ import { join as join6 } from "path";
12625
+ import { homedir as homedir6 } from "os";
12626
+ function getProfilesDir() {
12627
+ const dataDir = process.env["BROWSER_DATA_DIR"] ?? join6(homedir6(), ".browser");
12628
+ const dir = join6(dataDir, "profiles");
12629
+ mkdirSync6(dir, { recursive: true });
12630
+ return dir;
12631
+ }
12632
+ function getProfileDir2(name) {
12633
+ return join6(getProfilesDir(), name);
12634
+ }
12635
+ async function saveProfile(page, name) {
12636
+ const dir = getProfileDir2(name);
12637
+ mkdirSync6(dir, { recursive: true });
12638
+ const cookies = await page.context().cookies();
12639
+ writeFileSync(join6(dir, "cookies.json"), JSON.stringify(cookies, null, 2));
12640
+ let localStorage2 = {};
12641
+ try {
12642
+ localStorage2 = await page.evaluate(() => {
12643
+ const result = {};
12644
+ for (let i = 0;i < window.localStorage.length; i++) {
12645
+ const key = window.localStorage.key(i);
12646
+ result[key] = window.localStorage.getItem(key);
12647
+ }
12648
+ return result;
12649
+ });
12650
+ } catch {}
12651
+ writeFileSync(join6(dir, "storage.json"), JSON.stringify(localStorage2, null, 2));
12652
+ const savedAt = new Date().toISOString();
12653
+ const url = page.url();
12654
+ const meta = { saved_at: savedAt, url };
12655
+ writeFileSync(join6(dir, "meta.json"), JSON.stringify(meta, null, 2));
12656
+ return {
12657
+ name,
12658
+ saved_at: savedAt,
12659
+ url,
12660
+ cookie_count: cookies.length,
12661
+ storage_key_count: Object.keys(localStorage2).length
12662
+ };
12663
+ }
12664
+ function loadProfile(name) {
12665
+ const dir = getProfileDir2(name);
12666
+ if (!existsSync3(dir)) {
12667
+ throw new Error(`Profile not found: ${name}`);
12668
+ }
12669
+ const cookiesPath = join6(dir, "cookies.json");
12670
+ const storagePath = join6(dir, "storage.json");
12671
+ const metaPath = join6(dir, "meta.json");
12672
+ const cookies = existsSync3(cookiesPath) ? JSON.parse(readFileSync(cookiesPath, "utf8")) : [];
12673
+ const localStorage2 = existsSync3(storagePath) ? JSON.parse(readFileSync(storagePath, "utf8")) : {};
12674
+ let savedAt = new Date().toISOString();
12675
+ let url;
12676
+ if (existsSync3(metaPath)) {
12677
+ const meta = JSON.parse(readFileSync(metaPath, "utf8"));
12678
+ savedAt = meta.saved_at ?? savedAt;
12679
+ url = meta.url;
12680
+ }
12681
+ return { cookies, localStorage: localStorage2, saved_at: savedAt, url };
12682
+ }
12683
+ async function applyProfile(page, profileData) {
12684
+ if (profileData.cookies.length > 0) {
12685
+ await page.context().addCookies(profileData.cookies);
12686
+ }
12687
+ const storageKeys = Object.keys(profileData.localStorage);
12688
+ if (storageKeys.length > 0) {
12689
+ try {
12690
+ await page.evaluate((storage) => {
12691
+ for (const [key, value] of Object.entries(storage)) {
12692
+ window.localStorage.setItem(key, value);
12693
+ }
12694
+ }, profileData.localStorage);
12695
+ } catch {}
12696
+ }
12697
+ return {
12698
+ cookies_applied: profileData.cookies.length,
12699
+ storage_keys_applied: storageKeys.length
12700
+ };
12701
+ }
12702
+ function listProfiles() {
12703
+ const dir = getProfilesDir();
12704
+ if (!existsSync3(dir))
12705
+ return [];
12706
+ const entries = readdirSync2(dir, { withFileTypes: true });
12707
+ const profiles = [];
12708
+ for (const entry of entries) {
12709
+ if (!entry.isDirectory())
12710
+ continue;
12711
+ const name = entry.name;
12712
+ const profileDir = join6(dir, name);
12713
+ let savedAt = "";
12714
+ let url;
12715
+ let cookieCount = 0;
12716
+ let storageKeyCount = 0;
12717
+ try {
12718
+ const metaPath = join6(profileDir, "meta.json");
12719
+ if (existsSync3(metaPath)) {
12720
+ const meta = JSON.parse(readFileSync(metaPath, "utf8"));
12721
+ savedAt = meta.saved_at ?? "";
12722
+ url = meta.url;
12723
+ }
12724
+ const cookiesPath = join6(profileDir, "cookies.json");
12725
+ if (existsSync3(cookiesPath)) {
12726
+ const cookies = JSON.parse(readFileSync(cookiesPath, "utf8"));
12727
+ cookieCount = Array.isArray(cookies) ? cookies.length : 0;
12728
+ }
12729
+ const storagePath = join6(profileDir, "storage.json");
12730
+ if (existsSync3(storagePath)) {
12731
+ const storage = JSON.parse(readFileSync(storagePath, "utf8"));
12732
+ storageKeyCount = Object.keys(storage).length;
12733
+ }
12734
+ } catch {}
12735
+ profiles.push({
12736
+ name,
12737
+ saved_at: savedAt,
12738
+ url,
12739
+ cookie_count: cookieCount,
12740
+ storage_key_count: storageKeyCount
12741
+ });
12742
+ }
12743
+ return profiles.sort((a, b) => b.saved_at.localeCompare(a.saved_at));
12744
+ }
12745
+ function deleteProfile(name) {
12746
+ const dir = getProfileDir2(name);
12747
+ if (!existsSync3(dir))
12748
+ return false;
12749
+ try {
12750
+ rmSync(dir, { recursive: true, force: true });
12751
+ return true;
12752
+ } catch {
12753
+ return false;
12754
+ }
12755
+ }
12756
+ var init_profiles = () => {};
12757
+
12758
+ // src/lib/auth.ts
12759
+ var exports_auth = {};
12760
+ __export(exports_auth, {
12761
+ loginWithCredentials: () => loginWithCredentials,
12762
+ getCredentials: () => getCredentials
12763
+ });
12764
+ import { existsSync as existsSync4, readFileSync as readFileSync2 } from "fs";
12765
+ import { join as join7 } from "path";
12766
+ import { homedir as homedir7 } from "os";
12767
+ async function getCredentials(service) {
12768
+ try {
12769
+ const { getSecret } = await import(`${homedir7()}/Workspace/hasna/opensource/opensourcedev/open-secrets/src/store.js`);
12770
+ const email = getSecret(`${service}_email`) ?? getSecret(`${service}_username`) ?? getSecret(`${service}_login`);
12771
+ const password = getSecret(`${service}_password`) ?? getSecret(`${service}_pass`);
12772
+ if (email?.value && password?.value) {
12773
+ return { email: email.value, password: password.value };
12774
+ }
12775
+ } catch {}
12776
+ const secretsPath = join7(homedir7(), ".secrets");
12777
+ if (existsSync4(secretsPath)) {
12778
+ const content = readFileSync2(secretsPath, "utf8");
12779
+ const lines = content.split(`
12780
+ `);
12781
+ const prefix = service.toUpperCase().replace(/[^A-Z0-9]/g, "_");
12782
+ const vars = {};
12783
+ for (const line of lines) {
12784
+ const match = line.match(/^export\s+([A-Z_]+)=["']?(.+?)["']?\s*$/);
12785
+ if (match)
12786
+ vars[match[1]] = match[2];
12787
+ }
12788
+ const email = vars[`${prefix}_EMAIL`] ?? vars[`${prefix}_USERNAME`];
12789
+ const password = vars[`${prefix}_PASSWORD`];
12790
+ if (email && password)
12791
+ return { email, password };
12792
+ }
12793
+ const envPrefix = service.toUpperCase().replace(/[^A-Z0-9]/g, "_");
12794
+ const envEmail = process.env[`${envPrefix}_EMAIL`] ?? process.env[`${envPrefix}_USERNAME`];
12795
+ const envPass = process.env[`${envPrefix}_PASSWORD`];
12796
+ if (envEmail && envPass)
12797
+ return { email: envEmail, password: envPass };
12798
+ return null;
12799
+ }
12800
+ async function loginWithCredentials(page, credentials, opts) {
12801
+ const { fillForm: fillForm2 } = await Promise.resolve().then(() => (init_actions(), exports_actions));
12802
+ const { saveProfile: saveProfile2 } = await Promise.resolve().then(() => (init_profiles(), exports_profiles));
12803
+ try {
12804
+ if (opts?.loginUrl) {
12805
+ await page.goto(opts.loginUrl, { waitUntil: "domcontentloaded" });
12806
+ await new Promise((r) => setTimeout(r, 500));
12807
+ }
12808
+ const emailSel = opts?.emailSelector ?? 'input[type="email"], input[name="email"], input[id*="email"], input[placeholder*="email" i]';
12809
+ const passSel = opts?.passwordSelector ?? 'input[type="password"]';
12810
+ const submitSel = opts?.submitSelector ?? 'button[type="submit"], input[type="submit"], button:contains("Sign in"), button:contains("Log in"), button:contains("Login")';
12811
+ const fields = {};
12812
+ if (credentials.email)
12813
+ fields[emailSel] = credentials.email;
12814
+ else if (credentials.username)
12815
+ fields[emailSel] = credentials.username;
12816
+ if (credentials.password)
12817
+ fields[passSel] = credentials.password;
12818
+ const fillResult = await fillForm2(page, fields, submitSel);
12819
+ const successText = opts?.waitForText ?? "dashboard|profile|account|welcome|signed in|logout";
12820
+ await new Promise((r) => setTimeout(r, 1500));
12821
+ const currentUrl = page.url?.() ?? "";
12822
+ const logged_in = fillResult.errors.length === 0;
12823
+ let profile_saved = false;
12824
+ if (opts?.saveProfile && logged_in) {
12825
+ try {
12826
+ await saveProfile2(page, opts.saveProfile);
12827
+ profile_saved = true;
12828
+ } catch {}
12829
+ }
12830
+ return {
12831
+ logged_in,
12832
+ redirect_url: currentUrl,
12833
+ profile_saved,
12834
+ method: "secrets_vault"
12835
+ };
12836
+ } catch (err) {
12837
+ return {
12838
+ logged_in: false,
12839
+ redirect_url: "",
12840
+ profile_saved: false,
12841
+ method: "not_found",
12842
+ error: err instanceof Error ? err.message : String(err)
12843
+ };
12844
+ }
12845
+ }
12846
+ var init_auth = () => {};
12847
+
12848
+ // src/lib/daemon-client.ts
12849
+ var exports_daemon_client = {};
12850
+ __export(exports_daemon_client, {
12851
+ isDaemonRunning: () => isDaemonRunning,
12852
+ getDaemonStatus: () => getDaemonStatus,
12853
+ getDaemonPort: () => getDaemonPort,
12854
+ getDaemonPidFile: () => getDaemonPidFile,
12855
+ getDaemonPid: () => getDaemonPid
12856
+ });
12857
+ import { existsSync as existsSync5, readFileSync as readFileSync3 } from "fs";
12858
+ import { join as join8 } from "path";
12859
+ import { homedir as homedir8 } from "os";
12860
+ function getDaemonPidFile() {
12861
+ return PID_FILE;
12862
+ }
12863
+ function getDaemonPort() {
12864
+ return parseInt(process.env["BROWSER_DAEMON_PORT"] ?? String(DEFAULT_PORT), 10);
12865
+ }
12866
+ function isDaemonRunning() {
12867
+ if (!existsSync5(PID_FILE))
12868
+ return false;
12869
+ try {
12870
+ const pid = parseInt(readFileSync3(PID_FILE, "utf8").trim(), 10);
12871
+ process.kill(pid, 0);
12872
+ return true;
12873
+ } catch {
12874
+ return false;
12875
+ }
12876
+ }
12877
+ function getDaemonPid() {
12878
+ if (!existsSync5(PID_FILE))
12879
+ return null;
12880
+ try {
12881
+ return parseInt(readFileSync3(PID_FILE, "utf8").trim(), 10);
12882
+ } catch {
12883
+ return null;
12884
+ }
12885
+ }
12886
+ async function getDaemonStatus() {
12887
+ const pid = getDaemonPid();
12888
+ const port = getDaemonPort();
12889
+ if (!isDaemonRunning())
12890
+ return { running: false, pid: null, port };
12891
+ try {
12892
+ const res = await fetch(`http://localhost:${port}/health`, { signal: AbortSignal.timeout(2000) });
12893
+ const data = await res.json();
12894
+ return { running: true, pid, port, sessions: data.active_sessions ?? 0, uptime_ms: data.uptime_ms };
12895
+ } catch {
12896
+ return { running: true, pid, port };
12897
+ }
12898
+ }
12899
+ var PID_FILE, DEFAULT_PORT = 7030;
12900
+ var init_daemon_client = __esm(() => {
12901
+ PID_FILE = join8(process.env["BROWSER_DATA_DIR"] ?? join8(homedir8(), ".browser"), "daemon.pid");
12902
+ });
12903
+
12119
12904
  // node_modules/zod/v3/helpers/util.js
12120
12905
  var util, objectUtil, ZodParsedType, getParsedType = (data) => {
12121
12906
  const t = typeof data;
@@ -16186,17 +16971,17 @@ __export(exports_downloads, {
16186
16971
  deleteDownload: () => deleteDownload,
16187
16972
  cleanStaleDownloads: () => cleanStaleDownloads
16188
16973
  });
16189
- import { randomUUID as randomUUID9 } from "crypto";
16190
- import { join as join5, basename, extname } from "path";
16191
- import { mkdirSync as mkdirSync5, existsSync as existsSync2, readdirSync as readdirSync2, statSync, unlinkSync as unlinkSync2, copyFileSync, writeFileSync, readFileSync } from "fs";
16192
- import { homedir as homedir5 } from "os";
16974
+ import { randomUUID as randomUUID10 } from "crypto";
16975
+ import { join as join9, basename, extname } from "path";
16976
+ import { mkdirSync as mkdirSync7, existsSync as existsSync6, readdirSync as readdirSync3, statSync, unlinkSync as unlinkSync2, copyFileSync, writeFileSync as writeFileSync2, readFileSync as readFileSync4 } from "fs";
16977
+ import { homedir as homedir9 } from "os";
16193
16978
  function getDataDir3() {
16194
- return process.env["BROWSER_DATA_DIR"] ?? join5(homedir5(), ".browser");
16979
+ return process.env["BROWSER_DATA_DIR"] ?? join9(homedir9(), ".browser");
16195
16980
  }
16196
16981
  function getDownloadsDir(sessionId) {
16197
- const base = join5(getDataDir3(), "downloads");
16198
- const dir = sessionId ? join5(base, sessionId) : base;
16199
- mkdirSync5(dir, { recursive: true });
16982
+ const base = join9(getDataDir3(), "downloads");
16983
+ const dir = sessionId ? join9(base, sessionId) : base;
16984
+ mkdirSync7(dir, { recursive: true });
16200
16985
  return dir;
16201
16986
  }
16202
16987
  function ensureDownloadsDir() {
@@ -16207,12 +16992,12 @@ function metaPath(filePath) {
16207
16992
  }
16208
16993
  function saveToDownloads(buffer, filename, opts) {
16209
16994
  const dir = getDownloadsDir(opts?.sessionId);
16210
- const id = randomUUID9();
16995
+ const id = randomUUID10();
16211
16996
  const ext = extname(filename) || "";
16212
16997
  const stem = basename(filename, ext);
16213
16998
  const uniqueName = `${stem}-${id.slice(0, 8)}${ext}`;
16214
- const filePath = join5(dir, uniqueName);
16215
- writeFileSync(filePath, buffer);
16999
+ const filePath = join9(dir, uniqueName);
17000
+ writeFileSync2(filePath, buffer);
16216
17001
  const meta = {
16217
17002
  id,
16218
17003
  type: opts?.type ?? detectType(filename),
@@ -16223,7 +17008,7 @@ function saveToDownloads(buffer, filename, opts) {
16223
17008
  original_name: filename,
16224
17009
  ...opts?.metadata
16225
17010
  };
16226
- writeFileSync(metaPath(filePath), JSON.stringify(meta, null, 2));
17011
+ writeFileSync2(metaPath(filePath), JSON.stringify(meta, null, 2));
16227
17012
  return {
16228
17013
  id,
16229
17014
  path: filePath,
@@ -16240,23 +17025,23 @@ function listDownloads(sessionId) {
16240
17025
  const dir = getDownloadsDir(sessionId);
16241
17026
  const results = [];
16242
17027
  function scanDir(d) {
16243
- if (!existsSync2(d))
17028
+ if (!existsSync6(d))
16244
17029
  return;
16245
- const entries = readdirSync2(d);
17030
+ const entries = readdirSync3(d);
16246
17031
  for (const entry of entries) {
16247
17032
  if (entry.endsWith(".meta.json"))
16248
17033
  continue;
16249
- const full = join5(d, entry);
17034
+ const full = join9(d, entry);
16250
17035
  const stat = statSync(full);
16251
17036
  if (stat.isDirectory()) {
16252
17037
  scanDir(full);
16253
17038
  continue;
16254
17039
  }
16255
17040
  const mpath = metaPath(full);
16256
- if (!existsSync2(mpath))
17041
+ if (!existsSync6(mpath))
16257
17042
  continue;
16258
17043
  try {
16259
- const meta = JSON.parse(readFileSync(mpath, "utf8"));
17044
+ const meta = JSON.parse(readFileSync4(mpath, "utf8"));
16260
17045
  results.push({
16261
17046
  id: meta.id,
16262
17047
  path: full,
@@ -16284,7 +17069,7 @@ function deleteDownload(id, sessionId) {
16284
17069
  return false;
16285
17070
  try {
16286
17071
  unlinkSync2(file.path);
16287
- if (existsSync2(file.meta_path))
17072
+ if (existsSync6(file.meta_path))
16288
17073
  unlinkSync2(file.meta_path);
16289
17074
  return true;
16290
17075
  } catch {
@@ -16330,67 +17115,10 @@ function detectType(filename) {
16330
17115
  }
16331
17116
  var init_downloads = () => {};
16332
17117
 
16333
- // src/lib/gallery-diff.ts
16334
- var exports_gallery_diff = {};
16335
- __export(exports_gallery_diff, {
16336
- diffImages: () => diffImages
16337
- });
16338
- import { join as join6 } from "path";
16339
- import { mkdirSync as mkdirSync6 } from "fs";
16340
- import { homedir as homedir6 } from "os";
16341
- async function diffImages(path1, path2) {
16342
- const img1 = import_sharp2.default(path1);
16343
- const img2 = import_sharp2.default(path2);
16344
- const [meta1, meta2] = await Promise.all([img1.metadata(), img2.metadata()]);
16345
- const w = Math.min(meta1.width ?? 1280, meta2.width ?? 1280);
16346
- const h = Math.min(meta1.height ?? 720, meta2.height ?? 720);
16347
- const [raw1, raw2] = await Promise.all([
16348
- import_sharp2.default(path1).resize(w, h, { fit: "fill" }).raw().toBuffer(),
16349
- import_sharp2.default(path2).resize(w, h, { fit: "fill" }).raw().toBuffer()
16350
- ]);
16351
- const totalPixels = w * h;
16352
- const channels = 3;
16353
- const diffBuffer = Buffer.alloc(raw1.length);
16354
- let changedPixels = 0;
16355
- for (let i = 0;i < raw1.length; i += channels) {
16356
- const dr = Math.abs(raw1[i] - raw2[i]);
16357
- const dg = Math.abs(raw1[i + 1] - raw2[i + 1]);
16358
- const db = Math.abs(raw1[i + 2] - raw2[i + 2]);
16359
- const diff = (dr + dg + db) / 3;
16360
- if (diff > 10) {
16361
- changedPixels++;
16362
- diffBuffer[i] = 255;
16363
- diffBuffer[i + 1] = 0;
16364
- diffBuffer[i + 2] = 0;
16365
- } else {
16366
- diffBuffer[i] = Math.round(raw1[i] * 0.4);
16367
- diffBuffer[i + 1] = Math.round(raw1[i + 1] * 0.4);
16368
- diffBuffer[i + 2] = Math.round(raw1[i + 2] * 0.4);
16369
- }
16370
- }
16371
- const dataDir = process.env["BROWSER_DATA_DIR"] ?? join6(homedir6(), ".browser");
16372
- const diffDir = join6(dataDir, "diffs");
16373
- mkdirSync6(diffDir, { recursive: true });
16374
- const diffPath = join6(diffDir, `diff-${Date.now()}.webp`);
16375
- const diffImageBuffer = await import_sharp2.default(diffBuffer, { raw: { width: w, height: h, channels } }).webp({ quality: 85 }).toBuffer();
16376
- await Bun.write(diffPath, diffImageBuffer);
16377
- return {
16378
- diff_path: diffPath,
16379
- diff_base64: diffImageBuffer.toString("base64"),
16380
- changed_pixels: changedPixels,
16381
- total_pixels: totalPixels,
16382
- changed_percent: changedPixels / totalPixels * 100
16383
- };
16384
- }
16385
- var import_sharp2;
16386
- var init_gallery_diff = __esm(() => {
16387
- import_sharp2 = __toESM(require_lib(), 1);
16388
- });
16389
-
16390
17118
  // src/lib/files-integration.ts
16391
- import { join as join7 } from "path";
16392
- import { mkdirSync as mkdirSync7, copyFileSync as copyFileSync2 } from "fs";
16393
- import { homedir as homedir7 } from "os";
17119
+ import { join as join10 } from "path";
17120
+ import { mkdirSync as mkdirSync8, copyFileSync as copyFileSync2 } from "fs";
17121
+ import { homedir as homedir10 } from "os";
16394
17122
  async function persistFile(localPath, opts) {
16395
17123
  try {
16396
17124
  const mod = await import("@hasna/files");
@@ -16399,12 +17127,12 @@ async function persistFile(localPath, opts) {
16399
17127
  return { id: ref.id, path: ref.path ?? localPath, permanent: true, provider: "open-files" };
16400
17128
  }
16401
17129
  } catch {}
16402
- const dataDir = process.env["BROWSER_DATA_DIR"] ?? join7(homedir7(), ".browser");
17130
+ const dataDir = process.env["BROWSER_DATA_DIR"] ?? join10(homedir10(), ".browser");
16403
17131
  const date = new Date().toISOString().split("T")[0];
16404
- const dir = join7(dataDir, "persistent", date);
16405
- mkdirSync7(dir, { recursive: true });
17132
+ const dir = join10(dataDir, "persistent", date);
17133
+ mkdirSync8(dir, { recursive: true });
16406
17134
  const filename = localPath.split("/").pop() ?? "file";
16407
- const targetPath = join7(dir, filename);
17135
+ const targetPath = join10(dir, filename);
16408
17136
  copyFileSync2(localPath, targetPath);
16409
17137
  return {
16410
17138
  id: `local-${Date.now()}`,
@@ -16510,150 +17238,6 @@ async function closeTab(page, index) {
16510
17238
  };
16511
17239
  }
16512
17240
 
16513
- // src/lib/profiles.ts
16514
- var exports_profiles = {};
16515
- __export(exports_profiles, {
16516
- saveProfile: () => saveProfile,
16517
- loadProfile: () => loadProfile,
16518
- listProfiles: () => listProfiles,
16519
- deleteProfile: () => deleteProfile,
16520
- applyProfile: () => applyProfile
16521
- });
16522
- import { mkdirSync as mkdirSync8, existsSync as existsSync4, readdirSync as readdirSync3, rmSync, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
16523
- import { join as join8 } from "path";
16524
- import { homedir as homedir8 } from "os";
16525
- function getProfilesDir() {
16526
- const dataDir = process.env["BROWSER_DATA_DIR"] ?? join8(homedir8(), ".browser");
16527
- const dir = join8(dataDir, "profiles");
16528
- mkdirSync8(dir, { recursive: true });
16529
- return dir;
16530
- }
16531
- function getProfileDir2(name) {
16532
- return join8(getProfilesDir(), name);
16533
- }
16534
- async function saveProfile(page, name) {
16535
- const dir = getProfileDir2(name);
16536
- mkdirSync8(dir, { recursive: true });
16537
- const cookies = await page.context().cookies();
16538
- writeFileSync2(join8(dir, "cookies.json"), JSON.stringify(cookies, null, 2));
16539
- let localStorage2 = {};
16540
- try {
16541
- localStorage2 = await page.evaluate(() => {
16542
- const result = {};
16543
- for (let i = 0;i < window.localStorage.length; i++) {
16544
- const key = window.localStorage.key(i);
16545
- result[key] = window.localStorage.getItem(key);
16546
- }
16547
- return result;
16548
- });
16549
- } catch {}
16550
- writeFileSync2(join8(dir, "storage.json"), JSON.stringify(localStorage2, null, 2));
16551
- const savedAt = new Date().toISOString();
16552
- const url = page.url();
16553
- const meta = { saved_at: savedAt, url };
16554
- writeFileSync2(join8(dir, "meta.json"), JSON.stringify(meta, null, 2));
16555
- return {
16556
- name,
16557
- saved_at: savedAt,
16558
- url,
16559
- cookie_count: cookies.length,
16560
- storage_key_count: Object.keys(localStorage2).length
16561
- };
16562
- }
16563
- function loadProfile(name) {
16564
- const dir = getProfileDir2(name);
16565
- if (!existsSync4(dir)) {
16566
- throw new Error(`Profile not found: ${name}`);
16567
- }
16568
- const cookiesPath = join8(dir, "cookies.json");
16569
- const storagePath = join8(dir, "storage.json");
16570
- const metaPath2 = join8(dir, "meta.json");
16571
- const cookies = existsSync4(cookiesPath) ? JSON.parse(readFileSync2(cookiesPath, "utf8")) : [];
16572
- const localStorage2 = existsSync4(storagePath) ? JSON.parse(readFileSync2(storagePath, "utf8")) : {};
16573
- let savedAt = new Date().toISOString();
16574
- let url;
16575
- if (existsSync4(metaPath2)) {
16576
- const meta = JSON.parse(readFileSync2(metaPath2, "utf8"));
16577
- savedAt = meta.saved_at ?? savedAt;
16578
- url = meta.url;
16579
- }
16580
- return { cookies, localStorage: localStorage2, saved_at: savedAt, url };
16581
- }
16582
- async function applyProfile(page, profileData) {
16583
- if (profileData.cookies.length > 0) {
16584
- await page.context().addCookies(profileData.cookies);
16585
- }
16586
- const storageKeys = Object.keys(profileData.localStorage);
16587
- if (storageKeys.length > 0) {
16588
- try {
16589
- await page.evaluate((storage) => {
16590
- for (const [key, value] of Object.entries(storage)) {
16591
- window.localStorage.setItem(key, value);
16592
- }
16593
- }, profileData.localStorage);
16594
- } catch {}
16595
- }
16596
- return {
16597
- cookies_applied: profileData.cookies.length,
16598
- storage_keys_applied: storageKeys.length
16599
- };
16600
- }
16601
- function listProfiles() {
16602
- const dir = getProfilesDir();
16603
- if (!existsSync4(dir))
16604
- return [];
16605
- const entries = readdirSync3(dir, { withFileTypes: true });
16606
- const profiles = [];
16607
- for (const entry of entries) {
16608
- if (!entry.isDirectory())
16609
- continue;
16610
- const name = entry.name;
16611
- const profileDir = join8(dir, name);
16612
- let savedAt = "";
16613
- let url;
16614
- let cookieCount = 0;
16615
- let storageKeyCount = 0;
16616
- try {
16617
- const metaPath2 = join8(profileDir, "meta.json");
16618
- if (existsSync4(metaPath2)) {
16619
- const meta = JSON.parse(readFileSync2(metaPath2, "utf8"));
16620
- savedAt = meta.saved_at ?? "";
16621
- url = meta.url;
16622
- }
16623
- const cookiesPath = join8(profileDir, "cookies.json");
16624
- if (existsSync4(cookiesPath)) {
16625
- const cookies = JSON.parse(readFileSync2(cookiesPath, "utf8"));
16626
- cookieCount = Array.isArray(cookies) ? cookies.length : 0;
16627
- }
16628
- const storagePath = join8(profileDir, "storage.json");
16629
- if (existsSync4(storagePath)) {
16630
- const storage = JSON.parse(readFileSync2(storagePath, "utf8"));
16631
- storageKeyCount = Object.keys(storage).length;
16632
- }
16633
- } catch {}
16634
- profiles.push({
16635
- name,
16636
- saved_at: savedAt,
16637
- url,
16638
- cookie_count: cookieCount,
16639
- storage_key_count: storageKeyCount
16640
- });
16641
- }
16642
- return profiles.sort((a, b) => b.saved_at.localeCompare(a.saved_at));
16643
- }
16644
- function deleteProfile(name) {
16645
- const dir = getProfileDir2(name);
16646
- if (!existsSync4(dir))
16647
- return false;
16648
- try {
16649
- rmSync(dir, { recursive: true, force: true });
16650
- return true;
16651
- } catch {
16652
- return false;
16653
- }
16654
- }
16655
- var init_profiles = () => {};
16656
-
16657
17241
  // src/lib/sanitize.ts
16658
17242
  var exports_sanitize = {};
16659
17243
  __export(exports_sanitize, {
@@ -16805,6 +17389,166 @@ var init_annotate = __esm(() => {
16805
17389
  import_sharp3 = __toESM(require_lib(), 1);
16806
17390
  });
16807
17391
 
17392
+ // src/lib/workflows.ts
17393
+ var exports_workflows = {};
17394
+ __export(exports_workflows, {
17395
+ saveWorkflowFromRecording: () => saveWorkflowFromRecording,
17396
+ saveWorkflow: () => saveWorkflow,
17397
+ runWorkflow: () => runWorkflow,
17398
+ listWorkflows: () => listWorkflows,
17399
+ getWorkflowByName: () => getWorkflowByName,
17400
+ getWorkflow: () => getWorkflow,
17401
+ deleteWorkflow: () => deleteWorkflow
17402
+ });
17403
+ import { randomUUID as randomUUID11 } from "crypto";
17404
+ function saveWorkflow(data) {
17405
+ const db = getDatabase();
17406
+ const id = randomUUID11();
17407
+ db.prepare("INSERT OR REPLACE INTO workflows (id, name, description, steps, start_url) VALUES (?, ?, ?, ?, ?)").run(id, data.name, data.description ?? null, JSON.stringify(data.steps), data.startUrl ?? null);
17408
+ return getWorkflow(id);
17409
+ }
17410
+ function saveWorkflowFromRecording(recordingId, name, description) {
17411
+ const db = getDatabase();
17412
+ const rec = db.query("SELECT steps, start_url FROM recordings WHERE id = ?").get(recordingId);
17413
+ if (!rec)
17414
+ throw new Error(`Recording not found: ${recordingId}`);
17415
+ const steps = JSON.parse(rec.steps);
17416
+ return saveWorkflow({ name, description, steps, startUrl: rec.start_url ?? undefined });
17417
+ }
17418
+ function getWorkflow(id) {
17419
+ const db = getDatabase();
17420
+ const row = db.query("SELECT * FROM workflows WHERE id = ?").get(id);
17421
+ if (!row)
17422
+ return null;
17423
+ return { ...row, steps: JSON.parse(row.steps) };
17424
+ }
17425
+ function getWorkflowByName(name) {
17426
+ const db = getDatabase();
17427
+ const row = db.query("SELECT * FROM workflows WHERE name = ?").get(name);
17428
+ if (!row)
17429
+ return null;
17430
+ return { ...row, steps: JSON.parse(row.steps) };
17431
+ }
17432
+ function listWorkflows() {
17433
+ const db = getDatabase();
17434
+ return db.query("SELECT * FROM workflows ORDER BY updated_at DESC").all().map((row) => ({ ...row, steps: JSON.parse(row.steps) }));
17435
+ }
17436
+ function deleteWorkflow(name) {
17437
+ const db = getDatabase();
17438
+ return db.prepare("DELETE FROM workflows WHERE name = ?").run(name).changes > 0;
17439
+ }
17440
+ function recordRun(id, healed) {
17441
+ const db = getDatabase();
17442
+ if (healed) {
17443
+ db.prepare("UPDATE workflows SET last_run = datetime('now'), last_heal = datetime('now'), heal_count = heal_count + 1, run_count = run_count + 1, updated_at = datetime('now') WHERE id = ?").run(id);
17444
+ } else {
17445
+ db.prepare("UPDATE workflows SET last_run = datetime('now'), run_count = run_count + 1, updated_at = datetime('now') WHERE id = ?").run(id);
17446
+ }
17447
+ }
17448
+ async function runWorkflow(workflow, page) {
17449
+ const t0 = Date.now();
17450
+ let executed = 0;
17451
+ let failed = 0;
17452
+ let healed = 0;
17453
+ const healedDetails = [];
17454
+ const errors2 = [];
17455
+ const updatedSteps = [...workflow.steps];
17456
+ for (let i = 0;i < workflow.steps.length; i++) {
17457
+ const step = workflow.steps[i];
17458
+ try {
17459
+ switch (step.type) {
17460
+ case "navigate":
17461
+ if (step.url)
17462
+ await page.goto(step.url, { waitUntil: "domcontentloaded", timeout: 30000 });
17463
+ break;
17464
+ case "click":
17465
+ if (step.selector) {
17466
+ try {
17467
+ await page.click(step.selector, { timeout: 5000 });
17468
+ } catch {
17469
+ const result = await healSelector(page, step.selector);
17470
+ if (result.found && result.locator) {
17471
+ await result.locator.click();
17472
+ healed++;
17473
+ const healedSelector = `[healed:${result.method}]${step.selector}`;
17474
+ healedDetails.push({ step: i, original: step.selector, healed_to: healedSelector, method: result.method });
17475
+ } else {
17476
+ throw new Error(`Click failed: ${step.selector} (self-healing exhausted)`);
17477
+ }
17478
+ }
17479
+ }
17480
+ break;
17481
+ case "type":
17482
+ if (step.selector && step.value) {
17483
+ try {
17484
+ await page.fill(step.selector, step.value);
17485
+ } catch {
17486
+ const result = await healSelector(page, step.selector);
17487
+ if (result.found && result.locator) {
17488
+ await result.locator.fill(step.value);
17489
+ healed++;
17490
+ healedDetails.push({ step: i, original: step.selector, healed_to: `[healed:${result.method}]`, method: result.method });
17491
+ } else {
17492
+ throw new Error(`Type failed: ${step.selector} (self-healing exhausted)`);
17493
+ }
17494
+ }
17495
+ }
17496
+ break;
17497
+ case "scroll":
17498
+ if (step.y)
17499
+ await page.mouse.wheel(0, step.y);
17500
+ break;
17501
+ case "hover":
17502
+ if (step.selector) {
17503
+ try {
17504
+ await page.hover(step.selector);
17505
+ } catch {}
17506
+ }
17507
+ break;
17508
+ case "select":
17509
+ if (step.selector && step.value) {
17510
+ try {
17511
+ await page.selectOption(step.selector, step.value);
17512
+ } catch {}
17513
+ }
17514
+ break;
17515
+ case "wait":
17516
+ if (step.selector) {
17517
+ try {
17518
+ await page.waitForSelector(step.selector, { timeout: 1e4 });
17519
+ } catch {}
17520
+ } else {
17521
+ await new Promise((r) => setTimeout(r, step.timestamp || 1000));
17522
+ }
17523
+ break;
17524
+ case "evaluate":
17525
+ if (step.value)
17526
+ await page.evaluate(step.value);
17527
+ break;
17528
+ default:
17529
+ break;
17530
+ }
17531
+ executed++;
17532
+ } catch (err) {
17533
+ failed++;
17534
+ errors2.push(`Step ${i} (${step.type}): ${err instanceof Error ? err.message : String(err)}`);
17535
+ }
17536
+ }
17537
+ recordRun(workflow.id, healed > 0);
17538
+ return {
17539
+ success: failed === 0,
17540
+ steps_executed: executed,
17541
+ steps_failed: failed,
17542
+ steps_healed: healed,
17543
+ healed_details: healedDetails,
17544
+ errors: errors2,
17545
+ duration_ms: Date.now() - t0
17546
+ };
17547
+ }
17548
+ var init_workflows = __esm(() => {
17549
+ init_schema();
17550
+ });
17551
+
16808
17552
  // src/lib/auth-flow.ts
16809
17553
  var exports_auth_flow = {};
16810
17554
  __export(exports_auth_flow, {
@@ -16819,10 +17563,10 @@ __export(exports_auth_flow, {
16819
17563
  getAuthFlow: () => getAuthFlow,
16820
17564
  deleteAuthFlow: () => deleteAuthFlow
16821
17565
  });
16822
- import { randomUUID as randomUUID10 } from "crypto";
17566
+ import { randomUUID as randomUUID12 } from "crypto";
16823
17567
  function saveAuthFlow(data) {
16824
17568
  const db = getDatabase();
16825
- const id = randomUUID10();
17569
+ const id = randomUUID12();
16826
17570
  db.prepare("INSERT OR REPLACE INTO auth_flows (id, name, domain, recording_id, storage_state_path) VALUES (?, ?, ?, ?, ?)").run(id, data.name, data.domain, data.recordingId ?? null, data.storageStatePath ?? null);
16827
17571
  return getAuthFlow(id);
16828
17572
  }
@@ -16865,9 +17609,9 @@ async function tryReplayAuth(page, domain) {
16865
17609
  return { replayed: false };
16866
17610
  if (flow.storage_state_path) {
16867
17611
  try {
16868
- const { existsSync: existsSync5, readFileSync: readFileSync3 } = await import("fs");
16869
- if (existsSync5(flow.storage_state_path)) {
16870
- const state = JSON.parse(readFileSync3(flow.storage_state_path, "utf8"));
17612
+ const { existsSync: existsSync7, readFileSync: readFileSync5 } = await import("fs");
17613
+ if (existsSync7(flow.storage_state_path)) {
17614
+ const state = JSON.parse(readFileSync5(flow.storage_state_path, "utf8"));
16871
17615
  if (state.cookies?.length) {
16872
17616
  await page.context().addCookies(state.cookies);
16873
17617
  await page.reload();
@@ -16992,95 +17736,88 @@ async function clickByVision(page, description, opts) {
16992
17736
  }
16993
17737
  var DEFAULT_MODEL = "claude-sonnet-4-5-20250929";
16994
17738
 
16995
- // src/lib/auth.ts
16996
- var exports_auth = {};
16997
- __export(exports_auth, {
16998
- loginWithCredentials: () => loginWithCredentials,
16999
- getCredentials: () => getCredentials
17739
+ // src/lib/datasets.ts
17740
+ var exports_datasets = {};
17741
+ __export(exports_datasets, {
17742
+ saveDataset: () => saveDataset,
17743
+ listDatasets: () => listDatasets,
17744
+ getDatasetByName: () => getDatasetByName,
17745
+ getDataset: () => getDataset,
17746
+ exportDataset: () => exportDataset,
17747
+ deleteDataset: () => deleteDataset
17000
17748
  });
17001
- import { existsSync as existsSync5, readFileSync as readFileSync3 } from "fs";
17002
- import { join as join9 } from "path";
17003
- import { homedir as homedir9 } from "os";
17004
- async function getCredentials(service) {
17005
- try {
17006
- const { getSecret } = await import(`${homedir9()}/Workspace/hasna/opensource/opensourcedev/open-secrets/src/store.js`);
17007
- const email = getSecret(`${service}_email`) ?? getSecret(`${service}_username`) ?? getSecret(`${service}_login`);
17008
- const password = getSecret(`${service}_password`) ?? getSecret(`${service}_pass`);
17009
- if (email?.value && password?.value) {
17010
- return { email: email.value, password: password.value };
17011
- }
17012
- } catch {}
17013
- const secretsPath = join9(homedir9(), ".secrets");
17014
- if (existsSync5(secretsPath)) {
17015
- const content = readFileSync3(secretsPath, "utf8");
17016
- const lines = content.split(`
17017
- `);
17018
- const prefix = service.toUpperCase().replace(/[^A-Z0-9]/g, "_");
17019
- const vars = {};
17020
- for (const line of lines) {
17021
- const match = line.match(/^export\s+([A-Z_]+)=["']?(.+?)["']?\s*$/);
17022
- if (match)
17023
- vars[match[1]] = match[2];
17024
- }
17025
- const email = vars[`${prefix}_EMAIL`] ?? vars[`${prefix}_USERNAME`];
17026
- const password = vars[`${prefix}_PASSWORD`];
17027
- if (email && password)
17028
- return { email, password };
17749
+ import { randomUUID as randomUUID13 } from "crypto";
17750
+ import { writeFileSync as writeFileSync3, mkdirSync as mkdirSync9 } from "fs";
17751
+ import { join as join11 } from "path";
17752
+ import { homedir as homedir11 } from "os";
17753
+ function saveDataset(data) {
17754
+ const db = getDatabase();
17755
+ const id = randomUUID13();
17756
+ const existing = db.query("SELECT id FROM datasets WHERE name = ?").get(data.name);
17757
+ if (existing) {
17758
+ db.prepare("UPDATE datasets SET data = ?, row_count = ?, source_url = ?, schema = ?, last_refresh = datetime('now'), updated_at = datetime('now') WHERE name = ?").run(JSON.stringify(data.rows), data.rows.length, data.sourceUrl ?? null, data.schema ? JSON.stringify(data.schema) : null, data.name);
17759
+ return getDataset(existing.id);
17029
17760
  }
17030
- const envPrefix = service.toUpperCase().replace(/[^A-Z0-9]/g, "_");
17031
- const envEmail = process.env[`${envPrefix}_EMAIL`] ?? process.env[`${envPrefix}_USERNAME`];
17032
- const envPass = process.env[`${envPrefix}_PASSWORD`];
17033
- if (envEmail && envPass)
17034
- return { email: envEmail, password: envPass };
17035
- return null;
17761
+ db.prepare("INSERT INTO datasets (id, name, source_url, source_type, data, row_count, schema) VALUES (?, ?, ?, ?, ?, ?, ?)").run(id, data.name, data.sourceUrl ?? null, data.sourceType ?? "page", JSON.stringify(data.rows), data.rows.length, data.schema ? JSON.stringify(data.schema) : null);
17762
+ return getDataset(id);
17036
17763
  }
17037
- async function loginWithCredentials(page, credentials, opts) {
17038
- const { fillForm: fillForm2 } = await Promise.resolve().then(() => (init_actions(), exports_actions));
17039
- const { saveProfile: saveProfile2 } = await Promise.resolve().then(() => (init_profiles(), exports_profiles));
17040
- try {
17041
- if (opts?.loginUrl) {
17042
- await page.goto(opts.loginUrl, { waitUntil: "domcontentloaded" });
17043
- await new Promise((r) => setTimeout(r, 500));
17764
+ function getDataset(id) {
17765
+ const db = getDatabase();
17766
+ const row = db.query("SELECT * FROM datasets WHERE id = ?").get(id);
17767
+ if (!row)
17768
+ return null;
17769
+ return { ...row, data: JSON.parse(row.data), schema: row.schema ? JSON.parse(row.schema) : null };
17770
+ }
17771
+ function getDatasetByName(name) {
17772
+ const db = getDatabase();
17773
+ const row = db.query("SELECT * FROM datasets WHERE name = ?").get(name);
17774
+ if (!row)
17775
+ return null;
17776
+ return { ...row, data: JSON.parse(row.data), schema: row.schema ? JSON.parse(row.schema) : null };
17777
+ }
17778
+ function listDatasets() {
17779
+ const db = getDatabase();
17780
+ return db.query("SELECT id, name, source_url, source_type, row_count, last_refresh, created_at, updated_at FROM datasets ORDER BY updated_at DESC").all().map((row) => ({ ...row, data: `${row.row_count} rows`, schema: null }));
17781
+ }
17782
+ function deleteDataset(name) {
17783
+ const db = getDatabase();
17784
+ return db.prepare("DELETE FROM datasets WHERE name = ?").run(name).changes > 0;
17785
+ }
17786
+ function exportDataset(name, format) {
17787
+ const dataset = getDatasetByName(name);
17788
+ if (!dataset)
17789
+ throw new Error(`Dataset '${name}' not found`);
17790
+ const dir = join11(process.env["BROWSER_DATA_DIR"] ?? join11(homedir11(), ".browser"), "exports");
17791
+ mkdirSync9(dir, { recursive: true });
17792
+ const filename = `${name}.${format}`;
17793
+ const path = join11(dir, filename);
17794
+ if (format === "csv") {
17795
+ const rows = dataset.data;
17796
+ if (rows.length === 0) {
17797
+ writeFileSync3(path, "");
17798
+ return { path, size: 0 };
17044
17799
  }
17045
- const emailSel = opts?.emailSelector ?? 'input[type="email"], input[name="email"], input[id*="email"], input[placeholder*="email" i]';
17046
- const passSel = opts?.passwordSelector ?? 'input[type="password"]';
17047
- const submitSel = opts?.submitSelector ?? 'button[type="submit"], input[type="submit"], button:contains("Sign in"), button:contains("Log in"), button:contains("Login")';
17048
- const fields = {};
17049
- if (credentials.email)
17050
- fields[emailSel] = credentials.email;
17051
- else if (credentials.username)
17052
- fields[emailSel] = credentials.username;
17053
- if (credentials.password)
17054
- fields[passSel] = credentials.password;
17055
- const fillResult = await fillForm2(page, fields, submitSel);
17056
- const successText = opts?.waitForText ?? "dashboard|profile|account|welcome|signed in|logout";
17057
- await new Promise((r) => setTimeout(r, 1500));
17058
- const currentUrl = page.url?.() ?? "";
17059
- const logged_in = fillResult.errors.length === 0;
17060
- let profile_saved = false;
17061
- if (opts?.saveProfile && logged_in) {
17062
- try {
17063
- await saveProfile2(page, opts.saveProfile);
17064
- profile_saved = true;
17065
- } catch {}
17800
+ const headers = Object.keys(rows[0]);
17801
+ const csvLines = [headers.join(",")];
17802
+ for (const row of rows) {
17803
+ csvLines.push(headers.map((h) => {
17804
+ const val = String(row[h] ?? "");
17805
+ return val.includes(",") || val.includes('"') ? `"${val.replace(/"/g, '""')}"` : val;
17806
+ }).join(","));
17066
17807
  }
17067
- return {
17068
- logged_in,
17069
- redirect_url: currentUrl,
17070
- profile_saved,
17071
- method: "secrets_vault"
17072
- };
17073
- } catch (err) {
17074
- return {
17075
- logged_in: false,
17076
- redirect_url: "",
17077
- profile_saved: false,
17078
- method: "not_found",
17079
- error: err instanceof Error ? err.message : String(err)
17080
- };
17808
+ const content = csvLines.join(`
17809
+ `);
17810
+ writeFileSync3(path, content);
17811
+ return { path, size: content.length };
17812
+ } else {
17813
+ const content = JSON.stringify(dataset.data, null, 2);
17814
+ writeFileSync3(path, content);
17815
+ return { path, size: content.length };
17081
17816
  }
17082
17817
  }
17083
- var init_auth = () => {};
17818
+ var init_datasets = __esm(() => {
17819
+ init_schema();
17820
+ });
17084
17821
 
17085
17822
  // ../open-mementos/dist/index.js
17086
17823
  var exports_dist = {};
@@ -17192,10 +17929,10 @@ __export(exports_dist, {
17192
17929
  DEFAULT_CONFIG: () => DEFAULT_CONFIG
17193
17930
  });
17194
17931
  import { Database as Database2 } from "bun:sqlite";
17195
- import { existsSync as existsSync6, mkdirSync as mkdirSync9 } from "fs";
17196
- import { dirname, join as join10, resolve } from "path";
17197
- import { existsSync as existsSync22, mkdirSync as mkdirSync22, readFileSync as readFileSync4, readdirSync as readdirSync4, writeFileSync as writeFileSync3, unlinkSync as unlinkSync3 } from "fs";
17198
- import { homedir as homedir10 } from "os";
17932
+ import { existsSync as existsSync7, mkdirSync as mkdirSync10 } from "fs";
17933
+ import { dirname, join as join12, resolve } from "path";
17934
+ import { existsSync as existsSync22, mkdirSync as mkdirSync22, readFileSync as readFileSync5, readdirSync as readdirSync4, writeFileSync as writeFileSync4, unlinkSync as unlinkSync3 } from "fs";
17935
+ import { homedir as homedir12 } from "os";
17199
17936
  import { basename as basename2, dirname as dirname2, join as join22, resolve as resolve2 } from "path";
17200
17937
  import { existsSync as existsSync32, mkdirSync as mkdirSync32, readFileSync as readFileSync22, writeFileSync as writeFileSync22 } from "fs";
17201
17938
  import { homedir as homedir22 } from "os";
@@ -17209,8 +17946,8 @@ function isInMemoryDb(path) {
17209
17946
  function findNearestMementosDb(startDir) {
17210
17947
  let dir = resolve(startDir);
17211
17948
  while (true) {
17212
- const candidate = join10(dir, ".mementos", "mementos.db");
17213
- if (existsSync6(candidate))
17949
+ const candidate = join12(dir, ".mementos", "mementos.db");
17950
+ if (existsSync7(candidate))
17214
17951
  return candidate;
17215
17952
  const parent = dirname(dir);
17216
17953
  if (parent === dir)
@@ -17222,7 +17959,7 @@ function findNearestMementosDb(startDir) {
17222
17959
  function findGitRoot(startDir) {
17223
17960
  let dir = resolve(startDir);
17224
17961
  while (true) {
17225
- if (existsSync6(join10(dir, ".git")))
17962
+ if (existsSync7(join12(dir, ".git")))
17226
17963
  return dir;
17227
17964
  const parent = dirname(dir);
17228
17965
  if (parent === dir)
@@ -17242,18 +17979,18 @@ function getDbPath() {
17242
17979
  if (process.env["MEMENTOS_DB_SCOPE"] === "project") {
17243
17980
  const gitRoot = findGitRoot(cwd);
17244
17981
  if (gitRoot) {
17245
- return join10(gitRoot, ".mementos", "mementos.db");
17982
+ return join12(gitRoot, ".mementos", "mementos.db");
17246
17983
  }
17247
17984
  }
17248
17985
  const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
17249
- return join10(home, ".mementos", "mementos.db");
17986
+ return join12(home, ".mementos", "mementos.db");
17250
17987
  }
17251
17988
  function ensureDir2(filePath) {
17252
17989
  if (isInMemoryDb(filePath))
17253
17990
  return;
17254
17991
  const dir = dirname(resolve(filePath));
17255
- if (!existsSync6(dir)) {
17256
- mkdirSync9(dir, { recursive: true });
17992
+ if (!existsSync7(dir)) {
17993
+ mkdirSync10(dir, { recursive: true });
17257
17994
  }
17258
17995
  }
17259
17996
  function getDatabase2(dbPath) {
@@ -19103,11 +19840,11 @@ function isValidCategory(value) {
19103
19840
  return VALID_CATEGORIES.includes(value);
19104
19841
  }
19105
19842
  function loadConfig() {
19106
- const configPath = join22(homedir10(), ".mementos", "config.json");
19843
+ const configPath = join22(homedir12(), ".mementos", "config.json");
19107
19844
  let fileConfig = {};
19108
19845
  if (existsSync22(configPath)) {
19109
19846
  try {
19110
- const raw = readFileSync4(configPath, "utf-8");
19847
+ const raw = readFileSync5(configPath, "utf-8");
19111
19848
  fileConfig = JSON.parse(raw);
19112
19849
  } catch {}
19113
19850
  }
@@ -19130,17 +19867,17 @@ function loadConfig() {
19130
19867
  return merged;
19131
19868
  }
19132
19869
  function profilesDir() {
19133
- return join22(homedir10(), ".mementos", "profiles");
19870
+ return join22(homedir12(), ".mementos", "profiles");
19134
19871
  }
19135
19872
  function globalConfigPath() {
19136
- return join22(homedir10(), ".mementos", "config.json");
19873
+ return join22(homedir12(), ".mementos", "config.json");
19137
19874
  }
19138
19875
  function readGlobalConfig() {
19139
19876
  const p = globalConfigPath();
19140
19877
  if (!existsSync22(p))
19141
19878
  return {};
19142
19879
  try {
19143
- return JSON.parse(readFileSync4(p, "utf-8"));
19880
+ return JSON.parse(readFileSync5(p, "utf-8"));
19144
19881
  } catch {
19145
19882
  return {};
19146
19883
  }
@@ -19148,7 +19885,7 @@ function readGlobalConfig() {
19148
19885
  function writeGlobalConfig(data) {
19149
19886
  const p = globalConfigPath();
19150
19887
  ensureDir22(dirname2(p));
19151
- writeFileSync3(p, JSON.stringify(data, null, 2), "utf-8");
19888
+ writeFileSync4(p, JSON.stringify(data, null, 2), "utf-8");
19152
19889
  }
19153
19890
  function getActiveProfile() {
19154
19891
  const envProfile = process.env["MEMENTOS_PROFILE"];
@@ -21813,30 +22550,30 @@ __export(exports_dist2, {
21813
22550
  acquireLock: () => acquireLock2
21814
22551
  });
21815
22552
  import { Database as Database3 } from "bun:sqlite";
21816
- import { mkdirSync as mkdirSync10 } from "fs";
21817
- import { join as join11, dirname as dirname3 } from "path";
21818
- import { homedir as homedir11 } from "os";
21819
- import { randomUUID as randomUUID11 } from "crypto";
22553
+ import { mkdirSync as mkdirSync11 } from "fs";
22554
+ import { join as join13, dirname as dirname3 } from "path";
22555
+ import { homedir as homedir13 } from "os";
22556
+ import { randomUUID as randomUUID14 } from "crypto";
21820
22557
  import { mkdirSync as mkdirSync23, copyFileSync as copyFileSync3, statSync as statSync2 } from "fs";
21821
22558
  import { join as join33 } from "path";
21822
22559
  import { homedir as homedir33 } from "os";
21823
- import { readFileSync as readFileSync5 } from "fs";
22560
+ import { readFileSync as readFileSync6 } from "fs";
21824
22561
  import { join as join23 } from "path";
21825
22562
  import { homedir as homedir23 } from "os";
21826
22563
  import { randomUUID as randomUUID22 } from "crypto";
21827
- import { readFileSync as readFileSync23, writeFileSync as writeFileSync4, mkdirSync as mkdirSync33 } from "fs";
22564
+ import { readFileSync as readFileSync23, writeFileSync as writeFileSync5, mkdirSync as mkdirSync33 } from "fs";
21828
22565
  import { join as join43, dirname as dirname22 } from "path";
21829
22566
  import { homedir as homedir42 } from "os";
21830
22567
  function getDbPath2() {
21831
22568
  if (process.env.CONVERSATIONS_DB_PATH)
21832
22569
  return process.env.CONVERSATIONS_DB_PATH;
21833
- return join11(homedir11(), ".conversations", "messages.db");
22570
+ return join13(homedir13(), ".conversations", "messages.db");
21834
22571
  }
21835
22572
  function getDb() {
21836
22573
  if (db)
21837
22574
  return db;
21838
22575
  const dbPath = getDbPath2();
21839
- mkdirSync10(dirname3(dbPath), { recursive: true });
22576
+ mkdirSync11(dirname3(dbPath), { recursive: true });
21840
22577
  db = new Database3(dbPath, { create: true });
21841
22578
  db.exec("PRAGMA journal_mode = WAL");
21842
22579
  db.exec("PRAGMA busy_timeout = 5000");
@@ -22078,7 +22815,7 @@ function loadConfig2() {
22078
22815
  if (cachedConfig && now2 - configLoadedAt < CONFIG_CACHE_MS)
22079
22816
  return cachedConfig;
22080
22817
  try {
22081
- const raw = readFileSync5(getConfigPath(), "utf-8");
22818
+ const raw = readFileSync6(getConfigPath(), "utf-8");
22082
22819
  cachedConfig = JSON.parse(raw);
22083
22820
  configLoadedAt = now2;
22084
22821
  return cachedConfig;
@@ -22196,7 +22933,7 @@ function guessMimeType(name) {
22196
22933
  function sendMessage(opts) {
22197
22934
  const db2 = getDb();
22198
22935
  const explicitSession = opts.session_id && opts.session_id.trim().length > 0 ? opts.session_id : undefined;
22199
- const sessionId = explicitSession ?? (opts.space ? `space:${opts.space}` : `${[opts.from, opts.to].sort().join("-")}-${randomUUID11().slice(0, 8)}`);
22936
+ const sessionId = explicitSession ?? (opts.space ? `space:${opts.space}` : `${[opts.from, opts.to].sort().join("-")}-${randomUUID14().slice(0, 8)}`);
22200
22937
  const metadata = opts.metadata ? JSON.stringify(opts.metadata) : null;
22201
22938
  const normalizedPriority = opts.priority === "low" || opts.priority === "normal" || opts.priority === "high" || opts.priority === "urgent" ? opts.priority : "normal";
22202
22939
  const blocking = opts.blocking ? 1 : 0;
@@ -23024,7 +23761,7 @@ function getAutoName() {
23024
23761
  cachedAutoName = name;
23025
23762
  try {
23026
23763
  mkdirSync33(dirname22(AGENT_ID_FILE), { recursive: true });
23027
- writeFileSync4(AGENT_ID_FILE, name + `
23764
+ writeFileSync5(AGENT_ID_FILE, name + `
23028
23765
  `, "utf-8");
23029
23766
  } catch {}
23030
23767
  return name;
@@ -24979,7 +25716,7 @@ Your code should look like:
24979
25716
  }
24980
25717
  }
24981
25718
  }
24982
- function checkPropTypes(typeSpecs, values, location, componentName, element) {
25719
+ function checkPropTypes(typeSpecs, values, location2, componentName, element) {
24983
25720
  {
24984
25721
  var has = Function.call.bind(hasOwnProperty);
24985
25722
  for (var typeSpecName in typeSpecs) {
@@ -24987,23 +25724,23 @@ Your code should look like:
24987
25724
  var error$1 = undefined;
24988
25725
  try {
24989
25726
  if (typeof typeSpecs[typeSpecName] !== "function") {
24990
- var err = Error((componentName || "React class") + ": " + location + " type `" + typeSpecName + "` is invalid; " + "it must be a function, usually from the `prop-types` package, but received `" + typeof typeSpecs[typeSpecName] + "`." + "This often happens because of typos such as `PropTypes.function` instead of `PropTypes.func`.");
25727
+ var err = Error((componentName || "React class") + ": " + location2 + " type `" + typeSpecName + "` is invalid; " + "it must be a function, usually from the `prop-types` package, but received `" + typeof typeSpecs[typeSpecName] + "`." + "This often happens because of typos such as `PropTypes.function` instead of `PropTypes.func`.");
24991
25728
  err.name = "Invariant Violation";
24992
25729
  throw err;
24993
25730
  }
24994
- error$1 = typeSpecs[typeSpecName](values, typeSpecName, componentName, location, null, "SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED");
25731
+ error$1 = typeSpecs[typeSpecName](values, typeSpecName, componentName, location2, null, "SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED");
24995
25732
  } catch (ex) {
24996
25733
  error$1 = ex;
24997
25734
  }
24998
25735
  if (error$1 && !(error$1 instanceof Error)) {
24999
25736
  setCurrentlyValidatingElement(element);
25000
- error("%s: type specification of %s" + " `%s` is invalid; the type checker " + "function must return `null` or an `Error` but returned a %s. " + "You may have forgotten to pass an argument to the type checker " + "creator (arrayOf, instanceOf, objectOf, oneOf, oneOfType, and " + "shape all require an argument).", componentName || "React class", location, typeSpecName, typeof error$1);
25737
+ error("%s: type specification of %s" + " `%s` is invalid; the type checker " + "function must return `null` or an `Error` but returned a %s. " + "You may have forgotten to pass an argument to the type checker " + "creator (arrayOf, instanceOf, objectOf, oneOf, oneOfType, and " + "shape all require an argument).", componentName || "React class", location2, typeSpecName, typeof error$1);
25001
25738
  setCurrentlyValidatingElement(null);
25002
25739
  }
25003
25740
  if (error$1 instanceof Error && !(error$1.message in loggedTypeFailures)) {
25004
25741
  loggedTypeFailures[error$1.message] = true;
25005
25742
  setCurrentlyValidatingElement(element);
25006
- error("Failed %s type: %s", location, error$1.message);
25743
+ error("Failed %s type: %s", location2, error$1.message);
25007
25744
  setCurrentlyValidatingElement(null);
25008
25745
  }
25009
25746
  }
@@ -26238,11 +26975,11 @@ __export(exports_dist3, {
26238
26975
  AgentNotFoundError: () => AgentNotFoundError2
26239
26976
  });
26240
26977
  import { Database as Database4 } from "bun:sqlite";
26241
- import { existsSync as existsSync7, mkdirSync as mkdirSync11 } from "fs";
26242
- import { dirname as dirname5, join as join12, resolve as resolve3 } from "path";
26978
+ import { existsSync as existsSync8, mkdirSync as mkdirSync12 } from "fs";
26979
+ import { dirname as dirname5, join as join14, resolve as resolve3 } from "path";
26243
26980
  import { existsSync as existsSync33 } from "fs";
26244
26981
  import { join as join34 } from "path";
26245
- import { existsSync as existsSync23, mkdirSync as mkdirSync24, readFileSync as readFileSync6, readdirSync as readdirSync5, statSync as statSync3, writeFileSync as writeFileSync5 } from "fs";
26982
+ import { existsSync as existsSync23, mkdirSync as mkdirSync24, readFileSync as readFileSync7, readdirSync as readdirSync5, statSync as statSync3, writeFileSync as writeFileSync6 } from "fs";
26246
26983
  import { join as join24 } from "path";
26247
26984
  import { existsSync as existsSync43, readFileSync as readFileSync24, readdirSync as readdirSync22, writeFileSync as writeFileSync23 } from "fs";
26248
26985
  import { join as join44 } from "path";
@@ -26472,8 +27209,8 @@ function isInMemoryDb2(path) {
26472
27209
  function findNearestTodosDb(startDir) {
26473
27210
  let dir = resolve3(startDir);
26474
27211
  while (true) {
26475
- const candidate = join12(dir, ".todos", "todos.db");
26476
- if (existsSync7(candidate))
27212
+ const candidate = join14(dir, ".todos", "todos.db");
27213
+ if (existsSync8(candidate))
26477
27214
  return candidate;
26478
27215
  const parent = dirname5(dir);
26479
27216
  if (parent === dir)
@@ -26485,7 +27222,7 @@ function findNearestTodosDb(startDir) {
26485
27222
  function findGitRoot2(startDir) {
26486
27223
  let dir = resolve3(startDir);
26487
27224
  while (true) {
26488
- if (existsSync7(join12(dir, ".git")))
27225
+ if (existsSync8(join14(dir, ".git")))
26489
27226
  return dir;
26490
27227
  const parent = dirname5(dir);
26491
27228
  if (parent === dir)
@@ -26505,18 +27242,18 @@ function getDbPath3() {
26505
27242
  if (process.env["TODOS_DB_SCOPE"] === "project") {
26506
27243
  const gitRoot = findGitRoot2(cwd);
26507
27244
  if (gitRoot) {
26508
- return join12(gitRoot, ".todos", "todos.db");
27245
+ return join14(gitRoot, ".todos", "todos.db");
26509
27246
  }
26510
27247
  }
26511
27248
  const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
26512
- return join12(home, ".todos", "todos.db");
27249
+ return join14(home, ".todos", "todos.db");
26513
27250
  }
26514
27251
  function ensureDir3(filePath) {
26515
27252
  if (isInMemoryDb2(filePath))
26516
27253
  return;
26517
27254
  const dir = dirname5(resolve3(filePath));
26518
- if (!existsSync7(dir)) {
26519
- mkdirSync11(dir, { recursive: true });
27255
+ if (!existsSync8(dir)) {
27256
+ mkdirSync12(dir, { recursive: true });
26520
27257
  }
26521
27258
  }
26522
27259
  function getDatabase3(dbPath) {
@@ -26986,24 +27723,24 @@ function listJsonFiles(dir) {
26986
27723
  }
26987
27724
  function readJsonFile(path) {
26988
27725
  try {
26989
- return JSON.parse(readFileSync6(path, "utf-8"));
27726
+ return JSON.parse(readFileSync7(path, "utf-8"));
26990
27727
  } catch {
26991
27728
  return null;
26992
27729
  }
26993
27730
  }
26994
27731
  function writeJsonFile(path, data) {
26995
- writeFileSync5(path, JSON.stringify(data, null, 2) + `
27732
+ writeFileSync6(path, JSON.stringify(data, null, 2) + `
26996
27733
  `);
26997
27734
  }
26998
27735
  function readHighWaterMark(dir) {
26999
27736
  const path = join24(dir, ".highwatermark");
27000
27737
  if (!existsSync23(path))
27001
27738
  return 1;
27002
- const val = parseInt(readFileSync6(path, "utf-8").trim(), 10);
27739
+ const val = parseInt(readFileSync7(path, "utf-8").trim(), 10);
27003
27740
  return isNaN(val) ? 1 : val;
27004
27741
  }
27005
27742
  function writeHighWaterMark(dir, value) {
27006
- writeFileSync5(join24(dir, ".highwatermark"), String(value));
27743
+ writeFileSync6(join24(dir, ".highwatermark"), String(value));
27007
27744
  }
27008
27745
  function getFileMtimeMs(path) {
27009
27746
  try {
@@ -31593,9 +32330,9 @@ __export(exports_dist4, {
31593
32330
  CATEGORIES: () => CATEGORIES,
31594
32331
  AGENT_TARGETS: () => AGENT_TARGETS
31595
32332
  });
31596
- import { existsSync as existsSync8, cpSync, mkdirSync as mkdirSync12, writeFileSync as writeFileSync6, rmSync as rmSync2, readdirSync as readdirSync6, statSync as statSync4, readFileSync as readFileSync7, accessSync, constants } from "fs";
31597
- import { join as join13, dirname as dirname6 } from "path";
31598
- import { homedir as homedir12 } from "os";
32333
+ import { existsSync as existsSync9, cpSync, mkdirSync as mkdirSync13, writeFileSync as writeFileSync7, rmSync as rmSync2, readdirSync as readdirSync6, statSync as statSync4, readFileSync as readFileSync8, accessSync, constants } from "fs";
32334
+ import { join as join15, dirname as dirname6 } from "path";
32335
+ import { homedir as homedir14 } from "os";
31599
32336
  import { fileURLToPath } from "url";
31600
32337
  import { existsSync as existsSync24, readFileSync as readFileSync25, readdirSync as readdirSync23 } from "fs";
31601
32338
  import { join as join25 } from "path";
@@ -31706,35 +32443,35 @@ function normalizeSkillName(name) {
31706
32443
  function findSkillsDir() {
31707
32444
  let dir = __dirname2;
31708
32445
  for (let i = 0;i < 5; i++) {
31709
- const candidate = join13(dir, "skills");
31710
- if (existsSync8(candidate)) {
32446
+ const candidate = join15(dir, "skills");
32447
+ if (existsSync9(candidate)) {
31711
32448
  return candidate;
31712
32449
  }
31713
32450
  dir = dirname6(dir);
31714
32451
  }
31715
- return join13(__dirname2, "..", "skills");
32452
+ return join15(__dirname2, "..", "skills");
31716
32453
  }
31717
32454
  function getSkillPath(name) {
31718
32455
  const skillName = normalizeSkillName(name);
31719
- return join13(SKILLS_DIR, skillName);
32456
+ return join15(SKILLS_DIR, skillName);
31720
32457
  }
31721
32458
  function skillExists(name) {
31722
- return existsSync8(getSkillPath(name));
32459
+ return existsSync9(getSkillPath(name));
31723
32460
  }
31724
32461
  function installSkill(name, options = {}) {
31725
32462
  const { targetDir = process.cwd(), overwrite = false } = options;
31726
32463
  const skillName = normalizeSkillName(name);
31727
32464
  const sourcePath = getSkillPath(name);
31728
- const destDir = join13(targetDir, ".skills");
31729
- const destPath = join13(destDir, skillName);
31730
- if (!existsSync8(sourcePath)) {
32465
+ const destDir = join15(targetDir, ".skills");
32466
+ const destPath = join15(destDir, skillName);
32467
+ if (!existsSync9(sourcePath)) {
31731
32468
  return {
31732
32469
  skill: name,
31733
32470
  success: false,
31734
32471
  error: `Skill '${name}' not found`
31735
32472
  };
31736
32473
  }
31737
- if (existsSync8(destPath) && !overwrite) {
32474
+ if (existsSync9(destPath) && !overwrite) {
31738
32475
  return {
31739
32476
  skill: name,
31740
32477
  success: false,
@@ -31743,10 +32480,10 @@ function installSkill(name, options = {}) {
31743
32480
  };
31744
32481
  }
31745
32482
  try {
31746
- if (!existsSync8(destDir)) {
31747
- mkdirSync12(destDir, { recursive: true });
32483
+ if (!existsSync9(destDir)) {
32484
+ mkdirSync13(destDir, { recursive: true });
31748
32485
  }
31749
- if (existsSync8(destPath) && overwrite) {
32486
+ if (existsSync9(destPath) && overwrite) {
31750
32487
  rmSync2(destPath, { recursive: true, force: true });
31751
32488
  }
31752
32489
  cpSync(sourcePath, destPath, {
@@ -31785,7 +32522,7 @@ function installSkills(names, options = {}) {
31785
32522
  return names.map((name) => installSkill(name, options));
31786
32523
  }
31787
32524
  function updateSkillsIndex(skillsDir) {
31788
- const indexPath = join13(skillsDir, "index.ts");
32525
+ const indexPath = join15(skillsDir, "index.ts");
31789
32526
  const meta = loadMeta(skillsDir);
31790
32527
  const disabledSet = new Set(meta.disabled || []);
31791
32528
  const skills = readdirSync6(skillsDir).filter((f) => f.startsWith("skill-") && !f.includes(".") && !disabledSet.has(f.replace("skill-", "")));
@@ -31801,31 +32538,31 @@ function updateSkillsIndex(skillsDir) {
31801
32538
 
31802
32539
  ${exports}
31803
32540
  `;
31804
- writeFileSync6(indexPath, content);
32541
+ writeFileSync7(indexPath, content);
31805
32542
  }
31806
32543
  function getMetaPath(skillsDir) {
31807
- return join13(skillsDir, ".meta.json");
32544
+ return join15(skillsDir, ".meta.json");
31808
32545
  }
31809
32546
  function loadMeta(skillsDir) {
31810
32547
  const metaPath2 = getMetaPath(skillsDir);
31811
- if (existsSync8(metaPath2)) {
32548
+ if (existsSync9(metaPath2)) {
31812
32549
  try {
31813
- return JSON.parse(readFileSync7(metaPath2, "utf-8"));
32550
+ return JSON.parse(readFileSync8(metaPath2, "utf-8"));
31814
32551
  } catch {}
31815
32552
  }
31816
32553
  return { skills: {} };
31817
32554
  }
31818
32555
  function saveMeta(skillsDir, meta) {
31819
- writeFileSync6(getMetaPath(skillsDir), JSON.stringify(meta, null, 2));
32556
+ writeFileSync7(getMetaPath(skillsDir), JSON.stringify(meta, null, 2));
31820
32557
  }
31821
32558
  function recordInstall(skillsDir, name) {
31822
32559
  const meta = loadMeta(skillsDir);
31823
32560
  const skillName = normalizeSkillName(name);
31824
32561
  let version = "unknown";
31825
32562
  try {
31826
- const pkgPath = join13(skillsDir, skillName, "package.json");
31827
- if (existsSync8(pkgPath)) {
31828
- const pkg = JSON.parse(readFileSync7(pkgPath, "utf-8"));
32563
+ const pkgPath = join15(skillsDir, skillName, "package.json");
32564
+ if (existsSync9(pkgPath)) {
32565
+ const pkg = JSON.parse(readFileSync8(pkgPath, "utf-8"));
31829
32566
  version = pkg.version || "unknown";
31830
32567
  }
31831
32568
  } catch {}
@@ -31838,12 +32575,12 @@ function recordRemove(skillsDir, name) {
31838
32575
  saveMeta(skillsDir, meta);
31839
32576
  }
31840
32577
  function getInstallMeta(targetDir = process.cwd()) {
31841
- return loadMeta(join13(targetDir, ".skills"));
32578
+ return loadMeta(join15(targetDir, ".skills"));
31842
32579
  }
31843
32580
  function disableSkill(name, targetDir = process.cwd()) {
31844
- const skillsDir = join13(targetDir, ".skills");
32581
+ const skillsDir = join15(targetDir, ".skills");
31845
32582
  const skillName = normalizeSkillName(name);
31846
- if (!existsSync8(join13(skillsDir, skillName)))
32583
+ if (!existsSync9(join15(skillsDir, skillName)))
31847
32584
  return false;
31848
32585
  const meta = loadMeta(skillsDir);
31849
32586
  const disabled = new Set(meta.disabled || []);
@@ -31856,7 +32593,7 @@ function disableSkill(name, targetDir = process.cwd()) {
31856
32593
  return true;
31857
32594
  }
31858
32595
  function enableSkill(name, targetDir = process.cwd()) {
31859
- const skillsDir = join13(targetDir, ".skills");
32596
+ const skillsDir = join15(targetDir, ".skills");
31860
32597
  const meta = loadMeta(skillsDir);
31861
32598
  const disabled = new Set(meta.disabled || []);
31862
32599
  if (!disabled.has(name))
@@ -31868,24 +32605,24 @@ function enableSkill(name, targetDir = process.cwd()) {
31868
32605
  return true;
31869
32606
  }
31870
32607
  function getDisabledSkills(targetDir = process.cwd()) {
31871
- const meta = loadMeta(join13(targetDir, ".skills"));
32608
+ const meta = loadMeta(join15(targetDir, ".skills"));
31872
32609
  return meta.disabled || [];
31873
32610
  }
31874
32611
  function getInstalledSkills(targetDir = process.cwd()) {
31875
- const skillsDir = join13(targetDir, ".skills");
31876
- if (!existsSync8(skillsDir)) {
32612
+ const skillsDir = join15(targetDir, ".skills");
32613
+ if (!existsSync9(skillsDir)) {
31877
32614
  return [];
31878
32615
  }
31879
32616
  return readdirSync6(skillsDir).filter((f) => {
31880
- const fullPath = join13(skillsDir, f);
32617
+ const fullPath = join15(skillsDir, f);
31881
32618
  return f.startsWith("skill-") && statSync4(fullPath).isDirectory();
31882
32619
  }).map((f) => f.replace("skill-", ""));
31883
32620
  }
31884
32621
  function removeSkill(name, targetDir = process.cwd()) {
31885
32622
  const skillName = normalizeSkillName(name);
31886
- const skillsDir = join13(targetDir, ".skills");
31887
- const skillPath = join13(skillsDir, skillName);
31888
- if (!existsSync8(skillPath)) {
32623
+ const skillsDir = join15(targetDir, ".skills");
32624
+ const skillPath = join15(skillsDir, skillName);
32625
+ if (!existsSync9(skillPath)) {
31889
32626
  return false;
31890
32627
  }
31891
32628
  rmSync2(skillPath, { recursive: true, force: true });
@@ -31896,25 +32633,25 @@ function removeSkill(name, targetDir = process.cwd()) {
31896
32633
  function getAgentSkillsDir(agent, scope = "global", projectDir) {
31897
32634
  const agentDir = `.${agent}`;
31898
32635
  if (scope === "project") {
31899
- return join13(projectDir || process.cwd(), agentDir, "skills");
32636
+ return join15(projectDir || process.cwd(), agentDir, "skills");
31900
32637
  }
31901
- return join13(homedir12(), agentDir, "skills");
32638
+ return join15(homedir14(), agentDir, "skills");
31902
32639
  }
31903
32640
  function getAgentSkillPath(name, agent, scope = "global", projectDir) {
31904
32641
  const skillName = normalizeSkillName(name);
31905
- return join13(getAgentSkillsDir(agent, scope, projectDir), skillName);
32642
+ return join15(getAgentSkillsDir(agent, scope, projectDir), skillName);
31906
32643
  }
31907
32644
  function installSkillForAgent(name, options, generateSkillMd) {
31908
32645
  const { agent, scope = "global", projectDir } = options;
31909
32646
  const skillName = normalizeSkillName(name);
31910
32647
  const sourcePath = getSkillPath(name);
31911
- if (!existsSync8(sourcePath)) {
32648
+ if (!existsSync9(sourcePath)) {
31912
32649
  return { skill: name, success: false, error: `Skill '${name}' not found` };
31913
32650
  }
31914
32651
  let skillMdContent = null;
31915
- const skillMdPath = join13(sourcePath, "SKILL.md");
31916
- if (existsSync8(skillMdPath)) {
31917
- skillMdContent = readFileSync7(skillMdPath, "utf-8");
32652
+ const skillMdPath = join15(sourcePath, "SKILL.md");
32653
+ if (existsSync9(skillMdPath)) {
32654
+ skillMdContent = readFileSync8(skillMdPath, "utf-8");
31918
32655
  } else if (generateSkillMd) {
31919
32656
  skillMdContent = generateSkillMd(name);
31920
32657
  }
@@ -31923,8 +32660,8 @@ function installSkillForAgent(name, options, generateSkillMd) {
31923
32660
  }
31924
32661
  const destDir = getAgentSkillPath(name, agent, scope, projectDir);
31925
32662
  if (scope === "global") {
31926
- const agentBaseDir2 = join13(homedir12(), `.${agent}`);
31927
- if (!existsSync8(agentBaseDir2)) {
32663
+ const agentBaseDir2 = join15(homedir14(), `.${agent}`);
32664
+ if (!existsSync9(agentBaseDir2)) {
31928
32665
  const agentLabels = {
31929
32666
  claude: "Claude Code",
31930
32667
  codex: "Codex CLI",
@@ -31947,8 +32684,8 @@ function installSkillForAgent(name, options, generateSkillMd) {
31947
32684
  }
31948
32685
  }
31949
32686
  try {
31950
- mkdirSync12(destDir, { recursive: true });
31951
- writeFileSync6(join13(destDir, "SKILL.md"), skillMdContent);
32687
+ mkdirSync13(destDir, { recursive: true });
32688
+ writeFileSync7(join15(destDir, "SKILL.md"), skillMdContent);
31952
32689
  return { skill: name, success: true, path: destDir };
31953
32690
  } catch (error) {
31954
32691
  return {
@@ -31961,7 +32698,7 @@ function installSkillForAgent(name, options, generateSkillMd) {
31961
32698
  function removeSkillForAgent(name, options) {
31962
32699
  const { agent, scope = "global", projectDir } = options;
31963
32700
  const destDir = getAgentSkillPath(name, agent, scope, projectDir);
31964
- if (!existsSync8(destDir)) {
32701
+ if (!existsSync9(destDir)) {
31965
32702
  return false;
31966
32703
  }
31967
32704
  rmSync2(destDir, { recursive: true, force: true });
@@ -33885,7 +34622,7 @@ __export(exports_cron_manager, {
33885
34622
  deleteCronJob: () => deleteCronJob,
33886
34623
  createCronJob: () => createCronJob
33887
34624
  });
33888
- import { randomUUID as randomUUID12 } from "crypto";
34625
+ import { randomUUID as randomUUID15 } from "crypto";
33889
34626
  function ensureCronTable() {
33890
34627
  const db2 = getDatabase();
33891
34628
  db2.exec(`
@@ -33914,7 +34651,7 @@ function ensureCronTable() {
33914
34651
  function createCronJob(schedule, task, name) {
33915
34652
  ensureCronTable();
33916
34653
  const db2 = getDatabase();
33917
- const id = randomUUID12();
34654
+ const id = randomUUID15();
33918
34655
  db2.prepare(`
33919
34656
  INSERT INTO cron_jobs (id, name, schedule, task_json, enabled)
33920
34657
  VALUES (?, ?, ?, ?, 1)
@@ -33972,7 +34709,7 @@ function getCronEvents(jobId, limit = 10) {
33972
34709
  async function executeCronJob(job) {
33973
34710
  ensureCronTable();
33974
34711
  const db2 = getDatabase();
33975
- const eventId = randomUUID12();
34712
+ const eventId = randomUUID15();
33976
34713
  const startedAt = new Date().toISOString();
33977
34714
  db2.prepare("INSERT INTO cron_events (id, job_id, started_at) VALUES (?, ?, ?)").run(eventId, job.id, startedAt);
33978
34715
  try {
@@ -34063,7 +34800,7 @@ __export(exports_url_watcher, {
34063
34800
  deleteWatchJob: () => deleteWatchJob,
34064
34801
  createWatchJob: () => createWatchJob
34065
34802
  });
34066
- import { randomUUID as randomUUID13 } from "crypto";
34803
+ import { randomUUID as randomUUID16 } from "crypto";
34067
34804
  import { createHash } from "crypto";
34068
34805
  function ensureWatchTables() {
34069
34806
  const db2 = getDatabase();
@@ -34095,7 +34832,7 @@ function ensureWatchTables() {
34095
34832
  function createWatchJob(url, schedule, opts) {
34096
34833
  ensureWatchTables();
34097
34834
  const db2 = getDatabase();
34098
- const id = randomUUID13();
34835
+ const id = randomUUID16();
34099
34836
  db2.prepare(`
34100
34837
  INSERT INTO watch_jobs (id, name, url, schedule, selector, extract_schema, enabled)
34101
34838
  VALUES (?, ?, ?, ?, ?, ?, 1)
@@ -34131,7 +34868,7 @@ function getWatchEvents(watchId, limit = 20) {
34131
34868
  async function checkWatchJob(job) {
34132
34869
  ensureWatchTables();
34133
34870
  const db2 = getDatabase();
34134
- const eventId = randomUUID13();
34871
+ const eventId = randomUUID16();
34135
34872
  const checkedAt = new Date().toISOString();
34136
34873
  let newContent = "";
34137
34874
  try {
@@ -34306,8 +35043,8 @@ var init_ai_task = () => {};
34306
35043
  var exports_mcp = {};
34307
35044
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
34308
35045
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
34309
- import { readFileSync as readFileSync8 } from "fs";
34310
- import { join as join14 } from "path";
35046
+ import { readFileSync as readFileSync9 } from "fs";
35047
+ import { join as join16 } from "path";
34311
35048
  function json(data) {
34312
35049
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
34313
35050
  }
@@ -34372,7 +35109,7 @@ var init_mcp = __esm(async () => {
34372
35109
  init_dialogs();
34373
35110
  init_profiles();
34374
35111
  init_types();
34375
- _pkg = JSON.parse(readFileSync8(join14(import.meta.dir, "../../package.json"), "utf8"));
35112
+ _pkg = JSON.parse(readFileSync9(join16(import.meta.dir, "../../package.json"), "utf8"));
34376
35113
  networkLogCleanup = new Map;
34377
35114
  consoleCaptureCleanup = new Map;
34378
35115
  harCaptures = new Map;
@@ -34451,6 +35188,25 @@ var init_mcp = __esm(async () => {
34451
35188
  return err(e);
34452
35189
  }
34453
35190
  });
35191
+ server.tool("browser_session_fork", "Fork a session: create a new session with the same auth state (cookies, storage) and URL as an existing one. Like git branch for browser sessions.", { source_session_id: exports_external.string(), name: exports_external.string().optional() }, async ({ source_session_id, name }) => {
35192
+ try {
35193
+ const sourcePage = getSessionPage(source_session_id);
35194
+ const sourceUrl = sourcePage.url();
35195
+ const tempName = `_fork_${Date.now()}`;
35196
+ const { saveStateFromPage: saveStateFromPage2 } = await Promise.resolve().then(() => (init_storage_state(), exports_storage_state));
35197
+ await saveStateFromPage2(sourcePage, tempName);
35198
+ const { session, page } = await createSession2({
35199
+ storageState: tempName,
35200
+ startUrl: sourceUrl,
35201
+ name: name ?? `fork-of-${source_session_id.slice(0, 8)}`
35202
+ });
35203
+ const { deleteState: deleteState2 } = await Promise.resolve().then(() => (init_storage_state(), exports_storage_state));
35204
+ deleteState2(tempName);
35205
+ return json({ forked_session: session, source_url: sourceUrl });
35206
+ } catch (e) {
35207
+ return err(e);
35208
+ }
35209
+ });
34454
35210
  server.tool("browser_session_timeline", "Get chronological action log for a session", { session_id: exports_external.string().optional(), limit: exports_external.number().optional().default(50) }, async ({ session_id, limit }) => {
34455
35211
  try {
34456
35212
  const sid = resolveSessionId(session_id);
@@ -35044,6 +35800,52 @@ var init_mcp = __esm(async () => {
35044
35800
  return err(e);
35045
35801
  }
35046
35802
  });
35803
+ server.tool("browser_intercept_response", "Intercept and modify API responses for testing. Mock data, simulate errors, add latency.", {
35804
+ session_id: exports_external.string().optional(),
35805
+ url_pattern: exports_external.string().describe("URL pattern to intercept (e.g. '**/api/users*')"),
35806
+ action: exports_external.enum(["mock", "delay", "error"]).describe("What to do with matched requests"),
35807
+ mock_body: exports_external.string().optional().describe("Response body for mock action"),
35808
+ mock_content_type: exports_external.string().optional().default("application/json"),
35809
+ status_code: exports_external.number().optional().default(200).describe("HTTP status code (for mock/error)"),
35810
+ delay_ms: exports_external.number().optional().default(3000).describe("Delay in ms (for delay action)")
35811
+ }, async ({ session_id, url_pattern, action, mock_body, mock_content_type, status_code, delay_ms }) => {
35812
+ try {
35813
+ const sid = resolveSessionId(session_id);
35814
+ const page = getSessionPage(sid);
35815
+ await page.route(url_pattern, async (route) => {
35816
+ if (action === "mock") {
35817
+ await route.fulfill({
35818
+ status: status_code,
35819
+ contentType: mock_content_type,
35820
+ body: mock_body ?? "{}"
35821
+ });
35822
+ } else if (action === "error") {
35823
+ await route.fulfill({
35824
+ status: status_code ?? 500,
35825
+ contentType: "application/json",
35826
+ body: JSON.stringify({ error: "Intercepted error", status: status_code })
35827
+ });
35828
+ } else if (action === "delay") {
35829
+ await new Promise((r) => setTimeout(r, delay_ms));
35830
+ await route.continue();
35831
+ }
35832
+ });
35833
+ logEvent(sid, "intercept_set", { url_pattern, action, status_code });
35834
+ return json({ intercepted: true, url_pattern, action });
35835
+ } catch (e) {
35836
+ return err(e);
35837
+ }
35838
+ });
35839
+ server.tool("browser_intercept_clear", "Remove all response intercepts from a session", { session_id: exports_external.string().optional() }, async ({ session_id }) => {
35840
+ try {
35841
+ const sid = resolveSessionId(session_id);
35842
+ const page = getSessionPage(sid);
35843
+ await page.unrouteAll({ behavior: "ignoreErrors" });
35844
+ return json({ cleared: true });
35845
+ } catch (e) {
35846
+ return err(e);
35847
+ }
35848
+ });
35047
35849
  server.tool("browser_performance", "Get performance metrics for the current page", { session_id: exports_external.string().optional() }, async ({ session_id }) => {
35048
35850
  try {
35049
35851
  const sid = resolveSessionId(session_id);
@@ -35054,6 +35856,72 @@ var init_mcp = __esm(async () => {
35054
35856
  return err(e);
35055
35857
  }
35056
35858
  });
35859
+ server.tool("browser_detect_env", "Detect if the current page is running in production, development, staging, or local environment. Analyzes URL, meta tags, source maps, analytics SDKs, and more.", { session_id: exports_external.string().optional() }, async ({ session_id }) => {
35860
+ try {
35861
+ const sid = resolveSessionId(session_id);
35862
+ const page = getSessionPage(sid);
35863
+ const { detectEnvironment: detectEnvironment2 } = await Promise.resolve().then(() => exports_env_detector);
35864
+ const result = await detectEnvironment2(page);
35865
+ return json(result);
35866
+ } catch (e) {
35867
+ return err(e);
35868
+ }
35869
+ });
35870
+ server.tool("browser_performance_deep", "Deep performance analysis: Web Vitals, resource breakdown by type, largest resources, third-party scripts with categories, DOM complexity, memory usage.", { session_id: exports_external.string().optional() }, async ({ session_id }) => {
35871
+ try {
35872
+ const sid = resolveSessionId(session_id);
35873
+ const page = getSessionPage(sid);
35874
+ const { getDeepPerformance: getDeepPerformance2 } = await Promise.resolve().then(() => (init_deep_performance(), exports_deep_performance));
35875
+ const result = await getDeepPerformance2(page);
35876
+ return json(result);
35877
+ } catch (e) {
35878
+ return err(e);
35879
+ }
35880
+ });
35881
+ server.tool("browser_accessibility_audit", "Run accessibility audit on the page. Injects axe-core and returns violations grouped by severity (critical, serious, moderate, minor).", { session_id: exports_external.string().optional(), selector: exports_external.string().optional().describe("Scope audit to a specific element") }, async ({ session_id, selector }) => {
35882
+ try {
35883
+ const sid = resolveSessionId(session_id);
35884
+ const page = getSessionPage(sid);
35885
+ await page.evaluate(`
35886
+ if (!window.axe) {
35887
+ const script = document.createElement('script');
35888
+ script.src = 'https://cdnjs.cloudflare.com/ajax/libs/axe-core/4.10.2/axe.min.js';
35889
+ document.head.appendChild(script);
35890
+ await new Promise((resolve, reject) => {
35891
+ script.onload = resolve;
35892
+ script.onerror = reject;
35893
+ });
35894
+ }
35895
+ `);
35896
+ await new Promise((r) => setTimeout(r, 500));
35897
+ const results = await page.evaluate((sel) => {
35898
+ const opts = {};
35899
+ if (sel)
35900
+ opts.include = [sel];
35901
+ return window.axe.run(opts.include ? { include: [sel] } : document).then((r) => ({
35902
+ violations: r.violations.map((v) => ({
35903
+ id: v.id,
35904
+ impact: v.impact,
35905
+ description: v.description,
35906
+ help: v.help,
35907
+ helpUrl: v.helpUrl,
35908
+ nodes_count: v.nodes.length,
35909
+ selectors: v.nodes.slice(0, 3).map((n) => n.target?.[0] ?? "")
35910
+ })),
35911
+ passes: r.passes.length,
35912
+ violations_count: r.violations.length,
35913
+ incomplete: r.incomplete.length
35914
+ }));
35915
+ }, selector);
35916
+ const byImpact = { critical: 0, serious: 0, moderate: 0, minor: 0 };
35917
+ for (const v of results.violations) {
35918
+ byImpact[v.impact] = (byImpact[v.impact] || 0) + 1;
35919
+ }
35920
+ return json({ ...results, by_impact: byImpact, score: Math.max(0, 100 - results.violations_count * 5) });
35921
+ } catch (e) {
35922
+ return err(e);
35923
+ }
35924
+ });
35057
35925
  server.tool("browser_console_log", "Get captured console messages for a session", { session_id: exports_external.string().optional(), level: exports_external.enum(["log", "warn", "error", "debug", "info"]).optional() }, async ({ session_id, level }) => {
35058
35926
  try {
35059
35927
  const sid = resolveSessionId(session_id);
@@ -35117,6 +35985,46 @@ var init_mcp = __esm(async () => {
35117
35985
  return err(e);
35118
35986
  }
35119
35987
  });
35988
+ server.tool("browser_workflow_save", "Save a recording as a reusable workflow with self-healing replay", { recording_id: exports_external.string(), name: exports_external.string(), description: exports_external.string().optional() }, async ({ recording_id, name, description }) => {
35989
+ try {
35990
+ const { saveWorkflowFromRecording: saveWorkflowFromRecording2 } = await Promise.resolve().then(() => (init_workflows(), exports_workflows));
35991
+ return json(saveWorkflowFromRecording2(recording_id, name, description));
35992
+ } catch (e) {
35993
+ return err(e);
35994
+ }
35995
+ });
35996
+ server.tool("browser_workflow_list", "List all saved workflows", {}, async () => {
35997
+ try {
35998
+ const { listWorkflows: listWorkflows2 } = await Promise.resolve().then(() => (init_workflows(), exports_workflows));
35999
+ const workflows = listWorkflows2();
36000
+ return json({ workflows: workflows.map((w) => ({ ...w, steps: `${w.steps.length} steps` })), count: workflows.length });
36001
+ } catch (e) {
36002
+ return err(e);
36003
+ }
36004
+ });
36005
+ server.tool("browser_workflow_run", "Run a saved workflow with self-healing. If selectors changed, auto-adapts and reports what was healed.", { session_id: exports_external.string().optional(), name: exports_external.string() }, async ({ session_id, name }) => {
36006
+ try {
36007
+ const sid = resolveSessionId(session_id);
36008
+ const page = getSessionPage(sid);
36009
+ const { getWorkflowByName: getWorkflowByName2, runWorkflow: runWorkflow2 } = await Promise.resolve().then(() => (init_workflows(), exports_workflows));
36010
+ const workflow = getWorkflowByName2(name);
36011
+ if (!workflow)
36012
+ return err(new Error(`Workflow '${name}' not found`));
36013
+ const result = await runWorkflow2(workflow, page);
36014
+ logEvent(sid, "workflow_run", { name, ...result });
36015
+ return json(result);
36016
+ } catch (e) {
36017
+ return err(e);
36018
+ }
36019
+ });
36020
+ server.tool("browser_workflow_delete", "Delete a saved workflow", { name: exports_external.string() }, async ({ name }) => {
36021
+ try {
36022
+ const { deleteWorkflow: deleteWorkflow2 } = await Promise.resolve().then(() => (init_workflows(), exports_workflows));
36023
+ return json({ deleted: deleteWorkflow2(name) });
36024
+ } catch (e) {
36025
+ return err(e);
36026
+ }
36027
+ });
35120
36028
  server.tool("browser_crawl", "Crawl a URL recursively and return discovered pages", {
35121
36029
  url: exports_external.string(),
35122
36030
  max_depth: exports_external.number().optional().default(2),
@@ -35415,6 +36323,61 @@ var init_mcp = __esm(async () => {
35415
36323
  return err(e);
35416
36324
  }
35417
36325
  });
36326
+ server.tool("browser_wait_for_idle", "Wait until no network requests are in-flight for a specified duration. Essential for SPAs that load data after navigation.", {
36327
+ session_id: exports_external.string().optional(),
36328
+ idle_time: exports_external.number().optional().default(2000).describe("How long (ms) network must be idle to consider page loaded"),
36329
+ timeout: exports_external.number().optional().default(30000).describe("Max wait time (ms) before giving up")
36330
+ }, async ({ session_id, idle_time, timeout }) => {
36331
+ try {
36332
+ const sid = resolveSessionId(session_id);
36333
+ const page = getSessionPage(sid);
36334
+ const t0 = Date.now();
36335
+ let lastActivity = Date.now();
36336
+ let pending = 0;
36337
+ const onRequest = () => {
36338
+ pending++;
36339
+ lastActivity = Date.now();
36340
+ };
36341
+ const onResponse = () => {
36342
+ pending = Math.max(0, pending - 1);
36343
+ if (pending === 0)
36344
+ lastActivity = Date.now();
36345
+ };
36346
+ const onFailed = () => {
36347
+ pending = Math.max(0, pending - 1);
36348
+ if (pending === 0)
36349
+ lastActivity = Date.now();
36350
+ };
36351
+ page.on("request", onRequest);
36352
+ page.on("response", onResponse);
36353
+ page.on("requestfailed", onFailed);
36354
+ try {
36355
+ await new Promise((resolve4, reject) => {
36356
+ const check = () => {
36357
+ const now3 = Date.now();
36358
+ if (now3 - t0 > timeout) {
36359
+ reject(new Error(`Timeout after ${timeout}ms (${pending} requests still pending)`));
36360
+ return;
36361
+ }
36362
+ if (pending === 0 && now3 - lastActivity >= idle_time) {
36363
+ resolve4();
36364
+ return;
36365
+ }
36366
+ setTimeout(check, 100);
36367
+ };
36368
+ check();
36369
+ });
36370
+ } finally {
36371
+ page.removeListener("request", onRequest);
36372
+ page.removeListener("response", onResponse);
36373
+ page.removeListener("requestfailed", onFailed);
36374
+ }
36375
+ const waited_ms = Date.now() - t0;
36376
+ return json({ idle: true, waited_ms, pending_requests: 0 });
36377
+ } catch (e) {
36378
+ return err(e);
36379
+ }
36380
+ });
35418
36381
  server.tool("browser_wait_for_text", "Wait until specific text appears on the page", { session_id: exports_external.string().optional(), text: exports_external.string(), timeout: exports_external.number().optional().default(1e4), exact: exports_external.boolean().optional().default(false) }, async ({ session_id, text, timeout, exact }) => {
35419
36382
  try {
35420
36383
  const sid = resolveSessionId(session_id);
@@ -35812,6 +36775,68 @@ var init_mcp = __esm(async () => {
35812
36775
  return err(e);
35813
36776
  }
35814
36777
  });
36778
+ server.tool("browser_detect_apis", "Scan network traffic for JSON API endpoints. Returns discovered endpoints with methods, status codes, and URLs.", { session_id: exports_external.string().optional() }, async ({ session_id }) => {
36779
+ try {
36780
+ const sid = resolveSessionId(session_id);
36781
+ const { detectAPIs: detectAPIs2 } = await Promise.resolve().then(() => (init_api_detector(), exports_api_detector));
36782
+ const apis = detectAPIs2(sid);
36783
+ return json({ apis, count: apis.length });
36784
+ } catch (e) {
36785
+ return err(e);
36786
+ }
36787
+ });
36788
+ server.tool("browser_extract_structured", "Extract structured data from page: tables, lists, JSON-LD, Open Graph, meta tags, and repeated elements (cards/items).", { session_id: exports_external.string().optional() }, async ({ session_id }) => {
36789
+ try {
36790
+ const sid = resolveSessionId(session_id);
36791
+ const page = getSessionPage(sid);
36792
+ const { extractStructuredData: extractStructuredData2 } = await Promise.resolve().then(() => exports_structured_extract);
36793
+ const data = await extractStructuredData2(page);
36794
+ return json({
36795
+ tables: data.tables.length,
36796
+ lists: data.lists.length,
36797
+ json_ld: data.jsonLd.length,
36798
+ open_graph: Object.keys(data.openGraph).length,
36799
+ meta_tags: Object.keys(data.metaTags).length,
36800
+ repeated_elements: data.repeatedElements.length,
36801
+ data
36802
+ });
36803
+ } catch (e) {
36804
+ return err(e);
36805
+ }
36806
+ });
36807
+ server.tool("browser_dataset_save", "Save extracted data as a named dataset for later use", { name: exports_external.string(), data: exports_external.array(exports_external.record(exports_external.unknown())), source_url: exports_external.string().optional() }, async ({ name, data, source_url }) => {
36808
+ try {
36809
+ const { saveDataset: saveDataset2 } = await Promise.resolve().then(() => (init_datasets(), exports_datasets));
36810
+ const dataset = saveDataset2({ name, rows: data, sourceUrl: source_url });
36811
+ return json({ id: dataset.id, name: dataset.name, row_count: dataset.row_count });
36812
+ } catch (e) {
36813
+ return err(e);
36814
+ }
36815
+ });
36816
+ server.tool("browser_dataset_list", "List all saved datasets", {}, async () => {
36817
+ try {
36818
+ const { listDatasets: listDatasets2 } = await Promise.resolve().then(() => (init_datasets(), exports_datasets));
36819
+ return json({ datasets: listDatasets2() });
36820
+ } catch (e) {
36821
+ return err(e);
36822
+ }
36823
+ });
36824
+ server.tool("browser_dataset_export", "Export a dataset as JSON or CSV file", { name: exports_external.string(), format: exports_external.enum(["json", "csv"]).optional().default("json") }, async ({ name, format }) => {
36825
+ try {
36826
+ const { exportDataset: exportDataset2 } = await Promise.resolve().then(() => (init_datasets(), exports_datasets));
36827
+ return json(exportDataset2(name, format));
36828
+ } catch (e) {
36829
+ return err(e);
36830
+ }
36831
+ });
36832
+ server.tool("browser_dataset_delete", "Delete a saved dataset", { name: exports_external.string() }, async ({ name }) => {
36833
+ try {
36834
+ const { deleteDataset: deleteDataset2 } = await Promise.resolve().then(() => (init_datasets(), exports_datasets));
36835
+ return json({ deleted: deleteDataset2(name) });
36836
+ } catch (e) {
36837
+ return err(e);
36838
+ }
36839
+ });
35815
36840
  server.tool("browser_help", "Show all available browser tools grouped by category with one-line descriptions", {}, async () => {
35816
36841
  try {
35817
36842
  const groups = {
@@ -35820,7 +36845,8 @@ var init_mcp = __esm(async () => {
35820
36845
  { tool: "browser_back", description: "Navigate back in history" },
35821
36846
  { tool: "browser_forward", description: "Navigate forward in history" },
35822
36847
  { tool: "browser_reload", description: "Reload the current page" },
35823
- { tool: "browser_wait_for_navigation", description: "Wait for URL change after action" }
36848
+ { tool: "browser_wait_for_navigation", description: "Wait for URL change after action" },
36849
+ { tool: "browser_wait_for_idle", description: "Wait for network idle (no pending requests)" }
35824
36850
  ],
35825
36851
  Interaction: [
35826
36852
  { tool: "browser_click", description: "Click element by ref or selector" },
@@ -35873,7 +36899,9 @@ var init_mcp = __esm(async () => {
35873
36899
  { tool: "browser_network_log", description: "Get captured network requests" },
35874
36900
  { tool: "browser_network_intercept", description: "Add a network interception rule" },
35875
36901
  { tool: "browser_har_start", description: "Start HAR capture" },
35876
- { tool: "browser_har_stop", description: "Stop HAR capture and get data" }
36902
+ { tool: "browser_har_stop", description: "Stop HAR capture and get data" },
36903
+ { tool: "browser_intercept_response", description: "Mock/delay/error API responses for testing" },
36904
+ { tool: "browser_intercept_clear", description: "Remove all response intercepts" }
35877
36905
  ],
35878
36906
  Performance: [
35879
36907
  { tool: "browser_performance", description: "Get performance metrics" }
@@ -35898,6 +36926,20 @@ var init_mcp = __esm(async () => {
35898
36926
  { tool: "browser_auth_list", description: "List all saved auth flows" },
35899
36927
  { tool: "browser_auth_delete", description: "Delete a saved auth flow" }
35900
36928
  ],
36929
+ Workflows: [
36930
+ { tool: "browser_workflow_save", description: "Save a recording as a reusable workflow" },
36931
+ { tool: "browser_workflow_list", description: "List all saved workflows" },
36932
+ { tool: "browser_workflow_run", description: "Run a workflow with self-healing replay" },
36933
+ { tool: "browser_workflow_delete", description: "Delete a saved workflow" }
36934
+ ],
36935
+ Data: [
36936
+ { tool: "browser_extract_structured", description: "Extract tables, lists, JSON-LD, Open Graph, meta tags, repeated elements" },
36937
+ { tool: "browser_detect_apis", description: "Scan network traffic for JSON API endpoints" },
36938
+ { tool: "browser_dataset_save", description: "Save extracted data as a named dataset" },
36939
+ { tool: "browser_dataset_list", description: "List all saved datasets" },
36940
+ { tool: "browser_dataset_export", description: "Export dataset as JSON or CSV" },
36941
+ { tool: "browser_dataset_delete", description: "Delete a saved dataset" }
36942
+ ],
35901
36943
  Crawl: [
35902
36944
  { tool: "browser_crawl", description: "Crawl a URL recursively" }
35903
36945
  ],
@@ -35942,6 +36984,7 @@ var init_mcp = __esm(async () => {
35942
36984
  { tool: "browser_session_untag", description: "Remove a tag from a session" },
35943
36985
  { tool: "browser_session_stats", description: "Get session stats and token usage" },
35944
36986
  { tool: "browser_session_timeline", description: "Get chronological action log" },
36987
+ { tool: "browser_session_fork", description: "Fork a session (same auth state + URL)" },
35945
36988
  { tool: "browser_tab_new", description: "Open a new tab" },
35946
36989
  { tool: "browser_tab_list", description: "List all open tabs" },
35947
36990
  { tool: "browser_tab_switch", description: "Switch to a tab by index" },
@@ -35951,6 +36994,9 @@ var init_mcp = __esm(async () => {
35951
36994
  { tool: "browser_check", description: "RECOMMENDED: One-call page summary with diagnostics" },
35952
36995
  { tool: "browser_version", description: "Show running binary version and tool count" },
35953
36996
  { tool: "browser_help", description: "Show this help (all tools)" },
36997
+ { tool: "browser_detect_env", description: "Detect environment (prod/dev/staging/local)" },
36998
+ { tool: "browser_performance_deep", description: "Deep performance: resources, third-party, DOM, memory" },
36999
+ { tool: "browser_accessibility_audit", description: "Run axe-core accessibility audit with severity breakdown" },
35954
37000
  { tool: "browser_snapshot_diff", description: "Diff current snapshot vs previous" },
35955
37001
  { tool: "browser_watch_start", description: "Watch page for DOM changes" },
35956
37002
  { tool: "browser_watch_get_changes", description: "Get captured DOM changes" },
@@ -36468,10 +37514,10 @@ __export(exports_snapshots, {
36468
37514
  deleteSnapshot: () => deleteSnapshot,
36469
37515
  createSnapshot: () => createSnapshot
36470
37516
  });
36471
- import { randomUUID as randomUUID14 } from "crypto";
37517
+ import { randomUUID as randomUUID17 } from "crypto";
36472
37518
  function createSnapshot(data) {
36473
37519
  const db2 = getDatabase();
36474
- const id = randomUUID14();
37520
+ const id = randomUUID17();
36475
37521
  db2.prepare("INSERT INTO snapshots (id, session_id, url, title, html, screenshot_path) VALUES (?, ?, ?, ?, ?, ?)").run(id, data.session_id, data.url, data.title ?? null, data.html ?? null, data.screenshot_path ?? null);
36476
37522
  return getSnapshot(id);
36477
37523
  }
@@ -36497,8 +37543,8 @@ var init_snapshots = __esm(() => {
36497
37543
 
36498
37544
  // src/server/index.ts
36499
37545
  var exports_server = {};
36500
- import { join as join15 } from "path";
36501
- import { existsSync as existsSync9 } from "fs";
37546
+ import { join as join17 } from "path";
37547
+ import { existsSync as existsSync10 } from "fs";
36502
37548
  function ok(data, status = 200) {
36503
37549
  return new Response(JSON.stringify(data), {
36504
37550
  status,
@@ -36524,7 +37570,7 @@ function serverError(e) {
36524
37570
  headers: { "Content-Type": "application/json", ...CORS_HEADERS }
36525
37571
  });
36526
37572
  }
36527
- var PORT, CORS_HEADERS, networkCleanup, consoleCleanup, harCaptures2, server2;
37573
+ var PORT, startTime, CORS_HEADERS, networkCleanup, consoleCleanup, harCaptures2, server2;
36528
37574
  var init_server = __esm(() => {
36529
37575
  init_session();
36530
37576
  init_actions();
@@ -36543,6 +37589,7 @@ var init_server = __esm(() => {
36543
37589
  init_downloads();
36544
37590
  init_gallery_diff();
36545
37591
  PORT = parseInt(process.env["BROWSER_SERVER_PORT"] ?? "7030");
37592
+ startTime = Date.now();
36546
37593
  CORS_HEADERS = {
36547
37594
  "Access-Control-Allow-Origin": "*",
36548
37595
  "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
@@ -36561,6 +37608,14 @@ var init_server = __esm(() => {
36561
37608
  return new Response(null, { status: 204, headers: CORS_HEADERS });
36562
37609
  }
36563
37610
  try {
37611
+ if (path === "/health" && method === "GET") {
37612
+ const activeSessions = listSessions2({ status: "active" });
37613
+ return ok({
37614
+ status: "ok",
37615
+ active_sessions: activeSessions.length,
37616
+ uptime_ms: Date.now() - startTime
37617
+ });
37618
+ }
36564
37619
  if (path === "/api/sessions" && method === "GET") {
36565
37620
  const status = url.searchParams.get("status");
36566
37621
  const projectId = url.searchParams.get("project_id") ?? undefined;
@@ -36748,14 +37803,14 @@ var init_server = __esm(() => {
36748
37803
  if (path.match(/^\/api\/gallery\/([^/]+)\/thumbnail$/) && method === "GET") {
36749
37804
  const id = path.split("/")[3];
36750
37805
  const entry = getEntry(id);
36751
- if (!entry?.thumbnail_path || !existsSync9(entry.thumbnail_path))
37806
+ if (!entry?.thumbnail_path || !existsSync10(entry.thumbnail_path))
36752
37807
  return notFound("Thumbnail not found");
36753
37808
  return new Response(Bun.file(entry.thumbnail_path), { headers: { ...CORS_HEADERS } });
36754
37809
  }
36755
37810
  if (path.match(/^\/api\/gallery\/([^/]+)\/image$/) && method === "GET") {
36756
37811
  const id = path.split("/")[3];
36757
37812
  const entry = getEntry(id);
36758
- if (!entry?.path || !existsSync9(entry.path))
37813
+ if (!entry?.path || !existsSync10(entry.path))
36759
37814
  return notFound("Image not found");
36760
37815
  return new Response(Bun.file(entry.path), { headers: { ...CORS_HEADERS } });
36761
37816
  }
@@ -36783,7 +37838,7 @@ var init_server = __esm(() => {
36783
37838
  if (path.match(/^\/api\/downloads\/([^/]+)\/raw$/) && method === "GET") {
36784
37839
  const id = path.split("/")[3];
36785
37840
  const file = getDownload(id);
36786
- if (!file || !existsSync9(file.path))
37841
+ if (!file || !existsSync10(file.path))
36787
37842
  return notFound("Download not found");
36788
37843
  return new Response(Bun.file(file.path), { headers: { ...CORS_HEADERS } });
36789
37844
  }
@@ -36791,13 +37846,13 @@ var init_server = __esm(() => {
36791
37846
  const id = path.split("/")[3];
36792
37847
  return ok({ deleted: deleteDownload(id) });
36793
37848
  }
36794
- const dashboardDist = join15(import.meta.dir, "../../dashboard/dist");
36795
- if (existsSync9(dashboardDist)) {
36796
- const filePath = path === "/" ? join15(dashboardDist, "index.html") : join15(dashboardDist, path);
36797
- if (existsSync9(filePath)) {
37849
+ const dashboardDist = join17(import.meta.dir, "../../dashboard/dist");
37850
+ if (existsSync10(dashboardDist)) {
37851
+ const filePath = path === "/" ? join17(dashboardDist, "index.html") : join17(dashboardDist, path);
37852
+ if (existsSync10(filePath)) {
36798
37853
  return new Response(Bun.file(filePath), { headers: CORS_HEADERS });
36799
37854
  }
36800
- return new Response(Bun.file(join15(dashboardDist, "index.html")), { headers: CORS_HEADERS });
37855
+ return new Response(Bun.file(join17(dashboardDist, "index.html")), { headers: CORS_HEADERS });
36801
37856
  }
36802
37857
  if (path === "/" || path === "") {
36803
37858
  return new Response("@hasna/browser REST API running. Dashboard not built.", {
@@ -36839,10 +37894,10 @@ init_projects();
36839
37894
  init_recorder();
36840
37895
  init_recordings();
36841
37896
  init_lightpanda();
36842
- import { readFileSync as readFileSync9 } from "fs";
36843
- import { join as join16 } from "path";
37897
+ import { readFileSync as readFileSync10 } from "fs";
37898
+ import { join as join18 } from "path";
36844
37899
  import chalk from "chalk";
36845
- var pkg = JSON.parse(readFileSync9(join16(import.meta.dir, "../../package.json"), "utf8"));
37900
+ var pkg = JSON.parse(readFileSync10(join18(import.meta.dir, "../../package.json"), "utf8"));
36846
37901
  var program2 = new Command;
36847
37902
  program2.name("browser").description("@hasna/browser \u2014 general-purpose browser agent CLI").version(pkg.version);
36848
37903
  program2.command("navigate <url>").description("Navigate to a URL and optionally take a screenshot").option("--engine <engine>", "Browser engine: playwright|cdp|lightpanda|auto", "auto").option("--screenshot", "Take a screenshot after navigation").option("--extract", "Extract page text after navigation").option("--headed", "Run in headed (visible) mode").option("--json", "Output as JSON").action(async (url, opts) => {
@@ -36903,6 +37958,148 @@ program2.command("check <url>").description("One-liner page health check: naviga
36903
37958
  }
36904
37959
  await closeSession2(session.id);
36905
37960
  });
37961
+ program2.command("audit <url>").description("Full site audit: env detection, performance, errors, APIs, data extraction, screenshot").option("--engine <engine>", "Browser engine", "auto").option("--headed", "Run in headed (visible) mode").option("--json", "Output as JSON").action(async (url, opts) => {
37962
+ const t0 = Date.now();
37963
+ const { session, page } = await createSession2({ engine: opts.engine, headless: !opts.headed, captureNetwork: true, captureConsole: true });
37964
+ if (!opts.json)
37965
+ console.log(chalk.gray(`Auditing: ${url}
37966
+ `));
37967
+ await navigate(page, url);
37968
+ await new Promise((r) => setTimeout(r, 2000));
37969
+ const title = await page.title();
37970
+ const currentUrl = page.url();
37971
+ let env = {};
37972
+ try {
37973
+ const { detectEnvironment: detectEnvironment2 } = await Promise.resolve().then(() => exports_env_detector);
37974
+ env = await detectEnvironment2(page);
37975
+ } catch {}
37976
+ let perf = {};
37977
+ try {
37978
+ const { getDeepPerformance: getDeepPerformance2 } = await Promise.resolve().then(() => (init_deep_performance(), exports_deep_performance));
37979
+ perf = await getDeepPerformance2(page);
37980
+ } catch {}
37981
+ const { getConsoleLog: getConsoleLog2 } = await Promise.resolve().then(() => (init_console_log(), exports_console_log));
37982
+ const errors2 = getConsoleLog2(session.id, "error");
37983
+ let apis = [];
37984
+ try {
37985
+ const { detectAPIs: detectAPIs2 } = await Promise.resolve().then(() => (init_api_detector(), exports_api_detector));
37986
+ apis = detectAPIs2(session.id);
37987
+ } catch {}
37988
+ let structured = {};
37989
+ try {
37990
+ const { extractStructuredData: extractStructuredData2 } = await Promise.resolve().then(() => exports_structured_extract);
37991
+ structured = await extractStructuredData2(page);
37992
+ } catch {}
37993
+ const screenshot = await takeScreenshot(page, { maxWidth: 1280, quality: 75 });
37994
+ const duration = Date.now() - t0;
37995
+ const report = {
37996
+ url: currentUrl,
37997
+ title,
37998
+ duration_ms: duration,
37999
+ environment: env,
38000
+ performance: {
38001
+ fcp_ms: perf.web_vitals?.fcp,
38002
+ ttfb_ms: perf.web_vitals?.ttfb,
38003
+ total_resources: perf.resources?.total_resources,
38004
+ total_transfer_kb: perf.resources ? +(perf.resources.total_transfer_bytes / 1024).toFixed(1) : 0,
38005
+ resource_breakdown: perf.resources?.by_type,
38006
+ third_party_count: perf.third_party?.length ?? 0,
38007
+ third_party: perf.third_party,
38008
+ dom_nodes: perf.dom?.node_count,
38009
+ dom_max_depth: perf.dom?.max_depth,
38010
+ memory_mb: perf.memory?.js_heap_used_mb
38011
+ },
38012
+ errors: { count: errors2.length, sample: errors2.slice(0, 3).map((e) => e.message?.slice(0, 100)) },
38013
+ apis: { count: apis.length, endpoints: apis.map((a) => `${a.method} ${a.url}`) },
38014
+ data: {
38015
+ tables: structured.tables?.length ?? 0,
38016
+ lists: structured.lists?.length ?? 0,
38017
+ json_ld: structured.jsonLd?.length ?? 0,
38018
+ open_graph: Object.keys(structured.openGraph ?? {}).length,
38019
+ repeated_elements: structured.repeatedElements?.length ?? 0
38020
+ },
38021
+ screenshot: screenshot.path
38022
+ };
38023
+ if (opts.json) {
38024
+ console.log(JSON.stringify(report, null, 2));
38025
+ } else {
38026
+ console.log(chalk.bold(`${title}`));
38027
+ console.log(chalk.blue(` ${currentUrl}
38028
+ `));
38029
+ const envColor = env.env === "prod" ? chalk.green : env.env === "staging" ? chalk.yellow : chalk.cyan;
38030
+ console.log(` Environment: ${envColor(env.env ?? "unknown")} (${env.confidence ?? "?"} confidence)`);
38031
+ console.log(` Performance: FCP ${perf.web_vitals?.fcp ? perf.web_vitals.fcp + "ms" : "?"}, TTFB ${perf.web_vitals?.ttfb ? Math.round(perf.web_vitals.ttfb) + "ms" : "?"}`);
38032
+ console.log(` Resources: ${perf.resources?.total_resources ?? "?"} resources (${report.performance.total_transfer_kb} KB)`);
38033
+ console.log(` Third-party: ${perf.third_party?.length ?? 0} scripts`);
38034
+ if (perf.third_party?.length > 0) {
38035
+ perf.third_party.slice(0, 5).forEach((tp) => {
38036
+ console.log(chalk.gray(` ${tp.domain} (${tp.category}, ${(tp.total_bytes / 1024).toFixed(1)}KB)`));
38037
+ });
38038
+ }
38039
+ console.log(` DOM: ${perf.dom?.node_count ?? "?"} nodes, depth ${perf.dom?.max_depth ?? "?"}`);
38040
+ console.log(` Memory: ${perf.memory?.js_heap_used_mb ?? "?"} MB heap`);
38041
+ const errColor = errors2.length > 0 ? chalk.red : chalk.green;
38042
+ console.log(` Errors: ${errColor(errors2.length + " console errors")}`);
38043
+ console.log(` APIs: ${apis.length} JSON endpoints detected`);
38044
+ apis.slice(0, 3).forEach((a) => console.log(chalk.gray(` ${a.method} ${a.url}`)));
38045
+ console.log(` Data: ${report.data.tables} tables, ${report.data.lists} lists, ${report.data.json_ld} JSON-LD, ${report.data.repeated_elements} repeated elements`);
38046
+ console.log(`
38047
+ Screenshot: ${screenshot.path}`);
38048
+ console.log(chalk.gray(` Completed in ${duration}ms`));
38049
+ }
38050
+ await closeSession2(session.id);
38051
+ });
38052
+ program2.command("compare <url1> <url2>").description("Compare two URLs: side-by-side screenshots + pixel diff + text diff").option("--engine <engine>", "Browser engine", "auto").option("--json", "Output as JSON").action(async (url1, url2, opts) => {
38053
+ const [s1, s2] = await Promise.all([
38054
+ createSession2({ engine: opts.engine, headless: true }),
38055
+ createSession2({ engine: opts.engine, headless: true })
38056
+ ]);
38057
+ await Promise.all([
38058
+ navigate(s1.page, url1),
38059
+ navigate(s2.page, url2)
38060
+ ]);
38061
+ const [ss1, ss2, text1, text2] = await Promise.all([
38062
+ takeScreenshot(s1.page, { format: "png" }),
38063
+ takeScreenshot(s2.page, { format: "png" }),
38064
+ getText(s1.page),
38065
+ getText(s2.page)
38066
+ ]);
38067
+ const { diffImages: diffImages2 } = await Promise.resolve().then(() => (init_gallery_diff(), exports_gallery_diff));
38068
+ const diff = await diffImages2(ss1.path, ss2.path);
38069
+ const words1 = text1.split(/\s+/).filter(Boolean);
38070
+ const words2 = text2.split(/\s+/).filter(Boolean);
38071
+ const set1 = new Set(words1);
38072
+ const set2 = new Set(words2);
38073
+ const common = words1.filter((w) => set2.has(w)).length;
38074
+ const textSimilarity = words1.length > 0 ? Math.round(common / Math.max(words1.length, words2.length) * 100) : 0;
38075
+ const result = {
38076
+ url1,
38077
+ url2,
38078
+ screenshot1: ss1.path,
38079
+ screenshot2: ss2.path,
38080
+ diff_image: diff.diff_path,
38081
+ pixel_change_percent: +diff.changed_percent.toFixed(2),
38082
+ text_similarity_percent: textSimilarity,
38083
+ text1_length: text1.length,
38084
+ text2_length: text2.length
38085
+ };
38086
+ if (opts.json) {
38087
+ console.log(JSON.stringify(result, null, 2));
38088
+ } else {
38089
+ console.log(chalk.bold(`URL Comparison:
38090
+ `));
38091
+ console.log(chalk.blue(` 1: ${url1}`));
38092
+ console.log(chalk.blue(` 2: ${url2}
38093
+ `));
38094
+ console.log(` Pixel diff: ${diff.changed_percent > 5 ? chalk.red(result.pixel_change_percent + "%") : chalk.green(result.pixel_change_percent + "%")} changed`);
38095
+ console.log(` Text similarity: ${textSimilarity > 80 ? chalk.green(textSimilarity + "%") : chalk.yellow(textSimilarity + "%")}`);
38096
+ console.log(chalk.gray(`
38097
+ Screenshot 1: ${ss1.path}`));
38098
+ console.log(chalk.gray(` Screenshot 2: ${ss2.path}`));
38099
+ console.log(chalk.gray(` Diff image: ${diff.diff_path}`));
38100
+ }
38101
+ await Promise.all([closeSession2(s1.session.id), closeSession2(s2.session.id)]);
38102
+ });
36906
38103
  program2.command("screenshot <url>").description("Navigate to a URL and take a screenshot").option("--engine <engine>", "Browser engine", "auto").option("--selector <selector>", "CSS selector for element screenshot").option("--full-page", "Capture full page").option("--format <format>", "Image format: png|jpeg|webp", "png").option("--headed", "Run in headed (visible) mode").action(async (url, opts) => {
36907
38104
  const { session, page } = await createSession2({ engine: opts.engine, headless: !opts.headed });
36908
38105
  await navigate(page, url);
@@ -37067,6 +38264,86 @@ program2.command("attach").description("Attach to a running Chrome browser via C
37067
38264
  console.log(chalk.blue(` Page: ${title} (${url})`));
37068
38265
  }
37069
38266
  });
38267
+ program2.command("login <url>").description("Login to a site: detect form, fill credentials from secrets, save auth state").option("--email <email>", "Email to login with").option("--save-as <name>", "Name to save storage state as").option("--engine <engine>", "Browser engine", "auto").option("--headed", "Run in headed (visible) mode").option("--json", "Output as JSON").action(async (url, opts) => {
38268
+ const { session, page } = await createSession2({ engine: opts.engine, headless: !opts.headed });
38269
+ await navigate(page, url);
38270
+ const formInfo = await page.evaluate(() => {
38271
+ const emailInput = document.querySelector('input[type="email"], input[name="email"], input[name="username"], input[autocomplete="email"], input[autocomplete="username"]');
38272
+ const passwordInput = document.querySelector('input[type="password"]');
38273
+ const submitBtn = document.querySelector('button[type="submit"], input[type="submit"], button:has(span)');
38274
+ return {
38275
+ hasEmailInput: !!emailInput,
38276
+ hasPasswordInput: !!passwordInput,
38277
+ hasSubmitButton: !!submitBtn,
38278
+ emailSelector: emailInput ? emailInput.id ? `#${emailInput.id}` : emailInput.name ? `input[name="${emailInput.name}"]` : 'input[type="email"]' : null,
38279
+ passwordSelector: passwordInput ? passwordInput.id ? `#${passwordInput.id}` : 'input[type="password"]' : null,
38280
+ submitSelector: submitBtn ? submitBtn.id ? `#${submitBtn.id}` : 'button[type="submit"]' : null,
38281
+ pageTitle: document.title
38282
+ };
38283
+ });
38284
+ if (!opts.json) {
38285
+ console.log(chalk.gray(`Page: ${formInfo.pageTitle}`));
38286
+ console.log(chalk.gray(` Email input: ${formInfo.hasEmailInput ? "\u2713" : "\u2717"}`));
38287
+ console.log(chalk.gray(` Password input: ${formInfo.hasPasswordInput ? "\u2713" : "\u2717"}`));
38288
+ console.log(chalk.gray(` Submit button: ${formInfo.hasSubmitButton ? "\u2713" : "\u2717"}`));
38289
+ }
38290
+ let email = opts.email;
38291
+ let password;
38292
+ if (!email) {
38293
+ try {
38294
+ const { getCredentials: getCredentials2 } = await Promise.resolve().then(() => (init_auth(), exports_auth));
38295
+ const hostname = new URL(url).hostname;
38296
+ const creds = await getCredentials2(hostname);
38297
+ if (creds) {
38298
+ email = creds.email ?? creds.username;
38299
+ password = creds.password;
38300
+ if (!opts.json)
38301
+ console.log(chalk.blue(` Credentials found for ${hostname}`));
38302
+ }
38303
+ } catch {}
38304
+ }
38305
+ if (email && formInfo.emailSelector) {
38306
+ await page.fill(formInfo.emailSelector, email);
38307
+ if (!opts.json)
38308
+ console.log(chalk.green(` \u2713 Filled email: ${email}`));
38309
+ }
38310
+ if (password && formInfo.passwordSelector) {
38311
+ await page.fill(formInfo.passwordSelector, password);
38312
+ if (!opts.json)
38313
+ console.log(chalk.green(` \u2713 Filled password`));
38314
+ }
38315
+ if (formInfo.hasSubmitButton && formInfo.submitSelector) {
38316
+ await page.click(formInfo.submitSelector);
38317
+ if (!opts.json)
38318
+ console.log(chalk.green(` \u2713 Submitted form`));
38319
+ try {
38320
+ await page.waitForNavigation({ timeout: 1e4 });
38321
+ } catch {}
38322
+ await new Promise((r) => setTimeout(r, 2000));
38323
+ }
38324
+ const finalUrl = page.url();
38325
+ const loggedIn = finalUrl !== url;
38326
+ let savedAs;
38327
+ if (opts.saveAs || loggedIn) {
38328
+ const name = opts.saveAs ?? new URL(url).hostname.replace(/\./g, "-");
38329
+ try {
38330
+ const { saveStateFromPage: saveStateFromPage2 } = await Promise.resolve().then(() => (init_storage_state(), exports_storage_state));
38331
+ await saveStateFromPage2(page, name);
38332
+ savedAs = name;
38333
+ if (!opts.json)
38334
+ console.log(chalk.green(` \u2713 State saved as: ${name}`));
38335
+ } catch {}
38336
+ }
38337
+ if (opts.json) {
38338
+ console.log(JSON.stringify({ session_id: session.id, url: finalUrl, logged_in: loggedIn, form_detected: formInfo.hasEmailInput, saved_as: savedAs }));
38339
+ } else {
38340
+ console.log(loggedIn ? chalk.green(`
38341
+ \u2713 Login successful \u2192 ${finalUrl}`) : chalk.yellow(`
38342
+ \u26A0 May need manual steps (magic link, 2FA, etc)`));
38343
+ }
38344
+ if (!opts.headed)
38345
+ await closeSession2(session.id);
38346
+ });
37070
38347
  program2.command("install-browser").description("Install a browser engine").option("--engine <engine>", "Engine to install: lightpanda|chromium", "chromium").action(async (opts) => {
37071
38348
  if (opts.engine === "chromium") {
37072
38349
  const { execSync: execSync3 } = await import("child_process");
@@ -37082,6 +38359,124 @@ program2.command("install-browser").description("Install a browser engine").opti
37082
38359
  }
37083
38360
  }
37084
38361
  });
38362
+ var daemonCmd = program2.command("daemon").description("Manage the browser daemon (persistent background sessions)");
38363
+ daemonCmd.command("start").description("Start the browser daemon in the background").option("--port <port>", "Port to listen on", "7030").action(async (opts) => {
38364
+ const { isDaemonRunning: isDaemonRunning2, getDaemonPidFile: getDaemonPidFile2, getDaemonStatus: getDaemonStatus2 } = await Promise.resolve().then(() => (init_daemon_client(), exports_daemon_client));
38365
+ if (isDaemonRunning2()) {
38366
+ console.log(chalk.yellow("Daemon is already running."));
38367
+ const status = await getDaemonStatus2();
38368
+ console.log(chalk.gray(` PID: ${status.pid}, Port: ${status.port}, Sessions: ${status.sessions ?? "?"}`));
38369
+ return;
38370
+ }
38371
+ const { spawn: spawn2 } = await import("child_process");
38372
+ const { writeFileSync: writeFileSync8, mkdirSync: mkdirSync14 } = await import("fs");
38373
+ const { dirname: dirname4 } = await import("path");
38374
+ const pidFile = getDaemonPidFile2();
38375
+ mkdirSync14(dirname4(pidFile), { recursive: true });
38376
+ const child = spawn2(process.execPath, [import.meta.dir + "/../server/index.js"], {
38377
+ detached: true,
38378
+ stdio: "ignore",
38379
+ env: { ...process.env, BROWSER_SERVER_PORT: opts.port }
38380
+ });
38381
+ child.unref();
38382
+ if (child.pid) {
38383
+ writeFileSync8(pidFile, String(child.pid));
38384
+ await new Promise((r) => setTimeout(r, 1500));
38385
+ console.log(chalk.green(`\u2713 Daemon started`));
38386
+ console.log(chalk.gray(` PID: ${child.pid}, Port: ${opts.port}`));
38387
+ console.log(chalk.gray(` Sessions will persist across CLI invocations.`));
38388
+ console.log(chalk.gray(` Stop with: browser daemon stop`));
38389
+ } else {
38390
+ console.log(chalk.red("Failed to start daemon"));
38391
+ }
38392
+ });
38393
+ daemonCmd.command("stop").description("Stop the browser daemon").action(async () => {
38394
+ const { isDaemonRunning: isDaemonRunning2, getDaemonPid: getDaemonPid2, getDaemonPidFile: getDaemonPidFile2 } = await Promise.resolve().then(() => (init_daemon_client(), exports_daemon_client));
38395
+ const { unlinkSync: unlinkSync4 } = await import("fs");
38396
+ if (!isDaemonRunning2()) {
38397
+ console.log(chalk.gray("Daemon is not running."));
38398
+ return;
38399
+ }
38400
+ const pid = getDaemonPid2();
38401
+ if (pid) {
38402
+ try {
38403
+ process.kill(pid, "SIGTERM");
38404
+ } catch {}
38405
+ try {
38406
+ unlinkSync4(getDaemonPidFile2());
38407
+ } catch {}
38408
+ console.log(chalk.green(`\u2713 Daemon stopped (PID: ${pid})`));
38409
+ }
38410
+ });
38411
+ daemonCmd.command("status").description("Check daemon status").option("--json", "Output as JSON").action(async (opts) => {
38412
+ const { getDaemonStatus: getDaemonStatus2 } = await Promise.resolve().then(() => (init_daemon_client(), exports_daemon_client));
38413
+ const status = await getDaemonStatus2();
38414
+ if (opts.json) {
38415
+ console.log(JSON.stringify(status, null, 2));
38416
+ } else if (status.running) {
38417
+ console.log(chalk.green("\u25CF Daemon running"));
38418
+ console.log(chalk.gray(` PID: ${status.pid}`));
38419
+ console.log(chalk.gray(` Port: ${status.port}`));
38420
+ if (status.sessions != null)
38421
+ console.log(chalk.gray(` Active sessions: ${status.sessions}`));
38422
+ if (status.uptime_ms != null)
38423
+ console.log(chalk.gray(` Uptime: ${Math.round(status.uptime_ms / 1000)}s`));
38424
+ } else {
38425
+ console.log(chalk.gray("\u25CB Daemon not running"));
38426
+ console.log(chalk.gray(` Start with: browser daemon start`));
38427
+ }
38428
+ });
38429
+ program2.command("watch <url>").description("Monitor a URL for changes \u2014 periodic screenshot + diff").option("--engine <engine>", "Browser engine", "auto").option("--interval <seconds>", "Check interval in seconds", "30").option("--threshold <percent>", "Change threshold percent to report", "5").option("--headed", "Run in headed mode").option("--json", "Output as JSON").action(async (url, opts) => {
38430
+ const intervalMs = parseInt(opts.interval) * 1000;
38431
+ const threshold = parseFloat(opts.threshold);
38432
+ const { session, page } = await createSession2({ engine: opts.engine, headless: !opts.headed });
38433
+ console.log(chalk.gray(`Watching: ${url} (every ${opts.interval}s, threshold ${opts.threshold}%)`));
38434
+ console.log(chalk.gray(`Session: ${session.id} \u2014 Press Ctrl+C to stop
38435
+ `));
38436
+ await navigate(page, url);
38437
+ let baselineResult = await takeScreenshot(page, { format: "png" });
38438
+ let baselinePath = baselineResult.path;
38439
+ let checkCount = 0;
38440
+ if (!opts.json)
38441
+ console.log(chalk.blue(`[${new Date().toISOString()}] Baseline captured: ${baselinePath}`));
38442
+ const check = async () => {
38443
+ checkCount++;
38444
+ try {
38445
+ await page.reload({ waitUntil: "domcontentloaded" });
38446
+ await new Promise((r) => setTimeout(r, 2000));
38447
+ const newResult = await takeScreenshot(page, { format: "png" });
38448
+ const { diffImages: diffImages2 } = await Promise.resolve().then(() => (init_gallery_diff(), exports_gallery_diff));
38449
+ const diff = await diffImages2(baselinePath, newResult.path);
38450
+ const changed = diff.changed_percent > threshold;
38451
+ const timestamp = new Date().toISOString();
38452
+ if (opts.json) {
38453
+ console.log(JSON.stringify({ timestamp, check: checkCount, changed_percent: diff.changed_percent, changed, screenshot: newResult.path, diff_path: changed ? diff.diff_path : undefined }));
38454
+ } else if (changed) {
38455
+ console.log(chalk.red(`[${timestamp}] CHANGED: ${diff.changed_percent.toFixed(2)}% (${diff.changed_pixels} pixels)`));
38456
+ console.log(chalk.gray(` Screenshot: ${newResult.path}`));
38457
+ console.log(chalk.gray(` Diff: ${diff.diff_path}`));
38458
+ baselinePath = newResult.path;
38459
+ } else {
38460
+ console.log(chalk.green(`[${timestamp}] No change (${diff.changed_percent.toFixed(2)}%)`));
38461
+ }
38462
+ } catch (err2) {
38463
+ const msg = err2 instanceof Error ? err2.message : String(err2);
38464
+ if (opts.json) {
38465
+ console.log(JSON.stringify({ timestamp: new Date().toISOString(), check: checkCount, error: msg }));
38466
+ } else {
38467
+ console.log(chalk.red(`[${new Date().toISOString()}] Error: ${msg}`));
38468
+ }
38469
+ }
38470
+ };
38471
+ const timer = setInterval(check, intervalMs);
38472
+ process.on("SIGINT", async () => {
38473
+ clearInterval(timer);
38474
+ console.log(chalk.gray(`
38475
+ Stopping watch. ${checkCount} checks performed.`));
38476
+ await closeSession2(session.id);
38477
+ process.exit(0);
38478
+ });
38479
+ });
37085
38480
  program2.command("mcp").description("Start the MCP server (stdio)").action(async () => {
37086
38481
  await init_mcp().then(() => exports_mcp);
37087
38482
  });
@@ -37158,11 +38553,11 @@ galleryCmd.command("stats").description("Show gallery statistics").option("--pro
37158
38553
  });
37159
38554
  galleryCmd.command("clean").description("Delete gallery entries with missing files").action(async () => {
37160
38555
  const { listEntries: listEntries2, deleteEntry: deleteEntry2 } = await Promise.resolve().then(() => (init_gallery(), exports_gallery));
37161
- const { existsSync: existsSync10 } = await import("fs");
38556
+ const { existsSync: existsSync11 } = await import("fs");
37162
38557
  const entries = listEntries2({ limit: 9999 });
37163
38558
  let removed = 0;
37164
38559
  for (const e of entries) {
37165
- if (!existsSync10(e.path)) {
38560
+ if (!existsSync11(e.path)) {
37166
38561
  deleteEntry2(e.id);
37167
38562
  removed++;
37168
38563
  }