@openqa/cli 2.1.0 → 2.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -14,6 +14,8 @@
14
14
  <a href="https://github.com/Orka-Community/OpenQA/blob/main/LICENSE"><img src="https://img.shields.io/npm/l/@openqa/cli.svg" alt="license"></a>
15
15
  <a href="https://openqa.orkajs.com"><img src="https://img.shields.io/badge/docs-orkajs.com-blue.svg" alt="documentation"></a>
16
16
  <a href="https://discord.com/invite/DScfpuPysP"><img src="https://img.shields.io/badge/discord-join%20chat-7289da.svg" alt="discord"></a>
17
+ <a href="https://hub.docker.com/r/orklab/openqa"><img src="https://img.shields.io/docker/v/orklab/openqa?label=docker&logo=docker&color=0db7ed" alt="Docker Hub"></a>
18
+ <a href="https://hub.docker.com/r/orklab/openqa"><img src="https://img.shields.io/docker/pulls/orklab/openqa.svg" alt="Docker Pulls"></a>
17
19
  </p>
18
20
 
19
21
  ---
@@ -48,6 +50,30 @@ OpenQA is a **truly autonomous** QA testing agent that thinks, codes, and execut
48
50
  - **Regression Tests** - Verify bug fixes
49
51
  - **Performance Tests** - Load times, resource usage
50
52
 
