@hasna/testers 0.0.32 → 0.0.34

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.
Files changed (41) hide show
  1. package/LICENSE +2 -1
  2. package/README.md +11 -0
  3. package/dashboard/dist/assets/index-kezQIIQ1.js +49 -0
  4. package/dashboard/dist/index.html +1 -1
  5. package/dist/cli/index.js +69872 -2606
  6. package/dist/db/database.d.ts.map +1 -1
  7. package/dist/db/sessions.d.ts +36 -0
  8. package/dist/db/sessions.d.ts.map +1 -0
  9. package/dist/db/workflows.d.ts +10 -0
  10. package/dist/db/workflows.d.ts.map +1 -0
  11. package/dist/index.d.ts +6 -0
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +1353 -30
  14. package/dist/lib/ai-client.d.ts +2 -0
  15. package/dist/lib/ai-client.d.ts.map +1 -1
  16. package/dist/lib/browser.d.ts.map +1 -1
  17. package/dist/lib/open-projects.d.ts +14 -0
  18. package/dist/lib/open-projects.d.ts.map +1 -0
  19. package/dist/lib/repo-discovery.d.ts +102 -0
  20. package/dist/lib/repo-discovery.d.ts.map +1 -0
  21. package/dist/lib/repo-executor.d.ts +56 -0
  22. package/dist/lib/repo-executor.d.ts.map +1 -0
  23. package/dist/lib/runner.d.ts.map +1 -1
  24. package/dist/lib/todos-connector.d.ts +15 -1
  25. package/dist/lib/todos-connector.d.ts.map +1 -1
  26. package/dist/lib/workflow-agent.d.ts +32 -0
  27. package/dist/lib/workflow-agent.d.ts.map +1 -0
  28. package/dist/lib/workflow-runner.d.ts +27 -0
  29. package/dist/lib/workflow-runner.d.ts.map +1 -0
  30. package/dist/mcp/http.d.ts +19 -0
  31. package/dist/mcp/http.d.ts.map +1 -0
  32. package/dist/mcp/index.js +85700 -23461
  33. package/dist/mcp/server.d.ts +4 -0
  34. package/dist/mcp/server.d.ts.map +1 -0
  35. package/dist/sdk/index.d.ts +8 -1
  36. package/dist/sdk/index.d.ts.map +1 -1
  37. package/dist/server/index.js +46044 -15024
  38. package/dist/types/index.d.ts +69 -0
  39. package/dist/types/index.d.ts.map +1 -1
  40. package/package.json +8 -4
  41. package/dashboard/dist/assets/index-D52SWwDa.js +0 -49
