@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 +26 -0
- package/dist/agent/index-v2.js +277 -92
- package/dist/agent/index-v2.js.map +1 -1
- package/dist/agent/index.js +109 -118
- package/dist/agent/index.js.map +1 -1
- package/dist/cli/daemon.js +1045 -635
- package/dist/cli/dashboard.html.js +53 -16
- package/dist/cli/env.html.js +717 -529
- package/dist/cli/index.js +762 -537
- package/dist/cli/server.js +762 -537
- package/package.json +1 -1
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)
|
package/dist/agent/index-v2.js
CHANGED
|
@@ -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
|
|
775
|
-
import { join as
|
|
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
|
|
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 =
|
|
1098
|
-
if (!
|
|
1239
|
+
repoPath = join4(this.workDir, "repo");
|
|
1240
|
+
if (!existsSync3(repoPath)) {
|
|
1099
1241
|
logger.info("Cloning repository", { url: this.saasConfig.repoUrl });
|
|
1100
1242
|
try {
|
|
1101
|
-
|
|
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 || !
|
|
1252
|
+
if (!repoPath || !existsSync3(repoPath)) {
|
|
1111
1253
|
return "";
|
|
1112
1254
|
}
|
|
1113
1255
|
const analysis = [];
|
|
1114
1256
|
try {
|
|
1115
|
-
const packageJsonPath =
|
|
1116
|
-
if (
|
|
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 =
|
|
1142
|
-
if (
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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 &&
|
|
1312
|
-
const result =
|
|
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
|
|
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
|
|
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: "
|
|
1491
|
-
|
|
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: "
|
|
1519
|
-
|
|
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: "
|
|
1545
|
-
|
|
1546
|
-
|
|
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: "
|
|
1572
|
-
|
|
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 =
|
|
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
|
|
1939
|
-
import { join as
|
|
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 =
|
|
1968
|
-
if (
|
|
2152
|
+
const pkgPath = join6(repoPath, "package.json");
|
|
2153
|
+
if (existsSync4(pkgPath)) {
|
|
1969
2154
|
return this.detectNodeProject(repoPath, pkgPath);
|
|
1970
2155
|
}
|
|
1971
|
-
if (
|
|
2156
|
+
if (existsSync4(join6(repoPath, "requirements.txt")) || existsSync4(join6(repoPath, "pyproject.toml"))) {
|
|
1972
2157
|
return this.detectPythonProject(repoPath);
|
|
1973
2158
|
}
|
|
1974
|
-
if (
|
|
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 (
|
|
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 (
|
|
2000
|
-
else if (
|
|
2001
|
-
else if (
|
|
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 =
|
|
2054
|
-
if (
|
|
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 =
|
|
2059
|
-
if (
|
|
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
|
}
|