53
+ ## 🐳 Docker Hub
54
+
55
+ OpenQA is available on Docker Hub at **[orklab/openqa](https://hub.docker.com/r/orklab/openqa)**.
56
+
57
+ ```bash
58
+ # Pull the latest image
59
+ docker pull orklab/openqa:latest
60
+
61
+ # Run with your API key
62
+ docker run -d \
63
+ -p 4242:3000 \
64
+ -e LLM_PROVIDER=openai \
65
+ -e OPENAI_API_KEY=sk-xxx \
66
+ -e OPENQA_JWT_SECRET=$(openssl rand -hex 32) \
67
+ -e SAAS_URL=https://your-app.com \
68
+ -v openqa-data:/app/data \
69
+ --name openqa \
70
+ orklab/openqa:latest
71
+ ```
72
+
73
+ Then open **http://localhost:4242** — first run will prompt you to create an admin account.
74
+
75
+ ---
76
+
51
77
  ## 🚀 Quick Start
52
78
 
53
79
  ### Development (Local)
@@ -771,9 +771,9 @@ function createQuickConfig(name, description, url, options) {
771
771
  // agent/brain/index.ts
772
772
  init_esm_shims();
773
773
  import { EventEmitter as EventEmitter2 } from "events";
774
- import { writeFileSync as writeFileSync2, readFileSync as readFileSync3, existsSync as existsSync2, mkdirSync as mkdirSync2 } from "fs";
775
- import { join as join3 } from "path";
776
- import { execSync } from "child_process";
774
+ import { writeFileSync as writeFileSync2, readFileSync as readFileSync3, existsSync as existsSync3, mkdirSync as mkdirSync2 } from "fs";
775
+ import { join as join4 } from "path";
776
+ import { execSync as execSync2 } from "child_process";
777
777
 
778
778
  // agent/brain/llm-resilience.ts
779
779
  init_esm_shims();
@@ -1011,10 +1011,150 @@ var ResilientLLM = class extends EventEmitter {
1011
1011
  }
1012
1012
  };
1013
1013
 
1014
+ // agent/brain/diff-analyzer.ts
1015
+ init_esm_shims();
1016
+ import { execSync } from "child_process";
1017
+ import { existsSync as existsSync2 } from "fs";
1018
+ import { join as join3, basename, dirname as dirname2 } from "path";
1019
+ var DiffAnalyzer = class {
1020
+ /**
1021
+ * Get files changed between current branch and base branch
1022
+ */
1023
+ getChangedFiles(repoPath, baseBranch = "main") {
1024
+ try {
1025
+ const output = execSync(`git diff --name-only ${baseBranch}...HEAD`, {
1026
+ cwd: repoPath,
1027
+ stdio: "pipe"
1028
+ }).toString().trim();
1029
+ if (!output) return [];
1030
+ return output.split("\n").filter(Boolean);
1031
+ } catch {
1032
+ try {
1033
+ const output = execSync("git diff --name-only HEAD~1", {
1034
+ cwd: repoPath,
1035
+ stdio: "pipe"
1036
+ }).toString().trim();
1037
+ if (!output) return [];
1038
+ return output.split("\n").filter(Boolean);
1039
+ } catch {
1040
+ return [];
1041
+ }
1042
+ }
1043
+ }
1044
+ /**
1045
+ * Map changed source files to their likely test files
1046
+ */
1047
+ mapFilesToTests(changedFiles, repoPath) {
1048
+ const testFiles = /* @__PURE__ */ new Set();
1049
+ for (const file of changedFiles) {
1050
+ if (!this.isSourceFile(file)) continue;
1051
+ const candidates = this.getTestCandidates(file);
1052
+ for (const candidate of candidates) {
1053
+ if (existsSync2(join3(repoPath, candidate))) {
1054
+ testFiles.add(candidate);
1055
+ }
1056
+ }
1057
+ }
1058
+ for (const file of changedFiles) {
1059
+ if (this.isTestFile(file)) {
1060
+ testFiles.add(file);
1061
+ }
1062
+ }
1063
+ return Array.from(testFiles);
1064
+ }
1065
+ /**
1066
+ * Analyze a diff and return risk assessment + affected tests
1067
+ */
1068
+ analyze(repoPath, baseBranch = "main") {
1069
+ const changedFiles = this.getChangedFiles(repoPath, baseBranch);
1070
+ const affectedTests = this.mapFilesToTests(changedFiles, repoPath);
1071
+ const riskLevel = this.assessRisk(changedFiles);
1072
+ const summary = this.buildSummary(changedFiles, affectedTests, riskLevel);
1073
+ return { changedFiles, affectedTests, riskLevel, summary };
1074
+ }
1075
+ getTestCandidates(filePath) {
1076
+ const dir = dirname2(filePath);
1077
+ const base = basename(filePath);
1078
+ const candidates = [];
1079
+ const nameMatch = base.match(/^(.+)\.(tsx?|jsx?|vue|svelte|py|go|rs)$/);
1080
+ if (!nameMatch) return candidates;
1081
+ const name = nameMatch[1];
1082
+ const ext = nameMatch[2];
1083
+ const testExts = ext.startsWith("ts") ? ["test.ts", "test.tsx", "spec.ts", "spec.tsx"] : ext.startsWith("js") ? ["test.js", "test.jsx", "spec.js", "spec.jsx"] : ext === "py" ? ["test.py"] : ext === "go" ? ["_test.go"] : [];
1084
+ for (const testExt of testExts) {
1085
+ candidates.push(join3(dir, `${name}.${testExt}`));
1086
+ candidates.push(join3(dir, "__tests__", `${name}.${testExt}`));
1087
+ candidates.push(join3(dir, "test", `${name}.${testExt}`));
1088
+ candidates.push(join3(dir, "tests", `${name}.${testExt}`));
1089
+ candidates.push(join3("__tests__", dir, `${name}.${testExt}`));
1090
+ }
1091
+ if (ext === "go") {
1092
+ candidates.push(join3(dir, `${name}_test.go`));
1093
+ }
1094
+ return candidates;
1095
+ }
1096
+ isSourceFile(file) {
1097
+ return /\.(tsx?|jsx?|vue|svelte|py|go|rs)$/.test(file) && !this.isTestFile(file);
1098
+ }
1099
+ isTestFile(file) {
1100
+ return /\.(test|spec)\.(tsx?|jsx?|py)$/.test(file) || /_test\.go$/.test(file) || file.includes("__tests__/");
1101
+ }
1102
+ assessRisk(changedFiles) {
1103
+ const highRiskPatterns = [
1104
+ /auth/i,
1105
+ /security/i,
1106
+ /middleware/i,
1107
+ /database/i,
1108
+ /migration/i,
1109
+ /config/i,
1110
+ /\.env/,
1111
+ /package\.json$/,
1112
+ /docker/i,
1113
+ /ci\//i,
1114
+ /payment/i,
1115
+ /billing/i,
1116
+ /permission/i
1117
+ ];
1118
+ const mediumRiskPatterns = [
1119
+ /api/i,
1120
+ /route/i,
1121
+ /controller/i,
1122
+ /service/i,
1123
+ /model/i,
1124
+ /hook/i,
1125
+ /context/i,
1126
+ /store/i,
1127
+ /util/i
1128
+ ];
1129
+ let highCount = 0;
1130
+ let mediumCount = 0;
1131
+ for (const file of changedFiles) {
1132
+ if (highRiskPatterns.some((p) => p.test(file))) highCount++;
1133
+ else if (mediumRiskPatterns.some((p) => p.test(file))) mediumCount++;
1134
+ }
1135
+ if (highCount >= 2 || highCount >= 1 && changedFiles.length > 5) return "high";
1136
+ if (highCount >= 1 || mediumCount >= 3) return "medium";
1137
+ return "low";
1138
+ }
1139
+ buildSummary(changedFiles, affectedTests, riskLevel) {
1140
+ const lines = [];
1141
+ lines.push(`${changedFiles.length} file(s) changed, ${affectedTests.length} test(s) affected.`);
1142
+ lines.push(`Risk level: ${riskLevel}.`);
1143
+ if (affectedTests.length > 0) {
1144
+ lines.push(`Run: ${affectedTests.slice(0, 5).join(", ")}${affectedTests.length > 5 ? ` (+${affectedTests.length - 5} more)` : ""}`);
1145
+ } else if (changedFiles.length > 0) {
1146
+ lines.push("No matching test files found \u2014 consider running full suite.");
1147
+ }
1148
+ return lines.join(" ");
1149
+ }
1150
+ };
1151
+
1014
1152
  // agent/brain/index.ts
1015
1153
  var OpenQABrain = class extends EventEmitter2 {
1016
1154
  db;
1017
1155
  llm;
1156
+ cache = new LLMCache();
1157
+ diffAnalyzer = new DiffAnalyzer();
1018
1158
  saasConfig;
1019
1159
  generatedTests = /* @__PURE__ */ new Map();
1020
1160
  dynamicAgents = /* @__PURE__ */ new Map();
@@ -1075,7 +1215,9 @@ Respond in JSON format:
1075
1215
  "suggestedAgents": ["Agent for X", "Agent for Y"],
1076
1216
  "risks": ["Risk 1", "Risk 2"]
1077
1217
  }`;
1078
- const response = await this.llm.generate(prompt);
1218
+ const cached = this.cache.get(prompt);
1219
+ const response = cached ?? await this.llm.generate(prompt);
1220
+ if (!cached) this.cache.set(prompt, response);
1079
1221
  try {
1080
1222
  const jsonMatch = response.match(/\{[\s\S]*\}/);
1081
1223
  if (jsonMatch) {
@@ -1094,11 +1236,11 @@ Respond in JSON format:
1094
1236
  async analyzeCodebase() {
1095
1237
  let repoPath = this.saasConfig.localPath;
1096
1238
  if (this.saasConfig.repoUrl && !this.saasConfig.localPath) {
1097
- repoPath = join3(this.workDir, "repo");
1098
- if (!existsSync2(repoPath)) {
1239
+ repoPath = join4(this.workDir, "repo");
1240
+ if (!existsSync3(repoPath)) {
1099
1241
  logger.info("Cloning repository", { url: this.saasConfig.repoUrl });
1100
1242
  try {
1101
- execSync(`git clone --depth 1 ${this.saasConfig.repoUrl} ${repoPath}`, {
1243
+ execSync2(`git clone --depth 1 ${this.saasConfig.repoUrl} ${repoPath}`, {
1102
1244
  stdio: "pipe"
1103
1245
  });
1104
1246
  } catch (e) {
@@ -1107,13 +1249,13 @@ Respond in JSON format:
1107
1249
  }
1108
1250
  }
1109
1251
  }
1110
- if (!repoPath || !existsSync2(repoPath)) {
1252
+ if (!repoPath || !existsSync3(repoPath)) {
1111
1253
  return "";
1112
1254
  }
1113
1255
  const analysis = [];
1114
1256
  try {
1115
- const packageJsonPath = join3(repoPath, "package.json");
1116
- if (existsSync2(packageJsonPath)) {
1257
+ const packageJsonPath = join4(repoPath, "package.json");
1258
+ if (existsSync3(packageJsonPath)) {
1117
1259
  const pkg = JSON.parse(readFileSync3(packageJsonPath, "utf-8"));
1118
1260
  analysis.push(`### Package Info`);
1119
1261
  analysis.push(`- Name: ${pkg.name}`);
@@ -1138,8 +1280,8 @@ Respond in JSON format:
1138
1280
  }