package/dist/index.js CHANGED
@@ -9748,6 +9748,7 @@ function resetDatabase() {
9748
9748
  database.exec("DELETE FROM auth_presets");
9749
9749
  database.exec("DELETE FROM environments");
9750
9750
  database.exec("DELETE FROM schedules");
9751
+ database.exec("DELETE FROM testing_workflows");
9751
9752
  database.exec("DELETE FROM api_check_results");
9752
9753
  database.exec("DELETE FROM api_checks");
9753
9754
  database.exec("DELETE FROM runs");
@@ -9756,6 +9757,7 @@ function resetDatabase() {
9756
9757
  database.exec("DELETE FROM agents");
9757
9758
  database.exec("DELETE FROM scan_issues");
9758
9759
  database.exec("DELETE FROM projects");
9760
+ database.exec("DELETE FROM sessions");
9759
9761
  }
9760
9762
  function resolvePartialId(table, partialId) {
9761
9763
  const database = getDatabase();
@@ -10171,6 +10173,46 @@ ALTER TABLE runs ADD COLUMN pr_base_branch TEXT;
10171
10173
  ALTER TABLE runs ADD COLUMN pr_commit_sha TEXT;
10172
10174
  ALTER TABLE runs ADD COLUMN pr_url TEXT;
10173
10175
  ALTER TABLE runs ADD COLUMN gh_app_installation_id TEXT;
10176
+ `,
10177
+ `
10178
+ CREATE TABLE IF NOT EXISTS sessions (
10179
+ id TEXT PRIMARY KEY,
10180
+ tab_id INTEGER NOT NULL,
10181
+ url TEXT,
10182
+ title TEXT,
10183
+ entries TEXT NOT NULL DEFAULT '[]',
10184
+ entry_count INTEGER NOT NULL DEFAULT 0,
10185
+ error_count INTEGER NOT NULL DEFAULT 0,
10186
+ console_count INTEGER NOT NULL DEFAULT 0,
10187
+ nav_count INTEGER NOT NULL DEFAULT 0,
10188
+ status TEXT NOT NULL DEFAULT 'exported' CHECK(status IN ('live','saved','exported')),
10189
+ start_time TEXT NOT NULL,
10190
+ end_time TEXT,
10191
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
10192
+ );
10193
+
10194
+ CREATE INDEX IF NOT EXISTS idx_sessions_tab ON sessions(tab_id);
10195
+ CREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(status);
10196
+ CREATE INDEX IF NOT EXISTS idx_sessions_created ON sessions(created_at DESC);
10197
+ `,
10198
+ `
10199
+ CREATE TABLE IF NOT EXISTS testing_workflows (
10200
+ id TEXT PRIMARY KEY,
10201
+ project_id TEXT REFERENCES projects(id) ON DELETE CASCADE,
10202
+ name TEXT NOT NULL,
10203
+ description TEXT,
10204
+ scenario_filter TEXT NOT NULL DEFAULT '{}',
10205
+ persona_ids TEXT NOT NULL DEFAULT '[]',
10206
+ goal TEXT,
10207
+ execution TEXT NOT NULL DEFAULT '{"target":"local"}',
10208
+ settings TEXT NOT NULL DEFAULT '{}',
10209
+ enabled INTEGER NOT NULL DEFAULT 1,
10210
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
10211
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
10212
+ );
10213
+
10214
+ CREATE INDEX IF NOT EXISTS idx_testing_workflows_project ON testing_workflows(project_id);
10215
+ CREATE INDEX IF NOT EXISTS idx_testing_workflows_enabled ON testing_workflows(enabled);
10174
10216
  `
10175
10217
  ];
10176
10218
  });
@@ -11297,7 +11339,8 @@ async function getPage(browser, options) {
11297
11339
  }
11298
11340
  const viewport = options?.viewport ?? DEFAULT_VIEWPORT;
11299
11341
  try {
11300
- return await getBrowserPage(browser, { viewport, userAgent: options?.userAgent, locale: options?.locale });
11342
+ const page = await getBrowserPage(browser, { viewport, userAgent: options?.userAgent, locale: options?.locale });
11343
+ return page;
11301
11344
  } catch (error) {
11302
11345
  const message = error instanceof Error ? error.message : String(error);
11303
11346
  throw new BrowserError(`Failed to create page: ${message}`);
@@ -11640,6 +11683,41 @@ var init_healer = __esm(() => {
11640
11683
 
11641
11684
  // src/lib/ai-client.ts
11642
11685
  import Anthropic2 from "@anthropic-ai/sdk";
11686
+ import {
11687
+ click as browserClick,
11688
+ fill as browserFill,
11689
+ clickRef,
11690
+ typeRef,
11691
+ fillRef,
11692
+ selectRef,
11693
+ checkRef,
11694
+ hoverRef,
11695
+ getPageInfo,
11696
+ elementExists,
11697
+ getText,
11698
+ getUrl,
11699
+ getTitle,
11700
+ extractTable,
11701
+ getAriaSnapshot,
11702
+ crawl as browserCrawl,
11703
+ addInterceptRule,
11704
+ clearInterceptRules,
11705
+ startHAR,
11706
+ getPerformanceMetrics,
11707
+ startCoverage
11708
+ } from "@hasna/browser";
11709
+ async function takeSnapshot(page, _sessionId) {
11710
+ const tree = await getAriaSnapshot(page);
11711
+ return {
11712
+ tree,
11713
+ snapshot: tree,
11714
+ refs: [],
11715
+ interactive_count: 0
11716
+ };
11717
+ }
11718
+ async function extractStructuredData(page) {
11719
+ return getPageInfo(page);
11720
+ }
11643
11721
  function resolveModel2(nameOrPreset) {
11644
11722
  if (nameOrPreset in MODEL_MAP) {
11645
11723
  return MODEL_MAP[nameOrPreset];
@@ -11679,7 +11757,18 @@ async function executeTool(page, screenshotter, toolName, toolInput, context) {
11679
11757
  case "click": {
11680
11758
  const selector = toolInput.selector;
11681
11759
  try {
11682
- await page.click(selector);
11760
+ const healResult = await browserClick(page, selector, { selfHeal: true });
11761
+ const screenshot = await screenshotter.capture(page, {
11762
+ runId: context.runId,
11763
+ scenarioSlug: context.scenarioSlug,
11764
+ stepNumber: context.stepNumber,
11765
+ action: "click"
11766
+ });
11767
+ const healNote = healResult.healed ? ` [self-healed via ${healResult.method}]` : "";
11768
+ return {
11769
+ result: `Clicked element: ${selector}${healNote}`,
11770
+ screenshot
11771
+ };
11683
11772
  } catch (clickErr) {
11684
11773
  const errMsg = clickErr instanceof Error ? clickErr.message : String(clickErr);
11685
11774
  if (errMsg.includes("not found") || errMsg.includes("No element") || errMsg.includes("waiting for selector")) {
@@ -11688,29 +11777,23 @@ async function executeTool(page, screenshotter, toolName, toolInput, context) {
11688
11777
  const heal = await healSelector2({ page, failedSelector: selector, intent: `click the element matching "${selector}"` });
11689
11778
  if (heal.healed && heal.newSelector) {
11690
11779
  await page.click(heal.newSelector);
11691
- const screenshot2 = await screenshotter.capture(page, { runId: context.runId, scenarioSlug: context.scenarioSlug, stepNumber: context.stepNumber, action: "click" });
11692
- return { result: `Clicked element: ${heal.newSelector} [healed from "${selector}" \u2014 ${heal.reasoning}]`, screenshot: screenshot2 };
11780
+ const screenshot = await screenshotter.capture(page, { runId: context.runId, scenarioSlug: context.scenarioSlug, stepNumber: context.stepNumber, action: "click" });
11781
+ return { result: `Clicked element: ${heal.newSelector} [AI-healed from "${selector}" \u2014 ${heal.reasoning}]`, screenshot };
11693
11782
  }
11694
11783
  }
11695
11784
  }
11696
11785
  throw clickErr;
11697
11786
  }
11698
- const screenshot = await screenshotter.capture(page, {
11699
- runId: context.runId,
11700
- scenarioSlug: context.scenarioSlug,
11701
- stepNumber: context.stepNumber,
11702
- action: "click"
11703
- });
11704
- return {
11705
- result: `Clicked element: ${selector}`,
11706
- screenshot
11707
- };
11708
11787
  }
11709
11788
  case "fill": {
11710
11789
  const selector = toolInput.selector;
11711
11790
  const value = toolInput.value;
11712
11791
  try {
11713
- await page.fill(selector, value);
11792
+ const healResult = await browserFill(page, selector, value, undefined, true);
11793
+ const healNote = healResult.healed ? ` [self-healed via ${healResult.method}]` : "";
11794
+ return {
11795
+ result: `Filled "${selector}" with value${healNote}`
11796
+ };
11714
11797
  } catch (fillErr) {
11715
11798
  const errMsg = fillErr instanceof Error ? fillErr.message : String(fillErr);
11716
11799
  if (errMsg.includes("not found") || errMsg.includes("No element") || errMsg.includes("waiting for selector")) {
@@ -11719,15 +11802,12 @@ async function executeTool(page, screenshotter, toolName, toolInput, context) {
11719
11802
  const heal = await healSelector2({ page, failedSelector: selector, intent: `fill the input field "${selector}" with "${value}"` });
11720
11803
  if (heal.healed && heal.newSelector) {
11721
11804
  await page.fill(heal.newSelector, value);
11722
- return { result: `Filled "${heal.newSelector}" with value [healed from "${selector}"]` };
11805
+ return { result: `Filled "${heal.newSelector}" with value [AI-healed from "${selector}"]` };
11723
11806
  }
11724
11807
  }
11725
11808
  }
11726
11809
  throw fillErr;
11727
11810
  }
11728
- return {
11729
- result: `Filled "${selector}" with value`
11730
- };
11731
11811
  }
11732
11812
  case "select_option": {
11733
11813
  const selector = toolInput.selector;
@@ -11915,6 +11995,279 @@ async function executeTool(page, screenshotter, toolName, toolInput, context) {
11915
11995
  result: `Test ${status}: ${reasoning}`
11916
11996
  };
11917
11997
  }
11998
+ case "browser_snapshot": {
11999
+ const snapshot = await takeSnapshot(page, context.sessionId);
12000
+ return {
12001
+ result: snapshot.tree
12002
+ };
12003
+ }
12004
+ case "browser_click_ref": {
12005
+ const ref = toolInput.ref;
12006
+ await takeSnapshot(page, context.sessionId);
12007
+ await clickRef(page, context.sessionId, ref);
12008
+ const screenshot = await screenshotter.capture(page, {
12009
+ runId: context.runId,
12010
+ scenarioSlug: context.scenarioSlug,
12011
+ stepNumber: context.stepNumber,
12012
+ action: "click_ref"
12013
+ });
12014
+ return {
12015
+ result: `Clicked ref: ${ref}`,
12016
+ screenshot
12017
+ };
12018
+ }
12019
+ case "browser_type_ref": {
12020
+ const ref = toolInput.ref;
12021
+ const text = toolInput.text;
12022
+ const clear = toolInput.clear;
12023
+ await takeSnapshot(page, context.sessionId);
12024
+ await typeRef(page, context.sessionId, ref, text, { clear: clear ?? true });
12025
+ return {
12026
+ result: `Typed into ref ${ref}: "${text}"`
12027
+ };
12028
+ }
12029
+ case "browser_fill_ref": {
12030
+ const ref = toolInput.ref;
12031
+ const value = toolInput.value;
12032
+ await takeSnapshot(page, context.sessionId);
12033
+ await fillRef(page, context.sessionId, ref, value);
12034
+ return {
12035
+ result: `Filled ref ${ref} with value`
12036
+ };
12037
+ }
12038
+ case "browser_select_ref": {
12039
+ const ref = toolInput.ref;
12040
+ const value = toolInput.value;
12041
+ await takeSnapshot(page, context.sessionId);
12042
+ const selected = await selectRef(page, context.sessionId, ref, value);
12043
+ return {
12044
+ result: `Selected "${selected.join(", ")}" in ref ${ref}`
12045
+ };
12046
+ }
12047
+ case "browser_check_ref": {
12048
+ const ref = toolInput.ref;
12049
+ const checked = toolInput.checked;
12050
+ await takeSnapshot(page, context.sessionId);
12051
+ await checkRef(page, context.sessionId, ref, checked);
12052
+ return {
12053
+ result: `${checked ? "Checked" : "Unchecked"} ref: ${ref}`
12054
+ };
12055
+ }
12056
+ case "browser_hover_ref": {
12057
+ const ref = toolInput.ref;
12058
+ await takeSnapshot(page, context.sessionId);
12059
+ await hoverRef(page, context.sessionId, ref);
12060
+ const screenshot = await screenshotter.capture(page, {
12061
+ runId: context.runId,
12062
+ scenarioSlug: context.scenarioSlug,
12063
+ stepNumber: context.stepNumber,
12064
+ action: "hover_ref"
12065
+ });
12066
+ return {
12067
+ result: `Hovered ref: ${ref}`,
12068
+ screenshot
12069
+ };
12070
+ }
12071
+ case "browser_check": {
12072
+ const [info, snapshot] = await Promise.all([
12073
+ getPageInfo(page),
12074
+ takeSnapshot(page, context.sessionId)
12075
+ ]);
12076
+ const parts = [
12077
+ `URL: ${info.url}`,
12078
+ `Title: ${info.title}`,
12079
+ info.meta_description ? `Description: ${info.meta_description}` : null,
12080
+ `Links: ${info.links_count} | Images: ${info.images_count} | Forms: ${info.forms_count}`,
12081
+ `Text length: ${info.text_length} | Console errors: ${info.has_console_errors}`,
12082
+ `Viewport: ${info.viewport.width}x${info.viewport.height}`,
12083
+ ``,
12084
+ `Interactive elements: ${snapshot.interactive_count}`,
12085
+ ``,
12086
+ snapshot.tree
12087
+ ].filter(Boolean);
12088
+ return { result: parts.join(`
12089
+ `) };
12090
+ }
12091
+ case "browser_assert": {
12092
+ const assertionType = toolInput.assertion_type;
12093
+ const selector = toolInput.selector;
12094
+ const expected = toolInput.expected;
12095
+ const sessionId = context.sessionId ?? "default";
12096
+ switch (assertionType) {
12097
+ case "element_exists": {
12098
+ if (!selector)
12099
+ return { result: "Error: selector required for element_exists assertion" };
12100
+ const result = await elementExists(page, selector);
12101
+ const pass = result.exists;
12102
+ return { result: pass ? `PASS: element "${selector}" exists (${result.count} match${result.count !== 1 ? "es" : ""}, visible: ${result.visible})` : `FAIL: element "${selector}" not found` };
12103
+ }
12104
+ case "text_contains": {
12105
+ const text = selector ? await getText(page, selector) : await getText(page);
12106
+ const pass = expected !== undefined && text.includes(expected);
12107
+ return { result: pass ? `PASS: text contains "${expected}"` : `FAIL: text does not contain "${expected}". Found: "${text.slice(0, 200)}"` };
12108
+ }
12109
+ case "url_matches": {
12110
+ const url = await getUrl(page);
12111
+ const pattern = expected ?? "";
12112
+ const pass = new RegExp(pattern).test(url);
12113
+ return { result: pass ? `PASS: URL matches /${pattern}/` : `FAIL: URL "${url}" does not match /${pattern}/` };
12114
+ }
12115
+ case "title_contains": {
12116
+ const title = await getTitle(page);
12117
+ const pass = expected !== undefined && title.includes(expected);
12118
+ return { result: pass ? `PASS: title contains "${expected}"` : `FAIL: title "${title}" does not contain "${expected}"` };
12119
+ }
12120
+ default:
12121
+ return { result: `Unknown assertion type: ${assertionType}` };
12122
+ }
12123
+ }
12124
+ case "browser_extract": {
12125
+ const mode = toolInput.mode;
12126
+ const selector = toolInput.selector;
12127
+ switch (mode) {
12128
+ case "structured": {
12129
+ const data = await extractStructuredData(page);
12130
+ return { result: JSON.stringify(data, null, 2) };
12131
+ }
12132
+ case "table": {
12133
+ if (!selector)
12134
+ return { result: "Error: selector required for table extraction" };
12135
+ const rows = await extractTable(page, selector);
12136
+ return { result: JSON.stringify(rows, null, 2) };
12137
+ }
12138
+ case "text": {
12139
+ const text = selector ? await getText(page, selector) : await getText(page);
12140
+ return { result: text };
12141
+ }
12142
+ case "aria": {
12143
+ const aria = await getAriaSnapshot(page);
12144
+ return { result: aria };
12145
+ }
12146
+ default:
12147
+ return { result: `Unknown extract mode: ${mode}` };
12148
+ }
12149
+ }
12150
+ case "browser_crawl": {
12151
+ const url = toolInput.url;
12152
+ const maxDepth = toolInput.max_depth ?? 2;
12153
+ const maxPages = toolInput.max_pages ?? 20;
12154
+ const result = await browserCrawl(url, { maxDepth, maxPages });
12155
+ return { result: JSON.stringify(result, null, 2) };
12156
+ }
12157
+ case "browser_intercept": {
12158
+ const action = toolInput.action;
12159
+ const pattern = toolInput.pattern;
12160
+ const interceptAction = toolInput.intercept_action;
12161
+ const statusCode = toolInput.status_code;
12162
+ const body = toolInput.body;
12163
+ const sessionId = context.sessionId ?? "default";
12164
+ switch (action) {
12165
+ case "block": {
12166
+ if (!pattern)
12167
+ return { result: "Error: pattern required for block action" };
12168
+ await addInterceptRule(page, { pattern, action: "block" });
12169
+ return { result: `Blocked requests matching: ${pattern}` };
12170
+ }
12171
+ case "modify": {
12172
+ if (!pattern)
12173
+ return { result: "Error: pattern required for modify action" };
12174
+ await addInterceptRule(page, { pattern, action: "modify", response: { status: statusCode ?? 200, body: body ?? "" } });
12175
+ return { result: `Modified requests matching: ${pattern} \u2192 status ${statusCode ?? 200}` };
12176
+ }
12177
+ case "log": {
12178
+ if (!pattern)
12179
+ return { result: "Error: pattern required for log action" };
12180
+ await addInterceptRule(page, { pattern, action: "log" });
12181
+ return { result: `Logging requests matching: ${pattern}` };
12182
+ }
12183
+ case "clear": {
12184
+ await clearInterceptRules(page);
12185
+ return { result: "Cleared all intercept rules" };
12186
+ }
12187
+ case "har_start": {
12188
+ const harCapture = startHAR(page);
12189
+ activeHARs.set(sessionId, harCapture);
12190
+ return { result: "HAR capture started" };
12191
+ }
12192
+ case "har_stop": {
12193
+ const harCapture = activeHARs.get(sessionId);
12194
+ if (!harCapture)
12195
+ return { result: "Error: no active HAR capture for this session" };
12196
+ const har = harCapture.stop();
12197
+ activeHARs.delete(sessionId);
12198
+ const entryCount = har.log.entries.length;
12199
+ return { result: `HAR capture stopped: ${entryCount} entries captured
12200
+ ${JSON.stringify(har, null, 2)}` };
12201
+ }
12202
+ default:
12203
+ return { result: `Unknown intercept action: ${action}` };
12204
+ }
12205
+ }
12206
+ case "browser_performance": {
12207
+ const mode = toolInput.mode;
12208
+ const sessionId = context.sessionId ?? "default";
12209
+ switch (mode) {
12210
+ case "metrics": {
12211
+ const metrics = await getPerformanceMetrics(page);
12212
+ return { result: JSON.stringify(metrics, null, 2) };
12213
+ }
12214
+ case "deep": {
12215
+ const deep = await getPerformanceMetrics(page);
12216
+ return { result: JSON.stringify(deep, null, 2) };
12217
+ }
12218
+ case "coverage_start": {
12219
+ const session = await startCoverage(page);
12220
+ activeCoverage.set(sessionId, session);
12221
+ return { result: "Coverage tracking started" };
12222
+ }
12223
+ case "coverage_stop": {
12224
+ const session = activeCoverage.get(sessionId);
12225
+ if (!session)
12226
+ return { result: "Error: no active coverage session" };
12227
+ const result = await session.stop();
12228
+ activeCoverage.delete(sessionId);
12229
+ return { result: JSON.stringify(result, null, 2) };
12230
+ }
12231
+ default:
12232
+ return { result: `Unknown performance mode: ${mode}` };
12233
+ }
12234
+ }
12235
+ case "browser_a11y": {
12236
+ const level = toolInput.level ?? "AA";
12237
+ const snapshot = await page.accessibility.snapshot();
12238
+ if (!snapshot)
12239
+ return { result: "Error: could not capture accessibility tree" };
12240
+ const issues = [];
12241
+ const checkNode = (node, path = []) => {
12242
+ const label = node.name ?? "";
12243
+ const role = node.role ?? "";
12244
+ const nodePath = [...path, `${role}${label ? ` "${label}"` : ""}`];
12245
+ if (role === "img" && !label) {
12246
+ issues.push(`[A] Image missing alt text at ${nodePath.join(" > ")}`);
12247
+ }
12248
+ if (["button", "link", "textbox", "checkbox", "radio", "combobox", "menuitem"].includes(role) && !label) {
12249
+ issues.push(`[A] ${role} missing accessible name at ${nodePath.join(" > ")}`);
12250
+ }
12251
+ if (["textbox", "combobox", "slider", "spinbutton"].includes(role) && !label) {
12252
+ issues.push(`[AA] Form field (${role}) missing label at ${nodePath.join(" > ")}`);
12253
+ }
12254
+ if (role.startsWith("heading") && !label) {
12255
+ issues.push(`[AAA] Empty heading at ${nodePath.join(" > ")}`);
12256
+ }
12257
+ for (const child of node.children ?? []) {
12258
+ checkNode(child, nodePath);
12259
+ }
12260
+ };
12261
+ checkNode(snapshot);
12262
+ const maxLevel = level === "A" ? 0 : level === "AA" ? 1 : 2;
12263
+ const levelMap = { 0: ["A"], 1: ["A", "AA"], 2: ["A", "AA", "AAA"] };
12264
+ const applicable = levelMap[maxLevel] ?? ["A", "AA"];
12265
+ const filtered = issues.filter((i) => applicable.some((l) => i.includes(`[${l}]`)));
12266
+ const summary = filtered.length === 0 ? `No a11y issues found at WCAG ${level} level` : `${filtered.length} a11y issue${filtered.length > 1 ? "s" : ""} found at WCAG ${level} level:
12267
+ ${filtered.join(`
12268
+ `)}`;
12269
+ return { result: summary };
12270
+ }
11918
12271
  default:
11919
12272
  return { result: `Unknown tool: ${toolName}` };
11920
12273
  }
@@ -11931,6 +12284,7 @@ async function runAgentLoop(options) {
11931
12284
  screenshotter,
11932
12285
  model,
11933
12286
  runId,
12287
+ sessionId,
11934
12288
  maxTurns = 30,
11935
12289
  onStep,
11936
12290
  persona,
@@ -11955,17 +12309,23 @@ Instructions: ${persona.instructions}` : "",
11955
12309
  "You are an expert QA testing agent. Your job is to thoroughly test web application scenarios.",
11956
12310
  "You have browser tools to navigate, interact with, and inspect web pages.",
11957
12311
  "",
11958
- "Strategy:",
11959
- "1. First navigate to the target page and take a screenshot to understand the layout",
11960
- "2. If you can't find an element, use get_elements or get_page_html to discover selectors",
11961
- "3. Use scroll to discover content below the fold",
12312
+ "Strategy (snapshot \u2192 ref \u2192 act):",
12313
+ "1. Navigate to the target page, then call browser_snapshot to get an accessibility tree with element refs (@e0, @e1, ...)",
12314
+ "2. Use ref-based tools (browser_click_ref, browser_type_ref, browser_fill_ref, etc.) to interact with elements by their ref IDs \u2014 this is more reliable than CSS selectors",
12315
+ "3. After actions that change page state, call browser_snapshot again to see the updated tree",
11962
12316
  "4. Use wait_for or wait_for_navigation after actions that trigger page loads",
11963
12317
  "5. Take screenshots after every meaningful state change",
11964
12318
  "6. Use assert_text and assert_visible to verify expected outcomes",
11965
12319
  "7. When done testing, call report_result with detailed pass/fail reasoning",
11966
12320
  "",
12321
+ "When to use CSS-selector tools vs ref-based tools:",
12322
+ "- Prefer ref-based tools (browser_click_ref, etc.) \u2014 they resolve via the accessibility snapshot and self-heal on DOM changes",
12323
+ "- Use CSS-selector tools (click, fill) only when you need to target elements by a known stable selector",
12324
+ "- Both click and fill have built-in self-healing: if the selector breaks, they try alternative strategies automatically",
12325
+ "- If built-in healing fails, the AI healer kicks in as a deeper fallback",
12326
+ "",
11967
12327
  "Tips:",
11968
- "- Try multiple selector strategies: by text, by role, by class, by id",
12328
+ "- Call browser_snapshot before interacting \u2014 it gives you the current refs and interactive element count",
11969
12329
  "- If a click triggers navigation, use wait_for_navigation after",
11970
12330
  "- For forms, fill all fields before submitting",
11971
12331
  "- Check for error messages after form submissions",
@@ -12049,7 +12409,7 @@ Instructions: ${persona.instructions}` : "",
12049
12409
  if (onStep) {
12050
12410
  onStep({ type: "tool_call", toolName: toolBlock.name, toolInput, stepNumber });
12051
12411
  }
12052
- const execResult = await executeTool(page, screenshotter, toolBlock.name, toolInput, { runId, scenarioSlug, stepNumber, a11y });
12412
+ const execResult = await executeTool(page, screenshotter, toolBlock.name, toolInput, { runId, scenarioSlug, stepNumber, sessionId, a11y });
12053
12413
  if (onStep) {
12054
12414
  onStep({ type: "tool_result", toolName: toolBlock.name, toolResult: execResult.result, stepNumber });
12055
12415
  }
@@ -12203,9 +12563,11 @@ function createClientForModel(model, apiKey) {
12203
12563
  }
12204
12564
  return createClient(apiKey);
12205
12565
  }
12206
- var BROWSER_TOOLS;
12566
+ var activeHARs, activeCoverage, BROWSER_TOOLS;
12207
12567
  var init_ai_client = __esm(() => {
12208
12568
  init_types();
12569
+ activeHARs = new Map;
12570
+ activeCoverage = new Map;
12209
12571
  BROWSER_TOOLS = [
12210
12572
  {
12211
12573
  name: "navigate",
@@ -12508,6 +12870,218 @@ var init_ai_client = __esm(() => {
12508
12870
  },
12509
12871
  required: ["status", "reasoning"]
12510
12872
  }
12873
+ },
12874
+ {
12875
+ name: "browser_snapshot",
12876
+ description: "Take an ARIA accessibility snapshot of the current page. Returns a tree of interactive elements with refs (e.g. @e0, @e1) that can be used with browser_*_ref tools. Use this before interacting with elements to discover their refs.",
12877
+ input_schema: {
12878
+ type: "object",
12879
+ properties: {}
12880
+ }
12881
+ },
12882
+ {
12883
+ name: "browser_click_ref",
12884
+ description: "Click an element by its snapshot ref (e.g. @e0). More reliable than CSS selectors because it uses ARIA role-based resolution.",
12885
+ input_schema: {
12886
+ type: "object",
12887
+ properties: {
12888
+ ref: { type: "string", description: "Element ref from snapshot (e.g. @e0)." }
12889
+ },
12890
+ required: ["ref"]
12891
+ }
12892
+ },
12893
+ {
12894
+ name: "browser_type_ref",
12895
+ description: "Type text into an element by its snapshot ref. Optionally clears existing text first.",
12896
+ input_schema: {
12897
+ type: "object",
12898
+ properties: {
12899
+ ref: { type: "string", description: "Element ref from snapshot (e.g. @e0)." },
12900
+ text: { type: "string", description: "Text to type." },
12901
+ clear: { type: "boolean", description: "Clear existing text before typing (default: false)." }
12902
+ },
12903
+ required: ["ref", "text"]
12904
+ }
12905
+ },
12906
+ {
12907
+ name: "browser_fill_ref",
12908
+ description: "Fill an input element by its snapshot ref. Replaces existing content.",
12909
+ input_schema: {
12910
+ type: "object",
12911
+ properties: {
12912
+ ref: { type: "string", description: "Element ref from snapshot (e.g. @e0)." },
12913
+ value: { type: "string", description: "Value to fill." }
12914
+ },
12915
+ required: ["ref", "value"]
12916
+ }
12917
+ },
12918
+ {
12919
+ name: "browser_select_ref",
12920
+ description: "Select an option in a dropdown element by its snapshot ref.",
12921
+ input_schema: {
12922
+ type: "object",
12923
+ properties: {
12924
+ ref: { type: "string", description: "Element ref from snapshot (e.g. @e0)." },
12925
+ value: { type: "string", description: "Option value to select." }
12926
+ },
12927
+ required: ["ref", "value"]
12928
+ }
12929
+ },
12930
+ {
12931
+ name: "browser_check_ref",
12932
+ description: "Check or uncheck a checkbox/radio element by its snapshot ref.",
12933
+ input_schema: {
12934
+ type: "object",
12935
+ properties: {
12936
+ ref: { type: "string", description: "Element ref from snapshot (e.g. @e0)." },
12937
+ checked: { type: "boolean", description: "True to check, false to uncheck." }
12938
+ },
12939
+ required: ["ref", "checked"]
12940
+ }
12941
+ },
12942
+ {
12943
+ name: "browser_hover_ref",
12944
+ description: "Hover over an element by its snapshot ref.",
12945
+ input_schema: {
12946
+ type: "object",
12947
+ properties: {
12948
+ ref: { type: "string", description: "Element ref from snapshot (e.g. @e0)." }
12949
+ },
12950
+ required: ["ref"]
12951
+ }
12952
+ },
12953
+ {
12954
+ name: "browser_check",
12955
+ description: "Get a comprehensive page orientation: page info (URL, title, meta, link/image/form counts, console errors, viewport) plus an ARIA accessibility snapshot with interactive element refs. Use this to understand the current page state before testing.",
12956
+ input_schema: {
12957
+ type: "object",
12958
+ properties: {}
12959
+ }
12960
+ },
12961
+ {
12962
+ name: "browser_assert",
12963
+ description: "Run an assertion against the current page state. Supports: element_exists (check selector exists/visible), text_contains (check page text includes substring), url_matches (check URL matches pattern), title_contains (check page title includes substring). Returns pass/fail with details.",
12964
+ input_schema: {
12965
+ type: "object",
12966
+ properties: {
12967
+ assertion: {
12968
+ type: "string",
12969
+ enum: ["element_exists", "text_contains", "url_matches", "title_contains"],
12970
+ description: "The type of assertion to run."
12971
+ },
12972
+ selector: {
12973
+ type: "string",
12974
+ description: "CSS selector (required for element_exists)."
12975
+ },
12976
+ expected: {
12977
+ type: "string",
12978
+ description: "Expected value (text substring, URL pattern, or title substring)."
12979
+ },
12980
+ visible: {
12981
+ type: "boolean",
12982
+ description: "For element_exists: also require the element to be visible (default true)."
12983
+ }
12984
+ },
12985
+ required: ["assertion"]
12986
+ }
12987
+ },
12988
+ {
12989
+ name: "browser_extract",
12990
+ description: "Extract structured data from the current page. Modes: 'structured' (tables, lists, JSON-LD, OpenGraph, meta), 'table' (a specific HTML table), 'text' (page or element text), 'aria' (accessibility snapshot). Returns extracted data as JSON.",
12991
+ input_schema: {
12992
+ type: "object",
12993
+ properties: {
12994
+ mode: {
12995
+ type: "string",
12996
+ enum: ["structured", "table", "text", "aria"],
12997
+ description: "Extraction mode: 'structured' for auto-detected tables/lists/metadata, 'table' for a specific table selector, 'text' for page/element text, 'aria' for accessibility tree."
12998
+ },
12999
+ selector: {
13000
+ type: "string",
13001
+ description: "CSS selector for targeted extraction (required for 'table' mode, optional for 'text')."
13002
+ }
13003
+ },
13004
+ required: ["mode"]
13005
+ }
13006
+ },
13007
+ {
13008
+ name: "browser_crawl",
13009
+ description: "Crawl a website starting from a URL. Discovers linked pages within the same domain, up to a configurable depth. Returns a list of found URLs with their status codes and titles.",
13010
+ input_schema: {
13011
+ type: "object",
13012
+ properties: {
13013
+ url: {
13014
+ type: "string",
13015
+ description: "Starting URL to crawl from."
13016
+ },
13017
+ maxDepth: {
13018
+ type: "number",
13019
+ description: "Maximum crawl depth (default 2)."
13020
+ },
13021
+ maxPages: {
13022
+ type: "number",
13023
+ description: "Maximum number of pages to visit (default 20)."
13024
+ }
13025
+ },
13026
+ required: ["url"]
13027
+ }
13028
+ },
13029
+ {
13030
+ name: "browser_intercept",
13031
+ description: "Intercept network requests on the current page. Actions: 'block' (block matching requests), 'modify' (rewrite response), 'log' (log matching requests), 'clear' (remove all rules), 'har_start' (start HAR recording), 'har_stop' (stop and return HAR).",
13032
+ input_schema: {
13033
+ type: "object",
13034
+ properties: {
13035
+ action: {
13036
+ type: "string",
13037
+ enum: ["block", "modify", "log", "clear", "har_start", "har_stop"],
13038
+ description: "Intercept action to perform."
13039
+ },
13040
+ pattern: {
13041
+ type: "string",
13042
+ description: "URL pattern to match (required for block/modify/log)."
13043
+ },
13044
+ response: {
13045
+ type: "object",
13046
+ description: "Custom response for 'modify' action.",
13047
+ properties: {
13048
+ status: { type: "number" },
13049
+ body: { type: "string" },
13050
+ headers: { type: "object" }
13051
+ }
13052
+ }
13053
+ },
13054
+ required: ["action"]
13055
+ }
13056
+ },
13057
+ {
13058
+ name: "browser_performance",
13059
+ description: "Measure page performance. Modes: 'metrics' (Web Vitals: FCP, LCP, CLS, TTFB), 'deep' (full resource breakdown, third-party analysis, DOM complexity, main thread blocking, memory), 'coverage_start' (start JS/CSS coverage), 'coverage_stop' (stop and report unused code).",
13060
+ input_schema: {
13061
+ type: "object",
13062
+ properties: {
13063
+ mode: {
13064
+ type: "string",
13065
+ enum: ["metrics", "deep", "coverage_start", "coverage_stop"],
13066
+ description: "Performance measurement mode."
13067
+ }
13068
+ },
13069
+ required: ["mode"]
13070
+ }
13071
+ },
13072
+ {
13073
+ name: "browser_a11y",
13074
+ description: "Run an accessibility audit on the current page using the ARIA accessibility tree. Detects missing labels, roles, focus issues, and other common a11y problems. Returns a list of issues with severity and element details.",
13075
+ input_schema: {
13076
+ type: "object",
13077
+ properties: {
13078
+ level: {
13079
+ type: "string",
13080
+ enum: ["A", "AA", "AAA"],
13081
+ description: "WCAG conformance level to check against (default AA)."
13082
+ }
13083
+ }
13084
+ }
12511
13085
  }
12512
13086
  ];
12513
13087
  });
@@ -15085,17 +15659,17 @@ function resolveTodosDbPath() {
15085
15659
  return envPath;
15086
15660
  return join12(homedir10(), ".todos", "todos.db");
15087
15661
  }
15088
- function connectToTodos() {
15662
+ function connectToTodos(options = {}) {
15089
15663
  const dbPath = resolveTodosDbPath();
15090
15664
  if (!existsSync10(dbPath)) {
15091
15665
  throw new TodosConnectionError(`Todos database not found at ${dbPath}. Install @hasna/todos or set TODOS_DB_PATH.`);
15092
15666
  }
15093
- const db2 = new Database3(dbPath, { readonly: true });
15667
+ const db2 = new Database3(dbPath, { readonly: options.readonly ?? true });
15094
15668
  db2.exec("PRAGMA foreign_keys = ON");
15095
15669
  return db2;
15096
15670
  }
15097
15671
  function pullTasks(options = {}) {
15098
- const db2 = connectToTodos();
15672
+ const db2 = connectToTodos({ readonly: true });
15099
15673
  try {
15100
15674
  let query = "SELECT id, short_id, title, description, status, priority, tags, project_id FROM tasks WHERE 1=1";
15101
15675
  const params = [];
@@ -15199,7 +15773,7 @@ async function createFailureTasks(run, failedResults, scenarios) {
15199
15773
  return { created: 0, skipped: 0 };
15200
15774
  let db2 = null;
15201
15775
  try {
15202
- db2 = connectToTodos();
15776
+ db2 = connectToTodos({ readonly: false });
15203
15777
  } catch {
15204
15778
  return { created: 0, skipped: 0 };
15205
15779
  }
@@ -15498,6 +16072,7 @@ async function runSingleScenario(scenario, runId, options) {
15498
16072
  screenshotter,
15499
16073
  model,
15500
16074
  runId,
16075
+ sessionId: result.id,
15501
16076
  maxTurns: effectiveOptions.minimal ? 10 : 30,
15502
16077
  a11y: effectiveOptions.a11y,
15503
16078
  persona: persona ? {
@@ -17370,6 +17945,681 @@ async function startWatcher(options) {
17370
17945
  process.on("SIGTERM", cleanup);
17371
17946
  await new Promise(() => {});
17372
17947
  }
17948
+ // src/lib/repo-discovery.ts
17949
+ init_paths();
17950
+ import { existsSync as existsSync13, readFileSync as readFileSync5, readdirSync as readdirSync3, statSync, writeFileSync as writeFileSync4, mkdirSync as mkdirSync10, unlinkSync } from "fs";
17951
+ import { createHash } from "crypto";
17952
+ import { join as join15, resolve as resolve2, relative as relative2 } from "path";
17953
+ function getCacheDir() {
17954
+ const testersDir = getTestersDir();
17955
+ const cacheDir = join15(testersDir, "repo-index");
17956
+ if (!existsSync13(cacheDir)) {
17957
+ mkdirSync10(cacheDir, { recursive: true });
17958
+ }
17959
+ return cacheDir;
17960
+ }
17961
+ function pathHash(repoPath) {
17962
+ return createHash("sha256").update(repoPath).digest("hex").slice(0, 16);
17963
+ }
17964
+ function getCachePath(repoPath) {
17965
+ return join15(getCacheDir(), `${pathHash(repoPath)}.json`);
17966
+ }
17967
+ function isCacheStale(cached, repoPath) {
17968
+ for (const spec of cached.specs) {
17969
+ const fullPath = join15(repoPath, spec.file);
17970
+ if (!existsSync13(fullPath))
17971
+ return true;
17972
+ try {
17973
+ const stat = statSync(fullPath);
17974
+ if (stat.mtimeMs !== spec.mtimeMs)
17975
+ return true;
17976
+ } catch {
17977
+ return true;
17978
+ }
17979
+ }
17980
+ if (cached.configPath) {
17981
+ const configFullPath = join15(repoPath, cached.configPath);
17982
+ if (!existsSync13(configFullPath))
17983
+ return true;
17984
+ try {
17985
+ const stat = statSync(configFullPath);
17986
+ const age = Date.now() - new Date(cached.snapshotAt).getTime();
17987
+ if (age > 3600000)
17988
+ return true;
17989
+ } catch {
17990
+ return true;
17991
+ }
17992
+ }
17993
+ return false;
17994
+ }
17995
+ function loadCache(repoPath) {
17996
+ const cachePath = getCachePath(repoPath);
17997
+ if (!existsSync13(cachePath))
17998
+ return null;
17999
+ try {
18000
+ const raw = JSON.parse(readFileSync5(cachePath, "utf-8"));
18001
+ return raw;
18002
+ } catch {
18003
+ return null;
18004
+ }
18005
+ }
18006
+ function saveCache(snapshot) {
18007
+ const cachePath = getCachePath(snapshot.repoPath);
18008
+ writeFileSync4(cachePath, JSON.stringify(snapshot, null, 2), "utf-8");
18009
+ }
18010
+ function detectPackageManager(repoPath) {
18011
+ const result = {
18012
+ npm: existsSync13(join15(repoPath, "package-lock.json")),
18013
+ yarn: existsSync13(join15(repoPath, "yarn.lock")),
18014
+ pnpm: existsSync13(join15(repoPath, "pnpm-lock.yaml")),
18015
+ bun: existsSync13(join15(repoPath, "bun.lockb")) || existsSync13(join15(repoPath, "bun.lock")),
18016
+ preferred: "npm"
18017
+ };
18018
+ if (result.bun)
18019
+ result.preferred = "bun";
18020
+ else if (result.pnpm)
18021
+ result.preferred = "pnpm";
18022
+ else if (result.yarn)
18023
+ result.preferred = "yarn";
18024
+ else
18025
+ result.preferred = "npm";
18026
+ return result;
18027
+ }
18028
+ function detectDevScripts(repoPath) {
18029
+ const pkgPath = join15(repoPath, "package.json");
18030
+ if (!existsSync13(pkgPath)) {
18031
+ return { dev: null, test: null, seed: null, build: null };
18032
+ }
18033
+ let scripts;
18034
+ try {
18035
+ const pkg = JSON.parse(readFileSync5(pkgPath, "utf-8"));
18036
+ scripts = pkg.scripts ?? {};
18037
+ } catch {
18038
+ return { dev: null, test: null, seed: null, build: null };
18039
+ }
18040
+ const dev = scripts.dev ?? scripts.start ?? scripts["dev:server"] ?? null;
18041
+ const test = scripts.test ?? scripts["test:e2e"] ?? scripts["test:playwright"] ?? scripts.e2e ?? null;
18042
+ const seed = scripts.seed ?? scripts["db:seed"] ?? scripts.seedDb ?? null;
18043
+ const build = scripts.build ?? scripts["build:test"] ?? null;
18044
+ return { dev, test, seed, build };
18045
+ }
18046
+ function findPlaywrightConfig(repoPath) {
18047
+ const candidates = [
18048
+ "playwright.config.ts",
18049
+ "playwright.config.js",
18050
+ "playwright.config.mjs",
18051
+ "playwright.config.cjs",
18052
+ "playwright-ct.config.ts",
18053
+ "playwright-ct.config.js"
18054
+ ];
18055
+ for (const name of candidates) {
18056
+ if (existsSync13(join15(repoPath, name)))
18057
+ return name;
18058
+ }
18059
+ return null;
18060
+ }
18061
+ function extractTestGlobPatterns(configPath, repoPath) {
18062
+ if (!configPath) {
18063
+ return ["**/*.spec.ts", "**/*.spec.js", "**/*.test.ts", "**/*.test.js", "**/e2e/**/*.ts", "**/e2e/**/*.js", "**/tests/**/*.ts", "**/tests/**/*.js"];
18064
+ }
18065
+ const fullPath = join15(repoPath, configPath);
18066
+ let content;
18067
+ try {
18068
+ content = readFileSync5(fullPath, "utf-8");
18069
+ } catch {
18070
+ return ["**/*.spec.ts", "**/*.test.ts"];
18071
+ }
18072
+ const patterns = [];
18073
+ const testDirMatch = content.match(/testDir\s*[:=]\s*['"`]([^'"`]+)['"`]/);
18074
+ const testDir = testDirMatch?.[1];
18075
+ const testMatchArray = content.match(/testMatch\s*[:=]\s*\[([^\]]+)\]/);
18076
+ if (testMatchArray) {
18077
+ const items = testMatchArray[1].match(/['"`]([^'"`]+)['"`]/g);
18078
+ if (items) {
18079
+ for (const item of items) {
18080
+ patterns.push(item.replace(/['"`]/g, ""));
18081
+ }
18082
+ }
18083
+ }
18084
+ const testMatchSingle = content.match(/testMatch\s*[:=]\s*['"`]([^'"`]+)['"`]/);
18085
+ if (testMatchSingle) {
18086
+ patterns.push(testMatchSingle[1]);
18087
+ }
18088
+ if (testDir && patterns.length === 0) {
18089
+ patterns.push(`${testDir}/**/*.spec.ts`, `${testDir}/**/*.test.ts`, `${testDir}/**/*.spec.js`, `${testDir}/**/*.test.js`);
18090
+ }
18091
+ if (patterns.length === 0) {
18092
+ patterns.push("**/*.spec.ts", "**/*.test.ts", "**/*.spec.js", "**/*.test.js");
18093
+ }
18094
+ return patterns;
18095
+ }
18096
+ function approximateTestCount(filePath) {
18097
+ let content;
18098
+ try {
18099
+ content = readFileSync5(filePath, "utf-8");
18100
+ } catch {
18101
+ return 0;
18102
+ }
18103
+ const testCalls = (content.match(/(?:^|\n)\s*(?:test|it)\(/gm) || []).length;
18104
+ return testCalls || 0;
18105
+ }
18106
+ function findSpecFiles(repoPath, globPatterns) {
18107
+ const specs = [];
18108
+ const seen = new Set;
18109
+ for (const pattern of globPatterns) {
18110
+ const dirsToSearch = ["", ".", "tests", "e2e", "test", "__tests__", "specs", "src"];
18111
+ for (const dir of dirsToSearch) {
18112
+ const searchDir = dir ? join15(repoPath, dir) : repoPath;
18113
+ if (!existsSync13(searchDir))
18114
+ continue;
18115
+ try {
18116
+ const files = walkDir(searchDir);
18117
+ for (const file of files) {
18118
+ const relativePath = relative2(repoPath, file);
18119
+ if (seen.has(relativePath))
18120
+ continue;
18121
+ if (matchesGlob(relativePath, pattern)) {
18122
+ seen.add(relativePath);
18123
+ const content = readFileSync5(file, "utf-8");
18124
+ const contentHash = createHash("sha256").update(content).digest("hex").slice(0, 16);
18125
+ const stat = statSync(file);
18126
+ specs.push({
18127
+ file: relativePath,
18128
+ fromGlob: pattern,
18129
+ testCount: approximateTestCount(file),
18130
+ mtimeMs: stat.mtimeMs,
18131
+ contentHash
18132
+ });
18133
+ }
18134
+ }
18135
+ } catch {}
18136
+ }
18137
+ }
18138
+ specs.sort((a, b) => a.file.localeCompare(b.file));
18139
+ return specs;
18140
+ }
18141
+ function walkDir(dir) {
18142
+ const results = [];
18143
+ try {
18144
+ const entries = readdirSync3(dir, { withFileTypes: true });
18145
+ for (const entry of entries) {
18146
+ const fullPath = join15(dir, entry.name);
18147
+ if (entry.isDirectory()) {
18148
+ if (entry.name === "node_modules" || entry.name === ".git")
18149
+ continue;
18150
+ results.push(...walkDir(fullPath));
18151
+ } else if (entry.isFile()) {
18152
+ results.push(fullPath);
18153
+ }
18154
+ }
18155
+ } catch {}
18156
+ return results;
18157
+ }
18158
+ function matchesGlob(filePath, pattern) {
18159
+ let regex = pattern.replace(/\./g, "\\.").replace(/\*\*\//g, "(.+/)?").replace(/\*/g, "[^/]*").replace(/\?/g, "[^/]");
18160
+ regex = "^" + regex + "$";
18161
+ return new RegExp(regex).test(filePath);
18162
+ }
18163
+ function detectSuggestedUrl(repoPath) {
18164
+ const pkgPath = join15(repoPath, "package.json");
18165
+ if (!existsSync13(pkgPath))
18166
+ return null;
18167
+ try {
18168
+ const pkg = JSON.parse(readFileSync5(pkgPath, "utf-8"));
18169
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
18170
+ if ("next" in deps)
18171
+ return "http://localhost:3000";
18172
+ if ("vite" in deps)
18173
+ return "http://localhost:5173";
18174
+ if (Object.keys(deps).some((d) => d.startsWith("@remix-run")))
18175
+ return "http://localhost:3000";
18176
+ if ("nuxt" in deps)
18177
+ return "http://localhost:3000";
18178
+ if (Object.keys(deps).some((d) => d.startsWith("@angular")))
18179
+ return "http://localhost:4200";
18180
+ } catch {}
18181
+ return null;
18182
+ }
18183
+ function checkPlaywrightBrowserInstalled(repoPath) {
18184
+ const cacheDir = join15(repoPath, "node_modules", ".cache", "ms-playwright");
18185
+ if (existsSync13(cacheDir))
18186
+ return true;
18187
+ const globalCache = join15(repoPath, ".cache", "ms-playwright");
18188
+ if (existsSync13(globalCache))
18189
+ return true;
18190
+ return false;
18191
+ }
18192
+ function getInstallCommand(pm) {
18193
+ switch (pm.preferred) {
18194
+ case "npm":
18195
+ return "npm install";
18196
+ case "yarn":
18197
+ return "yarn install";
18198
+ case "pnpm":
18199
+ return "pnpm install";
18200
+ case "bun":
18201
+ return "bun install";
18202
+ }
18203
+ }
18204
+ function getPlaywrightInstallCommand(pm) {
18205
+ return "npx playwright install";
18206
+ }
18207
+ function discoverRepo(opts) {
18208
+ const repoPath = resolve2(opts.repoPath);
18209
+ if (!opts.refresh) {
18210
+ const cached = loadCache(repoPath);
18211
+ if (cached && !isCacheStale(cached, repoPath)) {
18212
+ return cached;
18213
+ }
18214
+ }
18215
+ const configPath = findPlaywrightConfig(repoPath);
18216
+ let configRaw = null;
18217
+ if (configPath) {
18218
+ try {
18219
+ configRaw = readFileSync5(join15(repoPath, configPath), "utf-8");
18220
+ } catch {
18221
+ configRaw = null;
18222
+ }
18223
+ }
18224
+ const globPatterns = extractTestGlobPatterns(configPath, repoPath);
18225
+ const specs = findSpecFiles(repoPath, globPatterns);
18226
+ const packageManager = detectPackageManager(repoPath);
18227
+ const devScripts = detectDevScripts(repoPath);
18228
+ const playwrightInstalled = existsSync13(join15(repoPath, "node_modules", "playwright")) || existsSync13(join15(repoPath, "node_modules", "@playwright", "test"));
18229
+ const browsersInstalled = checkPlaywrightBrowserInstalled(repoPath);
18230
+ const configExists = configPath !== null;
18231
+ const specsFound = specs.length > 0;
18232
+ const issues = [];
18233
+ if (!configExists)
18234
+ issues.push("No Playwright config file found");
18235
+ if (!playwrightInstalled)
18236
+ issues.push("Playwright is not installed (node_modules/playwright missing)");
18237
+ if (!browsersInstalled)
18238
+ issues.push("Playwright browsers not installed (run `npx playwright install`)");
18239
+ if (!specsFound)
18240
+ issues.push("No spec files found");
18241
+ const ready = issues.length === 0;
18242
+ const runPm = (script) => {
18243
+ const name = script.split(" ")[0];
18244
+ const pm = packageManager.preferred;
18245
+ if (pm === "npm")
18246
+ return `npm run ${name}`;
18247
+ return `${pm} run ${name}`;
18248
+ };
18249
+ const prep = {
18250
+ installCmd: playwrightInstalled ? null : getInstallCommand(packageManager),
18251
+ installBrowsersCmd: browsersInstalled ? null : getPlaywrightInstallCommand(packageManager),
18252
+ startDevCmd: devScripts.dev ? runPm(devScripts.dev) : null,
18253
+ buildCmd: devScripts.build ? runPm(devScripts.build) : null,
18254
+ seedCmd: devScripts.seed ? runPm(devScripts.seed) : null
18255
+ };
18256
+ const suggestedUrl = opts.baseUrl ?? detectSuggestedUrl(repoPath);
18257
+ const workingDir = opts.workingDir ?? repoPath;
18258
+ const specHashes = specs.map((s) => s.contentHash).join(",");
18259
+ const cacheKey = createHash("sha256").update(`${repoPath}:${specHashes}:${configRaw ?? ""}`).digest("hex").slice(0, 16);
18260
+ const snapshot = {
18261
+ repoPath,
18262
+ configPath,
18263
+ configRaw,
18264
+ specs,
18265
+ totalTests: specs.reduce((sum, s) => sum + s.testCount, 0),
18266
+ packageManager,
18267
+ devScripts,
18268
+ readiness: {
18269
+ playwrightInstalled,
18270
+ browsersInstalled,
18271
+ configExists,
18272
+ specsFound,
18273
+ ready,
18274
+ issues
18275
+ },
18276
+ prep,
18277
+ suggestedUrl,
18278
+ workingDir,
18279
+ snapshotAt: new Date().toISOString(),
18280
+ cacheKey
18281
+ };
18282
+ saveCache(snapshot);
18283
+ return snapshot;
18284
+ }
18285
+ function clearDiscoveryCache(repoPath) {
18286
+ const cacheDir = getCacheDir();
18287
+ if (!existsSync13(cacheDir))
18288
+ return;
18289
+ if (repoPath) {
18290
+ const cachePath = getCachePath(repoPath);
18291
+ if (existsSync13(cachePath)) {
18292
+ unlinkSync(cachePath);
18293
+ }
18294
+ } else {
18295
+ for (const file of readdirSync3(cacheDir)) {
18296
+ if (file.endsWith(".json")) {
18297
+ unlinkSync(join15(cacheDir, file));
18298
+ }
18299
+ }
18300
+ }
18301
+ }
18302
+ function getDiscoveryCacheInfo(repoPath) {
18303
+ const cachePath = getCachePath(repoPath);
18304
+ if (!existsSync13(cachePath))
18305
+ return null;
18306
+ const cached = loadCache(repoPath);
18307
+ if (!cached)
18308
+ return null;
18309
+ return {
18310
+ cached: true,
18311
+ stale: isCacheStale(cached, repoPath),
18312
+ path: cachePath
18313
+ };
18314
+ }
18315
+ // src/lib/repo-executor.ts
18316
+ init_runs();
18317
+ init_database();
18318
+ init_paths();
18319
+ import { execSync as execSync2 } from "child_process";
18320
+ import { existsSync as existsSync14, mkdirSync as mkdirSync11, writeFileSync as writeFileSync5 } from "fs";
18321
+ import { join as join16 } from "path";
18322
+ function resolvePlaywrightCmd(repoPath) {
18323
+ const localPw = join16(repoPath, "node_modules", ".bin", "playwright");
18324
+ if (existsSync14(localPw)) {
18325
+ return [localPw, "test"];
18326
+ }
18327
+ return ["npx", "playwright", "test"];
18328
+ }
18329
+ function buildPlaywrightArgs(specFiles, extraArgs = []) {
18330
+ const args = [];
18331
+ if (specFiles.length > 0) {
18332
+ args.push(...specFiles);
18333
+ }
18334
+ args.push("--reporter", "json");
18335
+ if (extraArgs.length > 0) {
18336
+ args.push(...extraArgs);
18337
+ }
18338
+ return args;
18339
+ }
18340
+ function runPlaywright(repoPath, workingDir, specFiles, extraArgs, timeoutMs) {
18341
+ const cmd = resolvePlaywrightCmd(repoPath);
18342
+ const args = buildPlaywrightArgs(specFiles, extraArgs, workingDir);
18343
+ const startTime = Date.now();
18344
+ try {
18345
+ const result = execSync2(`${cmd.join(" ")} ${args.join(" ")}`, {
18346
+ cwd: workingDir,
18347
+ encoding: "utf-8",
18348
+ timeout: timeoutMs,
18349
+ maxBuffer: 50 * 1024 * 1024,
18350
+ env: { ...process.env, CI: "1" }
18351
+ }).toString();
18352
+ return {
18353
+ exitCode: 0,
18354
+ stdout: result,
18355
+ stderr: "",
18356
+ durationMs: Date.now() - startTime
18357
+ };
18358
+ } catch (err) {
18359
+ const stdout = err.stdout?.toString() ?? "";
18360
+ const stderr = err.stderr?.toString() ?? "";
18361
+ const exitCode = err.status ?? err.code ?? -1;
18362
+ return {
18363
+ exitCode: typeof exitCode === "number" ? exitCode : -1,
18364
+ stdout,
18365
+ stderr,
18366
+ durationMs: Date.now() - startTime
18367
+ };
18368
+ }
18369
+ }
18370
+ function parsePlaywrightJsonOutput(stdout, stderr) {
18371
+ const testResults = [];
18372
+ try {
18373
+ const obj = JSON.parse(stdout);
18374
+ if (obj.suites) {
18375
+ for (const suite of obj.suites) {
18376
+ collectTestsFromSuite(suite, testResults);
18377
+ }
18378
+ }
18379
+ if (obj.tests && Array.isArray(obj.tests)) {
18380
+ for (const test of obj.tests) {
18381
+ testResults.push({
18382
+ name: test.title || test.name || "unknown test",
18383
+ status: test.outcome === "expected" ? "passed" : test.outcome === "skipped" ? "skipped" : "failed"
18384
+ });
18385
+ }
18386
+ }
18387
+ } catch {
18388
+ const lines = stdout.split(`
18389
+ `);
18390
+ for (const line of lines) {
18391
+ if (!line.trim())
18392
+ continue;
18393
+ try {
18394
+ const obj = JSON.parse(line);
18395
+ if (obj.suites) {
18396
+ for (const suite of obj.suites) {
18397
+ collectTestsFromSuite(suite, testResults);
18398
+ }
18399
+ }
18400
+ if (obj.tests && Array.isArray(obj.tests)) {
18401
+ for (const test of obj.tests) {
18402
+ testResults.push({
18403
+ name: test.title || test.name || "unknown test",
18404
+ status: test.outcome === "expected" ? "passed" : test.outcome === "skipped" ? "skipped" : "failed"
18405
+ });
18406
+ }
18407
+ }
18408
+ } catch {}
18409
+ }
18410
+ }
18411
+ if (testResults.length === 0) {
18412
+ const passed = (stdout.match(/\u2713/g) || []).length;
18413
+ const failed = (stdout.match(/\u2717/g) || []).length;
18414
+ if (passed > 0 || failed > 0) {
18415
+ testResults.push({ name: `${passed} passed, ${failed} failed`, status: failed > 0 ? "failed" : "passed" });
18416
+ } else {
18417
+ testResults.push({ name: "suite", status: stdout.includes("Error") ? "failed" : "passed" });
18418
+ }
18419
+ }
18420
+ return testResults;
18421
+ }
18422
+ function collectTestsFromSuite(suite, results) {
18423
+ if (suite.specs) {
18424
+ for (const spec of suite.specs) {
18425
+ const title = spec.title || spec.name || "unknown test";
18426
+ if (spec.tests && Array.isArray(spec.tests)) {
18427
+ for (const t of spec.tests) {
18428
+ results.push({
18429
+ name: title,
18430
+ status: t.outcome === "expected" ? "passed" : t.outcome === "skipped" ? "skipped" : "failed"
18431
+ });
18432
+ }
18433
+ }
18434
+ }
18435
+ }
18436
+ if (suite.suites) {
18437
+ for (const sub of suite.suites) {
18438
+ collectTestsFromSuite(sub, results);
18439
+ }
18440
+ }
18441
+ }
18442
+ function determineSpecStatus(exitCode, testResults) {
18443
+ if (exitCode === null)
18444
+ return "error";
18445
+ if (exitCode === 0)
18446
+ return "passed";
18447
+ if (testResults.length > 0 && testResults.every((t) => t.status === "passed"))
18448
+ return "passed";
18449
+ if (exitCode > 128)
18450
+ return "error";
18451
+ return "failed";
18452
+ }
18453
+ function runSingleSpec(repoPath, workingDir, spec, extraArgs, timeoutMs) {
18454
+ const result = runPlaywright(repoPath, workingDir, [spec.file], extraArgs, timeoutMs);
18455
+ const testResults = parsePlaywrightJsonOutput(result.stdout, result.stderr);
18456
+ const status = determineSpecStatus(result.exitCode, testResults);
18457
+ return {
18458
+ specFile: spec.file,
18459
+ status,
18460
+ exitCode: result.exitCode,
18461
+ stdout: result.stdout,
18462
+ stderr: result.stderr,
18463
+ durationMs: result.durationMs,
18464
+ testResults,
18465
+ error: result.exitCode !== 0 && result.stderr ? result.stderr.slice(0, 2000) : undefined
18466
+ };
18467
+ }
18468
+ async function runRepoTests(opts) {
18469
+ const { snapshot } = opts;
18470
+ const specFiles = opts.specFiles ?? snapshot.specs.map((s) => s.file);
18471
+ const timeout = opts.timeout ?? 300000;
18472
+ const workingDir = opts.snapshot.workingDir;
18473
+ const repoPath = snapshot.repoPath;
18474
+ const url = opts.url ?? snapshot.suggestedUrl ?? "http://localhost:3000";
18475
+ const run = createRun({
18476
+ projectId: opts.projectId,
18477
+ url,
18478
+ model: opts.model ?? "repo-native",
18479
+ headed: false,
18480
+ parallel: 1,
18481
+ metadata: {
18482
+ runType: "repo-native",
18483
+ repoPath,
18484
+ configPath: snapshot.configPath,
18485
+ cacheKey: snapshot.cacheKey,
18486
+ label: opts.label
18487
+ }
18488
+ });
18489
+ const specResults = [];
18490
+ const startTime = Date.now();
18491
+ for (const specFile of specFiles) {
18492
+ const spec = snapshot.specs.find((s) => s.file === specFile);
18493
+ if (!spec)
18494
+ continue;
18495
+ const result = runSingleSpec(repoPath, workingDir, spec, opts.extraArgs ?? [], timeout);
18496
+ specResults.push(result);
18497
+ const resultId = uuid();
18498
+ const timestamp = now();
18499
+ const db2 = getDatabase();
18500
+ db2.exec("PRAGMA foreign_keys = OFF");
18501
+ try {
18502
+ const reasoning = result.status === "passed" ? "All tests passed" : (result.error ?? "").slice(0, 500) || null;
18503
+ const errorStr = result.status !== "passed" ? result.error ?? null : null;
18504
+ db2.query(`
18505
+ INSERT INTO results (id, run_id, scenario_id, status, reasoning, error, steps_completed, steps_total, duration_ms, model, tokens_used, cost_cents, metadata, created_at, persona_id, persona_name)
18506
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, 0, ?, ?, NULL, NULL)
18507
+ `).run(resultId, run.id, "__repo__", result.status, reasoning, errorStr, result.testResults.filter((t) => t.status === "passed").length, result.testResults.length || 1, result.durationMs, "repo-native", JSON.stringify({
18508
+ specFile: result.specFile,
18509
+ exitCode: result.exitCode,
18510
+ testResults: result.testResults
18511
+ }), timestamp);
18512
+ } finally {
18513
+ db2.exec("PRAGMA foreign_keys = ON");
18514
+ }
18515
+ const resultRecord = { id: resultId };
18516
+ if (result.stdout || result.stderr) {
18517
+ const reportersDir = join16(getTestersDir(), "repo-run-output");
18518
+ mkdirSync11(reportersDir, { recursive: true });
18519
+ const outputFile = join16(reportersDir, `${resultRecord.id}.log`);
18520
+ writeFileSync5(outputFile, `=== stdout ===
18521
+ ${result.stdout}
18522
+
18523
+ === stderr ===
18524
+ ${result.stderr}
18525
+ `);
18526
+ }
18527
+ }
18528
+ const durationMs = Date.now() - startTime;
18529
+ const passed = specResults.filter((r) => r.status === "passed").length;
18530
+ const failed = specResults.filter((r) => r.status === "failed").length;
18531
+ const skipped = specResults.filter((r) => r.status === "skipped").length;
18532
+ const errored = specResults.filter((r) => r.status === "error").length;
18533
+ const status = failed > 0 || errored > 0 ? "failed" : "passed";
18534
+ const runMeta = run.metadata ?? {};
18535
+ updateRun(run.id, {
18536
+ status,
18537
+ total: specResults.length,
18538
+ passed,
18539
+ failed: failed + errored,
18540
+ metadata: JSON.stringify({
18541
+ ...runMeta,
18542
+ specResults: specResults.map((r) => ({
18543
+ specFile: r.specFile,
18544
+ status: r.status,
18545
+ exitCode: r.exitCode,
18546
+ testCount: r.testResults.length,
18547
+ durationMs: r.durationMs
18548
+ }))
18549
+ })
18550
+ });
18551
+ return {
18552
+ runId: run.id,
18553
+ specResults,
18554
+ total: specResults.length,
18555
+ passed,
18556
+ failed,
18557
+ skipped,
18558
+ errored,
18559
+ durationMs,
18560
+ status
18561
+ };
18562
+ }
18563
+ function runPrep(snapshot, steps) {
18564
+ const { prep } = snapshot;
18565
+ const results = [];
18566
+ for (const step of steps) {
18567
+ let cmd = null;
18568
+ switch (step) {
18569
+ case "install":
18570
+ cmd = prep.installCmd;
18571
+ break;
18572
+ case "browsers":
18573
+ cmd = prep.installBrowsersCmd;
18574
+ break;
18575
+ case "dev":
18576
+ cmd = prep.startDevCmd;
18577
+ break;
18578
+ case "build":
18579
+ cmd = prep.buildCmd;
18580
+ break;
18581
+ case "seed":
18582
+ cmd = prep.seedCmd;
18583
+ break;
18584
+ }
18585
+ if (!cmd) {
18586
+ results.push({ cmd: step, success: true, output: "Not needed (already satisfied)", durationMs: 0 });
18587
+ continue;
18588
+ }
18589
+ const startTime = Date.now();
18590
+ try {
18591
+ const output = execSync2(cmd, {
18592
+ cwd: snapshot.workingDir,
18593
+ encoding: "utf-8",
18594
+ timeout: 120000,
18595
+ maxBuffer: 50 * 1024 * 1024,
18596
+ env: { ...processSyncEnv() }
18597
+ });
18598
+ results.push({
18599
+ cmd,
18600
+ success: true,
18601
+ output: output.slice(0, 1000),
18602
+ durationMs: Date.now() - startTime
18603
+ });
18604
+ } catch (err) {
18605
+ const stdout = err.stdout?.toString() ?? "";
18606
+ const stderr = err.stderr?.toString() ?? "";
18607
+ results.push({
18608
+ cmd,
18609
+ success: false,
18610
+ output: (stderr || stdout).slice(0, 1000),
18611
+ durationMs: Date.now() - startTime
18612
+ });
18613
+ }
18614
+ }
18615
+ return {
18616
+ steps: results,
18617
+ allSucceeded: results.every((r) => r.success)
18618
+ };
18619
+ }
18620
+ function processSyncEnv() {
18621
+ return { ...process.env, CI: "1", FORCE_COLOR: "0" };
18622
+ }
17373
18623
  // src/lib/prod-debug.ts
17374
18624
  var UUID_RE = /\b[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\b/i;
17375
18625
  var SENSITIVE_PARAM_RE = /token|secret|key|password|code|state|cookie|session|grant|credential|auth|jwt|access/i;
@@ -17869,6 +19119,68 @@ async function postGitHubComment(run, results, options) {
17869
19119
  return false;
17870
19120
  }
17871
19121
  }
19122
+ // src/db/sessions.ts
19123
+ init_database();
19124
+ function createSession(input) {
19125
+ const db2 = getDatabase();
19126
+ const id = input.sessionId ?? uuid();
19127
+ const timestamp = now();
19128
+ db2.query(`
19129
+ INSERT INTO sessions (id, tab_id, url, title, entries, entry_count, error_count, console_count, nav_count, status, start_time, end_time, created_at)
19130
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
19131
+ `).run(id, input.tabId, input.url ?? null, input.title ?? null, input.entries, input.entryCount, input.errorCount ?? 0, input.consoleCount ?? 0, input.navCount ?? 0, input.status, input.startTime, input.endTime ?? null, timestamp);
19132
+ return getSession(id);
19133
+ }
19134
+ function getSession(id) {
19135
+ const db2 = getDatabase();
19136
+ let row = db2.query("SELECT * FROM sessions WHERE id = ?").get(id);
19137
+ if (row)
19138
+ return sessionFromRow(row);
19139
+ const fullId = resolvePartialId("sessions", id);
19140
+ if (fullId) {
19141
+ row = db2.query("SELECT * FROM sessions WHERE id = ?").get(fullId);
19142
+ if (row)
19143
+ return sessionFromRow(row);
19144
+ }
19145
+ return null;
19146
+ }
19147
+ function listSessions(limit = 50, offset = 0) {
19148
+ const db2 = getDatabase();
19149
+ const rows = db2.query("SELECT * FROM sessions ORDER BY created_at DESC LIMIT ? OFFSET ?").all(limit, offset);
19150
+ return rows.map(sessionFromRow);
19151
+ }
19152
+ function deleteSession(id) {
19153
+ const db2 = getDatabase();
19154
+ const result = db2.query("DELETE FROM sessions WHERE id = ?").run(id);
19155
+ return result.changes > 0;
19156
+ }
19157
+ function searchSessions(query, limit = 20) {
19158
+ const db2 = getDatabase();
19159
+ const rows = db2.query("SELECT * FROM sessions WHERE url LIKE ? OR title LIKE ? ORDER BY created_at DESC LIMIT ?").all(`%${query}%`, `%${query}%`, limit);
19160
+ return rows.map(sessionFromRow);
19161
+ }
19162
+ function countSessions() {
19163
+ const db2 = getDatabase();
19164
+ const row = db2.query("SELECT COUNT(*) as count FROM sessions").get();
19165
+ return row.count;
19166
+ }
19167
+ function sessionFromRow(row) {
19168
+ return {
19169
+ id: row.id,
19170
+ tabId: row.tab_id,
19171
+ url: row.url,
19172
+ title: row.title,
19173
+ entries: JSON.parse(row.entries),
19174
+ entryCount: row.entry_count,
19175
+ errorCount: row.error_count,
19176
+ consoleCount: row.console_count,
19177
+ navCount: row.nav_count,
19178
+ status: row.status,
19179
+ startTime: row.start_time,
19180
+ endTime: row.end_time,
19181
+ createdAt: row.created_at
19182
+ };
19183
+ }
17872
19184
  export {
17873
19185
  writeScenarioMeta,
17874
19186
  writeRunMeta,
@@ -17886,11 +19198,14 @@ export {
17886
19198
  slugify,
17887
19199
  shouldRunAt,
17888
19200
  shortUuid,
19201
+ searchSessions,
17889
19202
  screenshotFromRow,
17890
19203
  scheduleFromRow,
17891
19204
  scenarioFromRow,
17892
19205
  runSmoke,
17893
19206
  runSingleScenario,
19207
+ runRepoTests,
19208
+ runPrep,
17894
19209
  runFromRow,
17895
19210
  runByFilter,
17896
19211
  runBatch,
@@ -17919,6 +19234,7 @@ export {
17919
19234
  loadConfig,
17920
19235
  listWebhooks,
17921
19236
  listTemplateNames,
19237
+ listSessions,
17922
19238
  listScreenshots,
17923
19239
  listSchedules,
17924
19240
  listScenarios,
@@ -17941,6 +19257,7 @@ export {
17941
19257
  getTransitiveDependencies,
17942
19258
  getTemplate,
17943
19259
  getStarterScenarios,
19260
+ getSession,
17944
19261
  getScreenshotsByResult,
17945
19262
  getScreenshotDir,
17946
19263
  getScreenshot,
@@ -17958,6 +19275,7 @@ export {
17958
19275
  getFlow,
17959
19276
  getExitCode,
17960
19277
  getEnabledSchedules,
19278
+ getDiscoveryCacheInfo,
17961
19279
  getDependents,
17962
19280
  getDependencies,
17963
19281
  getDefaultConfig,
@@ -17989,15 +19307,18 @@ export {
17989
19307
  ensurePersonaAuthenticated,
17990
19308
  ensureDir,
17991
19309
  dispatchWebhooks,
19310
+ discoverRepo,
17992
19311
  diffRuns,
17993
19312
  detectFramework,
17994
19313
  deleteWebhook,
19314
+ deleteSession,
17995
19315
  deleteSchedule,
17996
19316
  deleteScenario,
17997
19317
  deleteRun,
17998
19318
  deleteFlow,
17999
19319
  deleteAuthPreset,
18000
19320
  createWebhook,
19321
+ createSession,
18001
19322
  createScreenshot,
18002
19323
  createSchedule,
18003
19324
  createScenario,
@@ -18008,10 +19329,12 @@ export {
18008
19329
  createFlow,
18009
19330
  createClient,
18010
19331
  createAuthPreset,
19332
+ countSessions,
18011
19333
  connectToTodos,
18012
19334
  closeLightpanda,
18013
19335
  closeDatabase,
18014
19336
  closeBrowser,
19337
+ clearDiscoveryCache,
18015
19338
  checkBudget,
18016
19339
  agentFromRow,
18017
19340
  addDependency,