1139
1281
  const routePatterns = ["routes", "pages", "views", "controllers", "api"];
1140
1282
  for (const pattern of routePatterns) {
1141
- const routeDir = join3(repoPath, "src", pattern);
1142
- if (existsSync2(routeDir)) {
1283
+ const routeDir = join4(repoPath, "src", pattern);
1284
+ if (existsSync3(routeDir)) {
1143
1285
  const routes = this.findFiles(routeDir, [".ts", ".tsx", ".js", ".jsx"], 20);
1144
1286
  if (routes.length > 0) {
1145
1287
  analysis.push(`
@@ -1166,7 +1308,7 @@ Respond in JSON format:
1166
1308
  for (const item of items) {
1167
1309
  if (results.length >= limit) return;
1168
1310
  if (item.startsWith(".") || item === "node_modules" || item === "dist" || item === "build") continue;
1169
- const fullPath = join3(d, item);
1311
+ const fullPath = join4(d, item);
1170
1312
  const stat = statSync2(fullPath);
1171
1313
  if (stat.isDirectory()) {
1172
1314
  find(fullPath);
@@ -1212,7 +1354,9 @@ Respond with JSON:
1212
1354
  "code": "// complete test code here",
1213
1355
  "priority": 1-5
1214
1356
  }`;
1215
- const response = await this.llm.generate(prompt);
1357
+ const cached2 = this.cache.get(prompt);
1358
+ const response = cached2 ?? await this.llm.generate(prompt);
1359
+ if (!cached2) this.cache.set(prompt, response);
1216
1360
  let testData = { name: target, description: "", code: "", priority: 3 };
1217
1361
  try {
1218
1362
  const jsonMatch = response.match(/\{[\s\S]*\}/);
@@ -1239,7 +1383,7 @@ Respond with JSON:
1239
1383
  }
1240
1384
  saveTest(test) {
1241
1385
  const filename = `${test.type}_${test.id}.ts`;
1242
- const filepath = join3(this.testsDir, filename);
1386
+ const filepath = join4(this.testsDir, filename);
1243
1387
  const content = `/**
1244
1388
  * Generated by OpenQA
1245
1389
  * Type: ${test.type}
@@ -1277,7 +1421,9 @@ Respond with JSON:
1277
1421
  "prompt": "Complete system prompt for this agent (be specific and detailed)",
1278
1422
  "tools": ["tool1", "tool2"] // from: navigate, click, fill, screenshot, check_console, create_issue, create_ticket
1279
1423
  }`;
1280
- const response = await this.llm.generate(prompt);
1424
+ const cached3 = this.cache.get(prompt);
1425
+ const response = cached3 ?? await this.llm.generate(prompt);
1426
+ if (!cached3) this.cache.set(prompt, response);
1281
1427
  let agentData = { name: purpose, purpose, prompt: "", tools: [] };
1282
1428
  try {
1283
1429
  const jsonMatch = response.match(/\{[\s\S]*\}/);
@@ -1308,8 +1454,8 @@ Respond with JSON:
1308
1454
  test.executedAt = /* @__PURE__ */ new Date();
1309
1455
  this.emit("test-started", test);
1310
1456
  try {
1311
- if (test.targetFile && existsSync2(test.targetFile)) {
1312
- const result = execSync(`npx playwright test ${test.targetFile} --reporter=json`, {
1457
+ if (test.targetFile && existsSync3(test.targetFile)) {
1458
+ const result = execSync2(`npx playwright test ${test.targetFile} --reporter=json`, {
1313
1459
  cwd: this.testsDir,
1314
1460
  stdio: "pipe",
1315
1461
  timeout: 12e4
@@ -1363,7 +1509,9 @@ Respond with JSON:
1363
1509
  { "type": "analyze", "target": "what to analyze", "reason": "why" }
1364
1510
  ]
1365
1511
  }`;
1366
- const response = await this.llm.generate(prompt);
1512
+ const cached4 = this.cache.get(prompt);
1513
+ const response = cached4 ?? await this.llm.generate(prompt);
1514
+ if (!cached4) this.cache.set(prompt, response);
1367
1515
  try {
1368
1516
  const jsonMatch = response.match(/\{[\s\S]*\}/);
1369
1517
  if (jsonMatch) {
@@ -1455,13 +1603,50 @@ Respond with JSON:
1455
1603
  }
1456
1604
  };
1457
1605
  }
1606
+ /**
1607
+ * B8 — Incremental mode: only generate tests for files changed vs baseBranch.
1608
+ * Emits 'diff-analyzed' with the DiffResult, then runs autonomously with a reduced
1609
+ * scope derived from the changed files.
1610
+ */
1611
+ async runIncrementally(baseBranch = "main") {
1612
+ const repoPath = this.saasConfig.localPath;
1613
+ if (!repoPath) {
1614
+ logger.warn("runIncrementally: no localPath configured, falling back to full run");
1615
+ return this.runAutonomously();
1616
+ }
1617
+ const diff = this.diffAnalyzer.analyze(repoPath, baseBranch);
1618
+ logger.info("Incremental diff", { changedFiles: diff.changedFiles.length, affectedTests: diff.affectedTests.length, riskLevel: diff.riskLevel });
1619
+ this.emit("diff-analyzed", diff);
1620
+ if (diff.changedFiles.length === 0) {
1621
+ logger.info("No changed files \u2014 skipping incremental run");
1622
+ this.emit("session-complete", { testsGenerated: 0, agentsCreated: 0, incremental: true });
1623
+ return;
1624
+ }
1625
+ for (const file of diff.changedFiles.slice(0, 10)) {
1626
+ const testType = this.inferTestType(file);
1627
+ const context = `Changed file: ${file}
1628
+ Risk level: ${diff.riskLevel}
1629
+ ${diff.summary}`;
1630
+ try {
1631
+ await this.generateTest(testType, `Test coverage for ${file}`, context);
1632
+ } catch (e) {
1633
+ logger.error("Failed to generate test for file", { file, error: e instanceof Error ? e.message : String(e) });
1634
+ }
1635
+ }
1636
+ this.emit("session-complete", {
1637
+ testsGenerated: this.generatedTests.size,
1638
+ agentsCreated: this.dynamicAgents.size,
1639
+ incremental: true,
1640
+ diff
1641
+ });
1642
+ }
1458
1643
  };
1459
1644
 
1460
1645
  // agent/tools/browser.ts
1461
1646
  init_esm_shims();
1462
1647
  import { chromium } from "playwright";
1463
1648
  import { mkdirSync as mkdirSync3 } from "fs";
1464
- import { join as join4 } from "path";
1649
+ import { join as join5 } from "path";
1465
1650
  var BrowserTools = class {
1466
1651
  browser = null;
1467
1652
  page = null;
@@ -1486,13 +1671,9 @@ var BrowserTools = class {
1486
1671
  {
1487
1672
  name: "navigate_to_page",
1488
1673
  description: "Navigate to a specific URL in the application",
1489
- parameters: {
1490
- type: "object",
1491
- properties: {
1492
- url: { type: "string", description: "The URL to navigate to" }
1493
- },
1494
- required: ["url"]
1495
- },
1674
+ parameters: [
1675
+ { name: "url", type: "string", description: "The URL to navigate to", required: true }
1676
+ ],
1496
1677
  execute: async ({ url }) => {
1497
1678
  if (!this.page) await this.initialize();
1498
1679
  try {
@@ -1505,24 +1686,20 @@ var BrowserTools = class {
1505
1686
  input: url,
1506
1687
  output: `Page title: ${title}`
1507
1688
  });
1508
- return `Successfully navigated to ${url}. Page title: "${title}"`;
1689
+ return { output: `Successfully navigated to ${url}. Page title: "${title}"` };
1509
1690
  } catch (error) {
1510
- return `Failed to navigate: ${error instanceof Error ? error.message : String(error)}`;
1691
+ return { output: `Failed to navigate: ${error instanceof Error ? error.message : String(error)}`, error: error instanceof Error ? error.message : String(error) };
1511
1692
  }
1512
1693
  }
1513
1694
  },
1514
1695
  {
1515
1696
  name: "click_element",
1516
1697
  description: "Click on an element using a CSS selector",
1517
- parameters: {
1518
- type: "object",
1519
- properties: {
1520
- selector: { type: "string", description: "CSS selector of the element to click" }
1521
- },
1522
- required: ["selector"]
1523
- },
1698
+ parameters: [
1699
+ { name: "selector", type: "string", description: "CSS selector of the element to click", required: true }
1700
+ ],
1524
1701
  execute: async ({ selector }) => {
1525
- if (!this.page) return "Browser not initialized. Navigate to a page first.";
1702
+ if (!this.page) return { output: "Browser not initialized. Navigate to a page first.", error: "Browser not initialized" };
1526
1703
  try {
1527
1704
  await this.page.click(selector, { timeout: 5e3 });
1528
1705
  this.db.createAction({
@@ -1531,25 +1708,21 @@ var BrowserTools = class {
1531
1708
  description: `Clicked element: ${selector}`,
1532
1709
  input: selector
1533
1710
  });
1534
- return `Successfully clicked element: ${selector}`;
1711
+ return { output: `Successfully clicked element: ${selector}` };
1535
1712
  } catch (error) {
1536
- return `Failed to click element: ${error instanceof Error ? error.message : String(error)}`;
1713
+ return { output: `Failed to click element: ${error instanceof Error ? error.message : String(error)}`, error: error instanceof Error ? error.message : String(error) };
1537
1714
  }
1538
1715
  }
1539
1716
  },
1540
1717
  {
1541
1718
  name: "fill_input",
1542
1719
  description: "Fill an input field with text",
1543
- parameters: {
1544
- type: "object",
1545
- properties: {
1546
- selector: { type: "string", description: "CSS selector of the input field" },
1547
- text: { type: "string", description: "Text to fill in the input" }
1548
- },
1549
- required: ["selector", "text"]
1550
- },
1720
+ parameters: [
1721
+ { name: "selector", type: "string", description: "CSS selector of the input field", required: true },
1722
+ { name: "text", type: "string", description: "Text to fill in the input", required: true }
1723
+ ],
1551
1724
  execute: async ({ selector, text }) => {
1552
- if (!this.page) return "Browser not initialized. Navigate to a page first.";
1725
+ if (!this.page) return { output: "Browser not initialized. Navigate to a page first.", error: "Browser not initialized" };
1553
1726
  try {
1554
1727
  await this.page.fill(selector, text);
1555
1728
  this.db.createAction({
@@ -1558,27 +1731,23 @@ var BrowserTools = class {
1558
1731
  description: `Filled input ${selector}`,
1559
1732
  input: `${selector} = ${text}`
1560
1733
  });
1561
- return `Successfully filled input ${selector} with text`;
1734
+ return { output: `Successfully filled input ${selector} with text` };
1562
1735
  } catch (error) {
1563
- return `Failed to fill input: ${error instanceof Error ? error.message : String(error)}`;
1736
+ return { output: `Failed to fill input: ${error instanceof Error ? error.message : String(error)}`, error: error instanceof Error ? error.message : String(error) };
1564
1737
  }
1565
1738
  }
1566
1739
  },
1567
1740
  {
1568
1741
  name: "take_screenshot",
1569
1742
  description: "Take a screenshot of the current page for evidence",
1570
- parameters: {
1571
- type: "object",
1572
- properties: {
1573
- name: { type: "string", description: "Name for the screenshot file" }
1574
- },
1575
- required: ["name"]
1576
- },
1743
+ parameters: [
1744
+ { name: "name", type: "string", description: "Name for the screenshot file", required: true }
1745
+ ],
1577
1746
  execute: async ({ name }) => {
1578
- if (!this.page) return "Browser not initialized. Navigate to a page first.";
1747
+ if (!this.page) return { output: "Browser not initialized. Navigate to a page first.", error: "Browser not initialized" };
1579
1748
  try {
1580
1749
  const filename = `${Date.now()}_${name}.png`;
1581
- const path2 = join4(this.screenshotDir, filename);
1750
+ const path2 = join5(this.screenshotDir, filename);
1582
1751
  await this.page.screenshot({ path: path2, fullPage: true });
1583
1752
  this.db.createAction({
1584
1753
  session_id: this.sessionId,
@@ -1586,38 +1755,32 @@ var BrowserTools = class {
1586
1755
  description: `Screenshot: ${name}`,
1587
1756
  screenshot_path: path2
1588
1757
  });
1589
- return `Screenshot saved: ${path2}`;
1758
+ return { output: `Screenshot saved: ${path2}` };
1590
1759
  } catch (error) {
1591
- return `Failed to take screenshot: ${error instanceof Error ? error.message : String(error)}`;
1760
+ return { output: `Failed to take screenshot: ${error instanceof Error ? error.message : String(error)}`, error: error instanceof Error ? error.message : String(error) };
1592
1761
  }
1593
1762
  }
1594
1763
  },
1595
1764
  {
1596
1765
  name: "get_page_content",
1597
1766
  description: "Get the text content of the current page",
1598
- parameters: {
1599
- type: "object",
1600
- properties: {}
1601
- },
1767
+ parameters: [],
1602
1768
  execute: async () => {
1603
- if (!this.page) return "Browser not initialized. Navigate to a page first.";
1769
+ if (!this.page) return { output: "Browser not initialized. Navigate to a page first.", error: "Browser not initialized" };
1604
1770
  try {
1605
1771
  const content = await this.page.textContent("body");
1606
- return content?.slice(0, 1e3) || "No content found";
1772
+ return { output: content?.slice(0, 1e3) || "No content found" };
1607
1773
  } catch (error) {
1608
- return `Failed to get content: ${error instanceof Error ? error.message : String(error)}`;
1774
+ return { output: `Failed to get content: ${error instanceof Error ? error.message : String(error)}`, error: error instanceof Error ? error.message : String(error) };
1609
1775
  }
1610
1776
  }
1611
1777
  },
1612
1778
  {
1613
1779
  name: "check_console_errors",
1614
1780
  description: "Check for JavaScript console errors on the page",
1615
- parameters: {
1616
- type: "object",
1617
- properties: {}
1618
- },
1781
+ parameters: [],
1619
1782
  execute: async () => {
1620
- if (!this.page) return "Browser not initialized. Navigate to a page first.";
1783
+ if (!this.page) return { output: "Browser not initialized. Navigate to a page first.", error: "Browser not initialized" };
1621
1784
  const errors = [];
1622
1785
  this.page.on("console", (msg) => {
1623
1786
  if (msg.type() === "error") {
@@ -1626,10 +1789,10 @@ var BrowserTools = class {
1626
1789
  });
1627
1790
  await this.page.waitForTimeout(2e3);
1628
1791
  if (errors.length > 0) {
1629
- return `Found ${errors.length} console errors:
1630
- ${errors.join("\n")}`;
1792
+ return { output: `Found ${errors.length} console errors:
1793
+ ${errors.join("\n")}` };
1631
1794
  }
1632
- return "No console errors detected";
1795
+ return { output: "No console errors detected" };
1633
1796
  }
1634
1797
  }
1635
1798
  ];
@@ -1740,6 +1903,16 @@ var GitListener = class extends EventEmitter3 {
1740
1903
  for (const commit of commits) {
1741
1904
  if (this.lastCommitSha && commit.sha === this.lastCommitSha) break;
1742
1905
  const isMerge = commit.parents && commit.parents.length > 1;
1906
+ let changedFiles;
1907
+ try {
1908
+ const { data: detail } = await this.octokit.repos.getCommit({
1909
+ owner: this.config.owner,
1910
+ repo: this.config.repo,
1911
+ ref: commit.sha
1912
+ });
1913
+ changedFiles = (detail.files ?? []).map((f) => f.filename);
1914
+ } catch {
1915
+ }
1743
1916
  const event = {
1744
1917
  type: isMerge ? "merge" : "push",
1745
1918
  provider: "github",
@@ -1747,7 +1920,8 @@ var GitListener = class extends EventEmitter3 {
1747
1920
  commit: commit.sha,
1748
1921
  author: commit.commit.author?.name || "unknown",
1749
1922
  message: commit.commit.message,
1750
- timestamp: new Date(commit.commit.author?.date || Date.now())
1923
+ timestamp: new Date(commit.commit.author?.date || Date.now()),
1924
+ changedFiles
1751
1925
  };
1752
1926
  this.emit("git-event", event);
1753
1927
  if (isMerge) {
@@ -1833,6 +2007,16 @@ var GitListener = class extends EventEmitter3 {
1833
2007
  for (const commit of commits) {
1834
2008
  if (this.lastCommitSha && commit.id === this.lastCommitSha) break;
1835
2009
  const isMerge = commit.parent_ids && commit.parent_ids.length > 1;
2010
+ let changedFiles;
2011
+ try {
2012
+ const diffRes = await fetch(
2013
+ `${baseUrl}/api/v4/projects/${projectPath}/repository/commits/${commit.id}/diff`,
2014
+ { headers }
2015
+ );
2016
+ const diffs = await diffRes.json();
2017
+ changedFiles = diffs.map((d) => d.new_path);
2018
+ } catch {
2019
+ }
1836
2020
  const event = {
1837
2021
  type: isMerge ? "merge" : "push",
1838
2022
  provider: "gitlab",
@@ -1840,7 +2024,8 @@ var GitListener = class extends EventEmitter3 {
1840
2024
  commit: commit.id,
1841
2025
  author: commit.author_name,
1842
2026
  message: commit.message,
1843
- timestamp: new Date(commit.created_at)
2027
+ timestamp: new Date(commit.created_at),
2028
+ changedFiles
1844
2029
  };
1845
2030
  this.emit("git-event", event);
1846
2031
  if (isMerge) {
@@ -1935,8 +2120,8 @@ var GitListener = class extends EventEmitter3 {
1935
2120
  init_esm_shims();
1936
2121
  import { EventEmitter as EventEmitter4 } from "events";
1937
2122
  import { spawn } from "child_process";
1938
- import { existsSync as existsSync3, readFileSync as readFileSync4, statSync } from "fs";
1939
- import { join as join5, resolve } from "path";
2123
+ import { existsSync as existsSync4, readFileSync as readFileSync4, statSync } from "fs";
2124
+ import { join as join6, resolve } from "path";
1940
2125
  function sanitizeRepoPath(inputPath) {
1941
2126
  if (typeof inputPath !== "string" || !inputPath.trim()) {
1942
2127
  throw new ProjectRunnerError("repoPath must be a non-empty string");
@@ -1964,14 +2149,14 @@ var ProjectRunner = class extends EventEmitter4 {
1964
2149
  */
1965
2150
  detectProjectType(repoPath) {
1966
2151
  repoPath = sanitizeRepoPath(repoPath);
1967
- const pkgPath = join5(repoPath, "package.json");
1968
- if (existsSync3(pkgPath)) {
2152
+ const pkgPath = join6(repoPath, "package.json");
2153
+ if (existsSync4(pkgPath)) {
1969
2154
  return this.detectNodeProject(repoPath, pkgPath);
1970
2155
  }
1971
- if (existsSync3(join5(repoPath, "requirements.txt")) || existsSync3(join5(repoPath, "pyproject.toml"))) {
2156
+ if (existsSync4(join6(repoPath, "requirements.txt")) || existsSync4(join6(repoPath, "pyproject.toml"))) {
1972
2157
  return this.detectPythonProject(repoPath);
1973
2158
  }
1974
- if (existsSync3(join5(repoPath, "go.mod"))) {
2159
+ if (existsSync4(join6(repoPath, "go.mod"))) {
1975
2160
  return {
1976
2161
  language: "go",
1977
2162
  packageManager: "go",
@@ -1980,7 +2165,7 @@ var ProjectRunner = class extends EventEmitter4 {
1980
2165
  devCommand: "go run ."
1981
2166
  };
1982
2167
  }
1983
- if (existsSync3(join5(repoPath, "Cargo.toml"))) {
2168
+ if (existsSync4(join6(repoPath, "Cargo.toml"))) {
1984
2169
  return {
1985
2170
  language: "rust",
1986
2171
  packageManager: "cargo",
@@ -1996,9 +2181,9 @@ var ProjectRunner = class extends EventEmitter4 {
1996
2181
  const scripts = pkg.scripts || {};
1997
2182
  const deps = { ...pkg.dependencies, ...pkg.devDependencies };
1998
2183
  let packageManager = "npm";
1999
- if (existsSync3(join5(repoPath, "pnpm-lock.yaml"))) packageManager = "pnpm";
2000
- else if (existsSync3(join5(repoPath, "yarn.lock"))) packageManager = "yarn";
2001
- else if (existsSync3(join5(repoPath, "bun.lockb"))) packageManager = "bun";
2184
+ if (existsSync4(join6(repoPath, "pnpm-lock.yaml"))) packageManager = "pnpm";
2185
+ else if (existsSync4(join6(repoPath, "yarn.lock"))) packageManager = "yarn";
2186
+ else if (existsSync4(join6(repoPath, "bun.lockb"))) packageManager = "bun";
2002
2187
  let framework;
2003
2188
  if (deps["next"]) framework = "next";
2004
2189
  else if (deps["nuxt"]) framework = "nuxt";
@@ -2050,13 +2235,13 @@ var ProjectRunner = class extends EventEmitter4 {
2050
2235
  }
2051
2236
  detectPythonProject(repoPath) {
2052
2237
  let testRunner;
2053
- const reqPath = join5(repoPath, "requirements.txt");
2054
- if (existsSync3(reqPath)) {
2238
+ const reqPath = join6(repoPath, "requirements.txt");
2239
+ if (existsSync4(reqPath)) {
2055
2240
  const content = readFileSync4(reqPath, "utf-8");
2056
2241
  if (content.includes("pytest")) testRunner = "pytest";
2057
2242
  }
2058
- const pyprojectPath = join5(repoPath, "pyproject.toml");
2059
- if (existsSync3(pyprojectPath)) {
2243
+ const pyprojectPath = join6(repoPath, "pyproject.toml");
2244
+ if (existsSync4(pyprojectPath)) {
2060
2245
  const content = readFileSync4(pyprojectPath, "utf-8");
2061
2246
  if (content.includes("pytest")) testRunner = "pytest";
2062
2247
  }