@openqa/cli 1.0.13 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/index.js +5 -0
- package/dist/agent/index.js.map +1 -1
- package/dist/cli/index.js +728 -1630
- package/dist/cli/server.js +695 -0
- package/dist/database/index.js +5 -0
- package/dist/database/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -1,1640 +1,750 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __esm = (fn, res) => function __init() {
|
|
5
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
|
+
};
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
2
11
|
|
|
3
|
-
//
|
|
4
|
-
import
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
import { Tracer } from "@orka-js/observability";
|
|
12
|
-
import { EventEmitter as EventEmitter3 } from "events";
|
|
12
|
+
// node_modules/tsup/assets/esm_shims.js
|
|
13
|
+
import path from "path";
|
|
14
|
+
import { fileURLToPath } from "url";
|
|
15
|
+
var init_esm_shims = __esm({
|
|
16
|
+
"node_modules/tsup/assets/esm_shims.js"() {
|
|
17
|
+
"use strict";
|
|
18
|
+
}
|
|
19
|
+
});
|
|
13
20
|
|
|
14
21
|
// database/index.ts
|
|
15
22
|
import { Low } from "lowdb";
|
|
16
23
|
import { JSONFile } from "lowdb/node";
|
|
17
24
|
import { dirname } from "path";
|
|
18
|
-
import { fileURLToPath } from "url";
|
|
25
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
19
26
|
import { mkdirSync } from "fs";
|
|
20
|
-
var __filename
|
|
21
|
-
var
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const adapter = new JSONFile(this.dbPath);
|
|
32
|
-
this.db = new Low(adapter, {
|
|
33
|
-
config: {},
|
|
34
|
-
test_sessions: [],
|
|
35
|
-
actions: [],
|
|
36
|
-
bugs: [],
|
|
37
|
-
kanban_tickets: []
|
|
38
|
-
});
|
|
39
|
-
this.db.read();
|
|
40
|
-
if (!this.db.data) {
|
|
41
|
-
this.db.data = {
|
|
42
|
-
config: {},
|
|
43
|
-
test_sessions: [],
|
|
44
|
-
actions: [],
|
|
45
|
-
bugs: [],
|
|
46
|
-
kanban_tickets: []
|
|
47
|
-
};
|
|
48
|
-
this.db.write();
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
async ensureInitialized() {
|
|
52
|
-
if (!this.db) {
|
|
53
|
-
this.initialize();
|
|
54
|
-
}
|
|
55
|
-
await this.db.read();
|
|
56
|
-
}
|
|
57
|
-
async getConfig(key) {
|
|
58
|
-
await this.ensureInitialized();
|
|
59
|
-
return this.db.data.config[key] || null;
|
|
60
|
-
}
|
|
61
|
-
async setConfig(key, value) {
|
|
62
|
-
await this.ensureInitialized();
|
|
63
|
-
this.db.data.config[key] = value;
|
|
64
|
-
await this.db.write();
|
|
65
|
-
}
|
|
66
|
-
async getAllConfig() {
|
|
67
|
-
await this.ensureInitialized();
|
|
68
|
-
return this.db.data.config;
|
|
69
|
-
}
|
|
70
|
-
async createSession(id, metadata) {
|
|
71
|
-
await this.ensureInitialized();
|
|
72
|
-
const session = {
|
|
73
|
-
id,
|
|
74
|
-
started_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
75
|
-
status: "running",
|
|
76
|
-
total_actions: 0,
|
|
77
|
-
bugs_found: 0,
|
|
78
|
-
metadata: metadata ? JSON.stringify(metadata) : void 0
|
|
79
|
-
};
|
|
80
|
-
this.db.data.test_sessions.push(session);
|
|
81
|
-
await this.db.write();
|
|
82
|
-
return session;
|
|
83
|
-
}
|
|
84
|
-
async getSession(id) {
|
|
85
|
-
await this.ensureInitialized();
|
|
86
|
-
return this.db.data.test_sessions.find((s) => s.id === id) || null;
|
|
87
|
-
}
|
|
88
|
-
async updateSession(id, updates) {
|
|
89
|
-
await this.ensureInitialized();
|
|
90
|
-
const index = this.db.data.test_sessions.findIndex((s) => s.id === id);
|
|
91
|
-
if (index !== -1) {
|
|
92
|
-
this.db.data.test_sessions[index] = { ...this.db.data.test_sessions[index], ...updates };
|
|
93
|
-
await this.db.write();
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
async getRecentSessions(limit = 10) {
|
|
97
|
-
await this.ensureInitialized();
|
|
98
|
-
return this.db.data.test_sessions.sort((a, b) => new Date(b.started_at).getTime() - new Date(a.started_at).getTime()).slice(0, limit);
|
|
99
|
-
}
|
|
100
|
-
async createAction(action) {
|
|
101
|
-
await this.ensureInitialized();
|
|
102
|
-
const newAction = {
|
|
103
|
-
id: `action_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
|
104
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
105
|
-
...action
|
|
106
|
-
};
|
|
107
|
-
this.db.data.actions.push(newAction);
|
|
108
|
-
await this.db.write();
|
|
109
|
-
return newAction;
|
|
110
|
-
}
|
|
111
|
-
async getSessionActions(sessionId) {
|
|
112
|
-
await this.ensureInitialized();
|
|
113
|
-
return this.db.data.actions.filter((a) => a.session_id === sessionId).sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
|
|
114
|
-
}
|
|
115
|
-
async createBug(bug) {
|
|
116
|
-
await this.ensureInitialized();
|
|
117
|
-
const newBug = {
|
|
118
|
-
id: `bug_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
|
119
|
-
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
120
|
-
updated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
121
|
-
...bug
|
|
122
|
-
};
|
|
123
|
-
this.db.data.bugs.push(newBug);
|
|
124
|
-
await this.db.write();
|
|
125
|
-
return newBug;
|
|
126
|
-
}
|
|
127
|
-
async updateBug(id, updates) {
|
|
128
|
-
await this.ensureInitialized();
|
|
129
|
-
const index = this.db.data.bugs.findIndex((b) => b.id === id);
|
|
130
|
-
if (index !== -1) {
|
|
131
|
-
this.db.data.bugs[index] = {
|
|
132
|
-
...this.db.data.bugs[index],
|
|
133
|
-
...updates,
|
|
134
|
-
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
135
|
-
};
|
|
136
|
-
await this.db.write();
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
async getAllBugs() {
|
|
140
|
-
await this.ensureInitialized();
|
|
141
|
-
return this.db.data.bugs.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
|
|
142
|
-
}
|
|
143
|
-
async getBugsByStatus(status) {
|
|
144
|
-
await this.ensureInitialized();
|
|
145
|
-
return this.db.data.bugs.filter((b) => b.status === status).sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
|
|
146
|
-
}
|
|
147
|
-
async createKanbanTicket(ticket) {
|
|
148
|
-
await this.ensureInitialized();
|
|
149
|
-
const newTicket = {
|
|
150
|
-
id: `ticket_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
|
151
|
-
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
152
|
-
updated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
153
|
-
...ticket
|
|
154
|
-
};
|
|
155
|
-
this.db.data.kanban_tickets.push(newTicket);
|
|
156
|
-
await this.db.write();
|
|
157
|
-
return newTicket;
|
|
158
|
-
}
|
|
159
|
-
async updateKanbanTicket(id, updates) {
|
|
160
|
-
await this.ensureInitialized();
|
|
161
|
-
const index = this.db.data.kanban_tickets.findIndex((t) => t.id === id);
|
|
162
|
-
if (index !== -1) {
|
|
163
|
-
this.db.data.kanban_tickets[index] = {
|
|
164
|
-
...this.db.data.kanban_tickets[index],
|
|
165
|
-
...updates,
|
|
166
|
-
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
167
|
-
};
|
|
168
|
-
await this.db.write();
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
async getKanbanTickets() {
|
|
172
|
-
await this.ensureInitialized();
|
|
173
|
-
return this.db.data.kanban_tickets.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
|
|
174
|
-
}
|
|
175
|
-
async getKanbanTicketsByColumn(column) {
|
|
176
|
-
await this.ensureInitialized();
|
|
177
|
-
return this.db.data.kanban_tickets.filter((t) => t.column === column).sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
|
|
178
|
-
}
|
|
179
|
-
async close() {
|
|
180
|
-
}
|
|
181
|
-
};
|
|
182
|
-
|
|
183
|
-
// agent/config/index.ts
|
|
184
|
-
import { config as dotenvConfig } from "dotenv";
|
|
185
|
-
dotenvConfig();
|
|
186
|
-
var ConfigManager = class {
|
|
187
|
-
db = null;
|
|
188
|
-
envConfig;
|
|
189
|
-
constructor(dbPath) {
|
|
190
|
-
this.envConfig = this.loadFromEnv();
|
|
191
|
-
}
|
|
192
|
-
loadFromEnv() {
|
|
193
|
-
return {
|
|
194
|
-
llm: {
|
|
195
|
-
provider: process.env.LLM_PROVIDER || "openai",
|
|
196
|
-
apiKey: process.env.OPENAI_API_KEY || process.env.ANTHROPIC_API_KEY,
|
|
197
|
-
model: process.env.LLM_MODEL,
|
|
198
|
-
baseUrl: process.env.OLLAMA_BASE_URL
|
|
199
|
-
},
|
|
200
|
-
saas: {
|
|
201
|
-
url: process.env.SAAS_URL || "",
|
|
202
|
-
authType: process.env.SAAS_AUTH_TYPE || "none",
|
|
203
|
-
username: process.env.SAAS_USERNAME,
|
|
204
|
-
password: process.env.SAAS_PASSWORD
|
|
205
|
-
},
|
|
206
|
-
github: process.env.GITHUB_TOKEN ? {
|
|
207
|
-
token: process.env.GITHUB_TOKEN,
|
|
208
|
-
owner: process.env.GITHUB_OWNER || "",
|
|
209
|
-
repo: process.env.GITHUB_REPO || ""
|
|
210
|
-
} : void 0,
|
|
211
|
-
agent: {
|
|
212
|
-
intervalMs: parseInt(process.env.AGENT_INTERVAL_MS || "3600000"),
|
|
213
|
-
maxIterations: parseInt(process.env.AGENT_MAX_ITERATIONS || "20"),
|
|
214
|
-
autoStart: process.env.AGENT_AUTO_START === "true"
|
|
215
|
-
},
|
|
216
|
-
web: {
|
|
217
|
-
port: parseInt(process.env.WEB_PORT || "4242"),
|
|
218
|
-
host: process.env.WEB_HOST || "0.0.0.0"
|
|
219
|
-
},
|
|
220
|
-
database: {
|
|
221
|
-
path: process.env.DB_PATH || "./data/openqa.db"
|
|
222
|
-
},
|
|
223
|
-
notifications: {
|
|
224
|
-
slack: process.env.SLACK_WEBHOOK_URL,
|
|
225
|
-
discord: process.env.DISCORD_WEBHOOK_URL
|
|
27
|
+
var __filename, __dirname, OpenQADatabase;
|
|
28
|
+
var init_database = __esm({
|
|
29
|
+
"database/index.ts"() {
|
|
30
|
+
"use strict";
|
|
31
|
+
init_esm_shims();
|
|
32
|
+
__filename = fileURLToPath2(import.meta.url);
|
|
33
|
+
__dirname = dirname(__filename);
|
|
34
|
+
OpenQADatabase = class {
|
|
35
|
+
constructor(dbPath = "./data/openqa.json") {
|
|
36
|
+
this.dbPath = dbPath;
|
|
37
|
+
this.initialize();
|
|
226
38
|
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
const dbConfig = await this.getDB().getAllConfig();
|
|
250
|
-
const merged = { ...this.envConfig };
|
|
251
|
-
for (const [key, value] of Object.entries(dbConfig)) {
|
|
252
|
-
const keys = key.split(".");
|
|
253
|
-
let obj = merged;
|
|
254
|
-
for (let i = 0; i < keys.length - 1; i++) {
|
|
255
|
-
if (!obj[keys[i]]) obj[keys[i]] = {};
|
|
256
|
-
obj = obj[keys[i]];
|
|
257
|
-
}
|
|
258
|
-
obj[keys[keys.length - 1]] = value;
|
|
259
|
-
}
|
|
260
|
-
return merged;
|
|
261
|
-
}
|
|
262
|
-
async getConfig() {
|
|
263
|
-
return await this.getAll();
|
|
264
|
-
}
|
|
265
|
-
// Synchronous version that only uses env vars (no DB)
|
|
266
|
-
getConfigSync() {
|
|
267
|
-
return this.envConfig;
|
|
268
|
-
}
|
|
269
|
-
};
|
|
270
|
-
|
|
271
|
-
// agent/tools/browser.ts
|
|
272
|
-
import { chromium } from "playwright";
|
|
273
|
-
import { mkdirSync as mkdirSync2 } from "fs";
|
|
274
|
-
import { join as join2 } from "path";
|
|
275
|
-
var BrowserTools = class {
|
|
276
|
-
browser = null;
|
|
277
|
-
page = null;
|
|
278
|
-
db;
|
|
279
|
-
sessionId;
|
|
280
|
-
screenshotDir = "./data/screenshots";
|
|
281
|
-
constructor(db, sessionId) {
|
|
282
|
-
this.db = db;
|
|
283
|
-
this.sessionId = sessionId;
|
|
284
|
-
mkdirSync2(this.screenshotDir, { recursive: true });
|
|
285
|
-
}
|
|
286
|
-
async initialize() {
|
|
287
|
-
this.browser = await chromium.launch({ headless: true });
|
|
288
|
-
const context = await this.browser.newContext({
|
|
289
|
-
viewport: { width: 1920, height: 1080 },
|
|
290
|
-
userAgent: "OpenQA/1.0 (Automated Testing Agent)"
|
|
291
|
-
});
|
|
292
|
-
this.page = await context.newPage();
|
|
293
|
-
}
|
|
294
|
-
getTools() {
|
|
295
|
-
return [
|
|
296
|
-
{
|
|
297
|
-
name: "navigate_to_page",
|
|
298
|
-
description: "Navigate to a specific URL in the application",
|
|
299
|
-
parameters: {
|
|
300
|
-
type: "object",
|
|
301
|
-
properties: {
|
|
302
|
-
url: { type: "string", description: "The URL to navigate to" }
|
|
303
|
-
},
|
|
304
|
-
required: ["url"]
|
|
305
|
-
},
|
|
306
|
-
execute: async ({ url }) => {
|
|
307
|
-
if (!this.page) await this.initialize();
|
|
308
|
-
try {
|
|
309
|
-
await this.page.goto(url, { waitUntil: "networkidle" });
|
|
310
|
-
const title = await this.page.title();
|
|
311
|
-
this.db.createAction({
|
|
312
|
-
session_id: this.sessionId,
|
|
313
|
-
type: "navigate",
|
|
314
|
-
description: `Navigated to ${url}`,
|
|
315
|
-
input: url,
|
|
316
|
-
output: `Page title: ${title}`
|
|
317
|
-
});
|
|
318
|
-
return `Successfully navigated to ${url}. Page title: "${title}"`;
|
|
319
|
-
} catch (error) {
|
|
320
|
-
return `Failed to navigate: ${error.message}`;
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
},
|
|
324
|
-
{
|
|
325
|
-
name: "click_element",
|
|
326
|
-
description: "Click on an element using a CSS selector",
|
|
327
|
-
parameters: {
|
|
328
|
-
type: "object",
|
|
329
|
-
properties: {
|
|
330
|
-
selector: { type: "string", description: "CSS selector of the element to click" }
|
|
331
|
-
},
|
|
332
|
-
required: ["selector"]
|
|
333
|
-
},
|
|
334
|
-
execute: async ({ selector }) => {
|
|
335
|
-
if (!this.page) return "Browser not initialized. Navigate to a page first.";
|
|
336
|
-
try {
|
|
337
|
-
await this.page.click(selector, { timeout: 5e3 });
|
|
338
|
-
this.db.createAction({
|
|
339
|
-
session_id: this.sessionId,
|
|
340
|
-
type: "click",
|
|
341
|
-
description: `Clicked element: ${selector}`,
|
|
342
|
-
input: selector
|
|
343
|
-
});
|
|
344
|
-
return `Successfully clicked element: ${selector}`;
|
|
345
|
-
} catch (error) {
|
|
346
|
-
return `Failed to click element: ${error.message}`;
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
},
|
|
350
|
-
{
|
|
351
|
-
name: "fill_input",
|
|
352
|
-
description: "Fill an input field with text",
|
|
353
|
-
parameters: {
|
|
354
|
-
type: "object",
|
|
355
|
-
properties: {
|
|
356
|
-
selector: { type: "string", description: "CSS selector of the input field" },
|
|
357
|
-
text: { type: "string", description: "Text to fill in the input" }
|
|
358
|
-
},
|
|
359
|
-
required: ["selector", "text"]
|
|
360
|
-
},
|
|
361
|
-
execute: async ({ selector, text }) => {
|
|
362
|
-
if (!this.page) return "Browser not initialized. Navigate to a page first.";
|
|
363
|
-
try {
|
|
364
|
-
await this.page.fill(selector, text);
|
|
365
|
-
this.db.createAction({
|
|
366
|
-
session_id: this.sessionId,
|
|
367
|
-
type: "fill",
|
|
368
|
-
description: `Filled input ${selector}`,
|
|
369
|
-
input: `${selector} = ${text}`
|
|
370
|
-
});
|
|
371
|
-
return `Successfully filled input ${selector} with text`;
|
|
372
|
-
} catch (error) {
|
|
373
|
-
return `Failed to fill input: ${error.message}`;
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
},
|
|
377
|
-
{
|
|
378
|
-
name: "take_screenshot",
|
|
379
|
-
description: "Take a screenshot of the current page for evidence",
|
|
380
|
-
parameters: {
|
|
381
|
-
type: "object",
|
|
382
|
-
properties: {
|
|
383
|
-
name: { type: "string", description: "Name for the screenshot file" }
|
|
384
|
-
},
|
|
385
|
-
required: ["name"]
|
|
386
|
-
},
|
|
387
|
-
execute: async ({ name }) => {
|
|
388
|
-
if (!this.page) return "Browser not initialized. Navigate to a page first.";
|
|
389
|
-
try {
|
|
390
|
-
const filename = `${Date.now()}_${name}.png`;
|
|
391
|
-
const path = join2(this.screenshotDir, filename);
|
|
392
|
-
await this.page.screenshot({ path, fullPage: true });
|
|
393
|
-
this.db.createAction({
|
|
394
|
-
session_id: this.sessionId,
|
|
395
|
-
type: "screenshot",
|
|
396
|
-
description: `Screenshot: ${name}`,
|
|
397
|
-
screenshot_path: path
|
|
398
|
-
});
|
|
399
|
-
return `Screenshot saved: ${path}`;
|
|
400
|
-
} catch (error) {
|
|
401
|
-
return `Failed to take screenshot: ${error.message}`;
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
},
|
|
405
|
-
{
|
|
406
|
-
name: "get_page_content",
|
|
407
|
-
description: "Get the text content of the current page",
|
|
408
|
-
parameters: {
|
|
409
|
-
type: "object",
|
|
410
|
-
properties: {}
|
|
411
|
-
},
|
|
412
|
-
execute: async () => {
|
|
413
|
-
if (!this.page) return "Browser not initialized. Navigate to a page first.";
|
|
414
|
-
try {
|
|
415
|
-
const content = await this.page.textContent("body");
|
|
416
|
-
return content?.slice(0, 1e3) || "No content found";
|
|
417
|
-
} catch (error) {
|
|
418
|
-
return `Failed to get content: ${error.message}`;
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
},
|
|
422
|
-
{
|
|
423
|
-
name: "check_console_errors",
|
|
424
|
-
description: "Check for JavaScript console errors on the page",
|
|
425
|
-
parameters: {
|
|
426
|
-
type: "object",
|
|
427
|
-
properties: {}
|
|
428
|
-
},
|
|
429
|
-
execute: async () => {
|
|
430
|
-
if (!this.page) return "Browser not initialized. Navigate to a page first.";
|
|
431
|
-
const errors = [];
|
|
432
|
-
this.page.on("console", (msg) => {
|
|
433
|
-
if (msg.type() === "error") {
|
|
434
|
-
errors.push(msg.text());
|
|
435
|
-
}
|
|
436
|
-
});
|
|
437
|
-
await this.page.waitForTimeout(2e3);
|
|
438
|
-
if (errors.length > 0) {
|
|
439
|
-
return `Found ${errors.length} console errors:
|
|
440
|
-
${errors.join("\n")}`;
|
|
441
|
-
}
|
|
442
|
-
return "No console errors detected";
|
|
39
|
+
db = null;
|
|
40
|
+
initialize() {
|
|
41
|
+
const dir = dirname(this.dbPath);
|
|
42
|
+
mkdirSync(dir, { recursive: true });
|
|
43
|
+
const adapter = new JSONFile(this.dbPath);
|
|
44
|
+
this.db = new Low(adapter, {
|
|
45
|
+
config: {},
|
|
46
|
+
test_sessions: [],
|
|
47
|
+
actions: [],
|
|
48
|
+
bugs: [],
|
|
49
|
+
kanban_tickets: []
|
|
50
|
+
});
|
|
51
|
+
this.db.read();
|
|
52
|
+
if (!this.db.data) {
|
|
53
|
+
this.db.data = {
|
|
54
|
+
config: {},
|
|
55
|
+
test_sessions: [],
|
|
56
|
+
actions: [],
|
|
57
|
+
bugs: [],
|
|
58
|
+
kanban_tickets: []
|
|
59
|
+
};
|
|
60
|
+
this.db.write();
|
|
443
61
|
}
|
|
444
62
|
}
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
if (this.browser) {
|
|
449
|
-
await this.browser.close();
|
|
450
|
-
this.browser = null;
|
|
451
|
-
this.page = null;
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
};
|
|
455
|
-
|
|
456
|
-
// agent/tools/github.ts
|
|
457
|
-
import { Octokit } from "@octokit/rest";
|
|
458
|
-
var GitHubTools = class {
|
|
459
|
-
octokit = null;
|
|
460
|
-
db;
|
|
461
|
-
sessionId;
|
|
462
|
-
config;
|
|
463
|
-
constructor(db, sessionId, config) {
|
|
464
|
-
this.db = db;
|
|
465
|
-
this.sessionId = sessionId;
|
|
466
|
-
this.config = config;
|
|
467
|
-
if (config.token) {
|
|
468
|
-
this.octokit = new Octokit({ auth: config.token });
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
getTools() {
|
|
472
|
-
return [
|
|
473
|
-
{
|
|
474
|
-
name: "create_github_issue",
|
|
475
|
-
description: "Create a GitHub issue when a critical bug is found. Use this for bugs that require developer attention.",
|
|
476
|
-
parameters: {
|
|
477
|
-
type: "object",
|
|
478
|
-
properties: {
|
|
479
|
-
title: { type: "string", description: "Issue title (concise and descriptive)" },
|
|
480
|
-
body: { type: "string", description: "Detailed description with steps to reproduce" },
|
|
481
|
-
severity: { type: "string", enum: ["low", "medium", "high", "critical"], description: "Bug severity" },
|
|
482
|
-
labels: { type: "array", items: { type: "string" }, description: "Labels for the issue" },
|
|
483
|
-
screenshot_path: { type: "string", description: "Path to screenshot evidence" }
|
|
484
|
-
},
|
|
485
|
-
required: ["title", "body", "severity"]
|
|
486
|
-
},
|
|
487
|
-
execute: async ({ title, body, severity, labels = [], screenshot_path }) => {
|
|
488
|
-
if (!this.octokit || !this.config.owner || !this.config.repo) {
|
|
489
|
-
return "GitHub not configured. Please set GITHUB_TOKEN, GITHUB_OWNER, and GITHUB_REPO.";
|
|
490
|
-
}
|
|
491
|
-
try {
|
|
492
|
-
const severityLabel = `severity: ${severity}`;
|
|
493
|
-
const allLabels = ["automated-qa", severityLabel, ...labels];
|
|
494
|
-
const issueBody = `## \u{1F916} Automated QA Report
|
|
495
|
-
|
|
496
|
-
${body}
|
|
497
|
-
|
|
498
|
-
---
|
|
499
|
-
|
|
500
|
-
**Severity:** ${severity.toUpperCase()}
|
|
501
|
-
**Detected by:** OpenQA Agent
|
|
502
|
-
**Session ID:** ${this.sessionId}
|
|
503
|
-
${screenshot_path ? `**Screenshot:** ${screenshot_path}` : ""}
|
|
504
|
-
|
|
505
|
-
*This issue was automatically created by OpenQA during automated testing.*`;
|
|
506
|
-
const issue = await this.octokit.rest.issues.create({
|
|
507
|
-
owner: this.config.owner,
|
|
508
|
-
repo: this.config.repo,
|
|
509
|
-
title: `[QA] ${title}`,
|
|
510
|
-
body: issueBody,
|
|
511
|
-
labels: allLabels
|
|
512
|
-
});
|
|
513
|
-
this.db.createAction({
|
|
514
|
-
session_id: this.sessionId,
|
|
515
|
-
type: "github_issue",
|
|
516
|
-
description: `Created GitHub issue: ${title}`,
|
|
517
|
-
input: JSON.stringify({ title, severity }),
|
|
518
|
-
output: issue.data.html_url
|
|
519
|
-
});
|
|
520
|
-
const bug = this.db.createBug({
|
|
521
|
-
session_id: this.sessionId,
|
|
522
|
-
title,
|
|
523
|
-
description: body,
|
|
524
|
-
severity,
|
|
525
|
-
status: "open",
|
|
526
|
-
github_issue_url: issue.data.html_url,
|
|
527
|
-
screenshot_path
|
|
528
|
-
});
|
|
529
|
-
return `\u2705 GitHub issue created successfully!
|
|
530
|
-
URL: ${issue.data.html_url}
|
|
531
|
-
Issue #${issue.data.number}`;
|
|
532
|
-
} catch (error) {
|
|
533
|
-
return `\u274C Failed to create GitHub issue: ${error.message}`;
|
|
534
|
-
}
|
|
63
|
+
async ensureInitialized() {
|
|
64
|
+
if (!this.db) {
|
|
65
|
+
this.initialize();
|
|
535
66
|
}
|
|
67
|
+
await this.db.read();
|
|
536
68
|
}
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
tags: JSON.stringify(allTags),
|
|
575
|
-
screenshot_url: screenshot_path
|
|
576
|
-
});
|
|
577
|
-
this.db.createAction({
|
|
578
|
-
session_id: this.sessionId,
|
|
579
|
-
type: "kanban_ticket",
|
|
580
|
-
description: `Created Kanban ticket: ${title}`,
|
|
581
|
-
input: JSON.stringify({ title, priority, column }),
|
|
582
|
-
output: ticket.id
|
|
583
|
-
});
|
|
584
|
-
return `\u2705 Kanban ticket created successfully!
|
|
585
|
-
ID: ${ticket.id}
|
|
586
|
-
Column: ${column}
|
|
587
|
-
Priority: ${priority}`;
|
|
588
|
-
} catch (error) {
|
|
589
|
-
return `\u274C Failed to create Kanban ticket: ${error.message}`;
|
|
590
|
-
}
|
|
591
|
-
}
|
|
592
|
-
},
|
|
593
|
-
{
|
|
594
|
-
name: "update_kanban_ticket",
|
|
595
|
-
description: "Update an existing Kanban ticket (move columns, change priority, etc.)",
|
|
596
|
-
parameters: {
|
|
597
|
-
type: "object",
|
|
598
|
-
properties: {
|
|
599
|
-
ticket_id: { type: "string", description: "ID of the ticket to update" },
|
|
600
|
-
column: { type: "string", enum: ["backlog", "to-do", "in-progress", "done"], description: "New column" },
|
|
601
|
-
priority: { type: "string", enum: ["low", "medium", "high", "critical"], description: "New priority" }
|
|
602
|
-
},
|
|
603
|
-
required: ["ticket_id"]
|
|
604
|
-
},
|
|
605
|
-
execute: async ({ ticket_id, column, priority }) => {
|
|
606
|
-
try {
|
|
607
|
-
const updates = {};
|
|
608
|
-
if (column) updates.column = column;
|
|
609
|
-
if (priority) updates.priority = priority;
|
|
610
|
-
this.db.updateKanbanTicket(ticket_id, updates);
|
|
611
|
-
return `\u2705 Kanban ticket ${ticket_id} updated successfully!`;
|
|
612
|
-
} catch (error) {
|
|
613
|
-
return `\u274C Failed to update Kanban ticket: ${error.message}`;
|
|
614
|
-
}
|
|
615
|
-
}
|
|
616
|
-
},
|
|
617
|
-
{
|
|
618
|
-
name: "get_kanban_board",
|
|
619
|
-
description: "Get all tickets from the Kanban board to see current status",
|
|
620
|
-
parameters: {
|
|
621
|
-
type: "object",
|
|
622
|
-
properties: {}
|
|
623
|
-
},
|
|
624
|
-
execute: async () => {
|
|
625
|
-
try {
|
|
626
|
-
const tickets = this.db.getKanbanTickets();
|
|
627
|
-
const byColumn = {
|
|
628
|
-
backlog: tickets.filter((t) => t.column === "backlog"),
|
|
629
|
-
"to-do": tickets.filter((t) => t.column === "to-do"),
|
|
630
|
-
"in-progress": tickets.filter((t) => t.column === "in-progress"),
|
|
631
|
-
done: tickets.filter((t) => t.column === "done")
|
|
632
|
-
};
|
|
633
|
-
const summary = `
|
|
634
|
-
\u{1F4CA} Kanban Board Status:
|
|
635
|
-
- Backlog: ${byColumn.backlog.length} tickets
|
|
636
|
-
- To Do: ${byColumn["to-do"].length} tickets
|
|
637
|
-
- In Progress: ${byColumn["in-progress"].length} tickets
|
|
638
|
-
- Done: ${byColumn.done.length} tickets
|
|
639
|
-
|
|
640
|
-
Total: ${tickets.length} tickets
|
|
641
|
-
`.trim();
|
|
642
|
-
return summary;
|
|
643
|
-
} catch (error) {
|
|
644
|
-
return `\u274C Failed to get Kanban board: ${error.message}`;
|
|
645
|
-
}
|
|
69
|
+
async getConfig(key) {
|
|
70
|
+
await this.ensureInitialized();
|
|
71
|
+
return this.db.data.config[key] || null;
|
|
72
|
+
}
|
|
73
|
+
async setConfig(key, value) {
|
|
74
|
+
await this.ensureInitialized();
|
|
75
|
+
this.db.data.config[key] = value;
|
|
76
|
+
await this.db.write();
|
|
77
|
+
}
|
|
78
|
+
async getAllConfig() {
|
|
79
|
+
await this.ensureInitialized();
|
|
80
|
+
return this.db.data.config;
|
|
81
|
+
}
|
|
82
|
+
async createSession(id, metadata) {
|
|
83
|
+
await this.ensureInitialized();
|
|
84
|
+
const session = {
|
|
85
|
+
id,
|
|
86
|
+
started_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
87
|
+
status: "running",
|
|
88
|
+
total_actions: 0,
|
|
89
|
+
bugs_found: 0,
|
|
90
|
+
metadata: metadata ? JSON.stringify(metadata) : void 0
|
|
91
|
+
};
|
|
92
|
+
this.db.data.test_sessions.push(session);
|
|
93
|
+
await this.db.write();
|
|
94
|
+
return session;
|
|
95
|
+
}
|
|
96
|
+
async getSession(id) {
|
|
97
|
+
await this.ensureInitialized();
|
|
98
|
+
return this.db.data.test_sessions.find((s) => s.id === id) || null;
|
|
99
|
+
}
|
|
100
|
+
async updateSession(id, updates) {
|
|
101
|
+
await this.ensureInitialized();
|
|
102
|
+
const index = this.db.data.test_sessions.findIndex((s) => s.id === id);
|
|
103
|
+
if (index !== -1) {
|
|
104
|
+
this.db.data.test_sessions[index] = { ...this.db.data.test_sessions[index], ...updates };
|
|
105
|
+
await this.db.write();
|
|
646
106
|
}
|
|
647
107
|
}
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
// agent/webhooks/git-listener.ts
|
|
653
|
-
import { EventEmitter } from "events";
|
|
654
|
-
import { Octokit as Octokit2 } from "@octokit/rest";
|
|
655
|
-
var GitListener = class extends EventEmitter {
|
|
656
|
-
config;
|
|
657
|
-
octokit = null;
|
|
658
|
-
lastCommitSha = null;
|
|
659
|
-
lastPipelineId = null;
|
|
660
|
-
pollInterval = null;
|
|
661
|
-
isRunning = false;
|
|
662
|
-
constructor(config) {
|
|
663
|
-
super();
|
|
664
|
-
this.config = {
|
|
665
|
-
branch: "main",
|
|
666
|
-
pollIntervalMs: 6e4,
|
|
667
|
-
gitlabUrl: "https://gitlab.com",
|
|
668
|
-
...config
|
|
669
|
-
};
|
|
670
|
-
if (config.provider === "github" && config.token) {
|
|
671
|
-
this.octokit = new Octokit2({ auth: config.token });
|
|
672
|
-
}
|
|
673
|
-
}
|
|
674
|
-
async start() {
|
|
675
|
-
if (this.isRunning) return;
|
|
676
|
-
this.isRunning = true;
|
|
677
|
-
console.log(`\u{1F517} GitListener started for ${this.config.provider}/${this.config.owner}/${this.config.repo}`);
|
|
678
|
-
await this.checkInitialState();
|
|
679
|
-
this.pollInterval = setInterval(() => {
|
|
680
|
-
this.poll().catch(console.error);
|
|
681
|
-
}, this.config.pollIntervalMs);
|
|
682
|
-
}
|
|
683
|
-
stop() {
|
|
684
|
-
this.isRunning = false;
|
|
685
|
-
if (this.pollInterval) {
|
|
686
|
-
clearInterval(this.pollInterval);
|
|
687
|
-
this.pollInterval = null;
|
|
688
|
-
}
|
|
689
|
-
console.log("\u{1F517} GitListener stopped");
|
|
690
|
-
}
|
|
691
|
-
async checkInitialState() {
|
|
692
|
-
try {
|
|
693
|
-
if (this.config.provider === "github") {
|
|
694
|
-
await this.checkGitHubState();
|
|
695
|
-
} else {
|
|
696
|
-
await this.checkGitLabState();
|
|
108
|
+
async getRecentSessions(limit = 10) {
|
|
109
|
+
await this.ensureInitialized();
|
|
110
|
+
return this.db.data.test_sessions.sort((a, b) => new Date(b.started_at).getTime() - new Date(a.started_at).getTime()).slice(0, limit);
|
|
697
111
|
}
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
112
|
+
async createAction(action) {
|
|
113
|
+
await this.ensureInitialized();
|
|
114
|
+
const newAction = {
|
|
115
|
+
id: `action_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
|
116
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
117
|
+
...action
|
|
118
|
+
};
|
|
119
|
+
this.db.data.actions.push(newAction);
|
|
120
|
+
await this.db.write();
|
|
121
|
+
return newAction;
|
|
708
122
|
}
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
}
|
|
713
|
-
async checkGitHubState() {
|
|
714
|
-
if (!this.octokit) return;
|
|
715
|
-
const { data: commits } = await this.octokit.repos.listCommits({
|
|
716
|
-
owner: this.config.owner,
|
|
717
|
-
repo: this.config.repo,
|
|
718
|
-
sha: this.config.branch,
|
|
719
|
-
per_page: 1
|
|
720
|
-
});
|
|
721
|
-
if (commits.length > 0) {
|
|
722
|
-
this.lastCommitSha = commits[0].sha;
|
|
723
|
-
}
|
|
724
|
-
try {
|
|
725
|
-
const { data: runs } = await this.octokit.actions.listWorkflowRunsForRepo({
|
|
726
|
-
owner: this.config.owner,
|
|
727
|
-
repo: this.config.repo,
|
|
728
|
-
branch: this.config.branch,
|
|
729
|
-
per_page: 1
|
|
730
|
-
});
|
|
731
|
-
if (runs.workflow_runs.length > 0) {
|
|
732
|
-
this.lastPipelineId = runs.workflow_runs[0].id.toString();
|
|
123
|
+
async getSessionActions(sessionId) {
|
|
124
|
+
await this.ensureInitialized();
|
|
125
|
+
return this.db.data.actions.filter((a) => a.session_id === sessionId).sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
|
|
733
126
|
}
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
for (const commit of commits) {
|
|
746
|
-
if (this.lastCommitSha && commit.sha === this.lastCommitSha) break;
|
|
747
|
-
const isMerge = commit.parents && commit.parents.length > 1;
|
|
748
|
-
const event = {
|
|
749
|
-
type: isMerge ? "merge" : "push",
|
|
750
|
-
provider: "github",
|
|
751
|
-
branch: this.config.branch,
|
|
752
|
-
commit: commit.sha,
|
|
753
|
-
author: commit.commit.author?.name || "unknown",
|
|
754
|
-
message: commit.commit.message,
|
|
755
|
-
timestamp: new Date(commit.commit.author?.date || Date.now())
|
|
756
|
-
};
|
|
757
|
-
this.emit("git-event", event);
|
|
758
|
-
if (isMerge) {
|
|
759
|
-
this.emit("merge", event);
|
|
760
|
-
console.log(`\u{1F500} Merge detected on ${this.config.branch}: ${commit.sha.slice(0, 7)}`);
|
|
127
|
+
async createBug(bug) {
|
|
128
|
+
await this.ensureInitialized();
|
|
129
|
+
const newBug = {
|
|
130
|
+
id: `bug_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
|
131
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
132
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
133
|
+
...bug
|
|
134
|
+
};
|
|
135
|
+
this.db.data.bugs.push(newBug);
|
|
136
|
+
await this.db.write();
|
|
137
|
+
return newBug;
|
|
761
138
|
}
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
branch: this.config.branch,
|
|
771
|
-
per_page: 5
|
|
772
|
-
});
|
|
773
|
-
for (const run of runs.workflow_runs) {
|
|
774
|
-
if (this.lastPipelineId && run.id.toString() === this.lastPipelineId) break;
|
|
775
|
-
if (run.status === "completed") {
|
|
776
|
-
const event = {
|
|
777
|
-
type: run.conclusion === "success" ? "pipeline_success" : "pipeline_failure",
|
|
778
|
-
provider: "github",
|
|
779
|
-
branch: this.config.branch,
|
|
780
|
-
commit: run.head_sha,
|
|
781
|
-
author: run.actor?.login || "unknown",
|
|
782
|
-
message: run.name || "",
|
|
783
|
-
timestamp: new Date(run.updated_at || Date.now()),
|
|
784
|
-
pipelineId: run.id.toString(),
|
|
785
|
-
pipelineStatus: run.conclusion || void 0
|
|
139
|
+
async updateBug(id, updates) {
|
|
140
|
+
await this.ensureInitialized();
|
|
141
|
+
const index = this.db.data.bugs.findIndex((b) => b.id === id);
|
|
142
|
+
if (index !== -1) {
|
|
143
|
+
this.db.data.bugs[index] = {
|
|
144
|
+
...this.db.data.bugs[index],
|
|
145
|
+
...updates,
|
|
146
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
786
147
|
};
|
|
787
|
-
this.
|
|
788
|
-
if (run.conclusion === "success") {
|
|
789
|
-
this.emit("pipeline-success", event);
|
|
790
|
-
console.log(`\u2705 Pipeline success: ${run.name} (${run.id})`);
|
|
791
|
-
} else {
|
|
792
|
-
this.emit("pipeline-failure", event);
|
|
793
|
-
console.log(`\u274C Pipeline failure: ${run.name} (${run.id})`);
|
|
794
|
-
}
|
|
148
|
+
await this.db.write();
|
|
795
149
|
}
|
|
796
150
|
}
|
|
797
|
-
|
|
798
|
-
this.
|
|
151
|
+
async getAllBugs() {
|
|
152
|
+
await this.ensureInitialized();
|
|
153
|
+
return this.db.data.bugs.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
|
|
799
154
|
}
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
async checkGitLabState() {
|
|
804
|
-
const headers = { "PRIVATE-TOKEN": this.config.token };
|
|
805
|
-
const projectPath = encodeURIComponent(`${this.config.owner}/${this.config.repo}`);
|
|
806
|
-
const baseUrl = this.config.gitlabUrl;
|
|
807
|
-
try {
|
|
808
|
-
const commitsRes = await fetch(
|
|
809
|
-
`${baseUrl}/api/v4/projects/${projectPath}/repository/commits?ref_name=${this.config.branch}&per_page=1`,
|
|
810
|
-
{ headers }
|
|
811
|
-
);
|
|
812
|
-
const commits = await commitsRes.json();
|
|
813
|
-
if (commits.length > 0) {
|
|
814
|
-
this.lastCommitSha = commits[0].id;
|
|
155
|
+
async getBugsByStatus(status) {
|
|
156
|
+
await this.ensureInitialized();
|
|
157
|
+
return this.db.data.bugs.filter((b) => b.status === status).sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
|
|
815
158
|
}
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
159
|
+
async createKanbanTicket(ticket) {
|
|
160
|
+
await this.ensureInitialized();
|
|
161
|
+
const newTicket = {
|
|
162
|
+
id: `ticket_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
|
163
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
164
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
165
|
+
...ticket
|
|
166
|
+
};
|
|
167
|
+
this.db.data.kanban_tickets.push(newTicket);
|
|
168
|
+
await this.db.write();
|
|
169
|
+
return newTicket;
|
|
823
170
|
}
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
171
|
+
async updateKanbanTicket(id, updates) {
|
|
172
|
+
await this.ensureInitialized();
|
|
173
|
+
const index = this.db.data.kanban_tickets.findIndex((t) => t.id === id);
|
|
174
|
+
if (index !== -1) {
|
|
175
|
+
this.db.data.kanban_tickets[index] = {
|
|
176
|
+
...this.db.data.kanban_tickets[index],
|
|
177
|
+
...updates,
|
|
178
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
179
|
+
};
|
|
180
|
+
await this.db.write();
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
async getKanbanTickets() {
|
|
184
|
+
await this.ensureInitialized();
|
|
185
|
+
return this.db.data.kanban_tickets.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
|
|
186
|
+
}
|
|
187
|
+
async getKanbanTicketsByColumn(column) {
|
|
188
|
+
await this.ensureInitialized();
|
|
189
|
+
return this.db.data.kanban_tickets.filter((t) => t.column === column).sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
|
|
190
|
+
}
|
|
191
|
+
async clearAllConfig() {
|
|
192
|
+
await this.ensureInitialized();
|
|
193
|
+
this.db.data.config = {};
|
|
194
|
+
await this.db.write();
|
|
195
|
+
}
|
|
196
|
+
async close() {
|
|
197
|
+
}
|
|
198
|
+
};
|
|
827
199
|
}
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
// agent/config/index.ts
|
|
203
|
+
import { config as dotenvConfig } from "dotenv";
|
|
204
|
+
var ConfigManager;
|
|
205
|
+
var init_config = __esm({
|
|
206
|
+
"agent/config/index.ts"() {
|
|
207
|
+
"use strict";
|
|
208
|
+
init_esm_shims();
|
|
209
|
+
init_database();
|
|
210
|
+
dotenvConfig();
|
|
211
|
+
ConfigManager = class {
|
|
212
|
+
db = null;
|
|
213
|
+
envConfig;
|
|
214
|
+
constructor(dbPath) {
|
|
215
|
+
this.envConfig = this.loadFromEnv();
|
|
216
|
+
}
|
|
217
|
+
loadFromEnv() {
|
|
218
|
+
return {
|
|
219
|
+
llm: {
|
|
220
|
+
provider: process.env.LLM_PROVIDER || "openai",
|
|
221
|
+
apiKey: process.env.OPENAI_API_KEY || process.env.ANTHROPIC_API_KEY,
|
|
222
|
+
model: process.env.LLM_MODEL,
|
|
223
|
+
baseUrl: process.env.OLLAMA_BASE_URL
|
|
224
|
+
},
|
|
225
|
+
saas: {
|
|
226
|
+
url: process.env.SAAS_URL || "",
|
|
227
|
+
authType: process.env.SAAS_AUTH_TYPE || "none",
|
|
228
|
+
username: process.env.SAAS_USERNAME,
|
|
229
|
+
password: process.env.SAAS_PASSWORD
|
|
230
|
+
},
|
|
231
|
+
github: process.env.GITHUB_TOKEN ? {
|
|
232
|
+
token: process.env.GITHUB_TOKEN,
|
|
233
|
+
owner: process.env.GITHUB_OWNER || "",
|
|
234
|
+
repo: process.env.GITHUB_REPO || ""
|
|
235
|
+
} : void 0,
|
|
236
|
+
agent: {
|
|
237
|
+
intervalMs: parseInt(process.env.AGENT_INTERVAL_MS || "3600000"),
|
|
238
|
+
maxIterations: parseInt(process.env.AGENT_MAX_ITERATIONS || "20"),
|
|
239
|
+
autoStart: process.env.AGENT_AUTO_START === "true"
|
|
240
|
+
},
|
|
241
|
+
web: {
|
|
242
|
+
port: parseInt(process.env.WEB_PORT || "4242"),
|
|
243
|
+
host: process.env.WEB_HOST || "0.0.0.0"
|
|
244
|
+
},
|
|
245
|
+
database: {
|
|
246
|
+
path: process.env.DB_PATH || "./data/openqa.db"
|
|
247
|
+
},
|
|
248
|
+
notifications: {
|
|
249
|
+
slack: process.env.SLACK_WEBHOOK_URL,
|
|
250
|
+
discord: process.env.DISCORD_WEBHOOK_URL
|
|
251
|
+
}
|
|
849
252
|
};
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
253
|
+
}
|
|
254
|
+
getDB() {
|
|
255
|
+
if (!this.db) {
|
|
256
|
+
this.db = new OpenQADatabase("./data/openqa.json");
|
|
854
257
|
}
|
|
258
|
+
return this.db;
|
|
855
259
|
}
|
|
856
|
-
|
|
857
|
-
|
|
260
|
+
async get(key) {
|
|
261
|
+
const dbValue = await this.getDB().getConfig(key);
|
|
262
|
+
if (dbValue) return dbValue;
|
|
263
|
+
const keys = key.split(".");
|
|
264
|
+
let value = this.envConfig;
|
|
265
|
+
for (const k of keys) {
|
|
266
|
+
value = value?.[k];
|
|
267
|
+
}
|
|
268
|
+
return value?.toString() || null;
|
|
858
269
|
}
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
)
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
commit: pipeline.sha,
|
|
872
|
-
author: pipeline.user?.name || "unknown",
|
|
873
|
-
message: `Pipeline #${pipeline.id}`,
|
|
874
|
-
timestamp: new Date(pipeline.updated_at),
|
|
875
|
-
pipelineId: pipeline.id.toString(),
|
|
876
|
-
pipelineStatus: pipeline.status
|
|
877
|
-
};
|
|
878
|
-
this.emit("git-event", event);
|
|
879
|
-
if (pipeline.status === "success") {
|
|
880
|
-
this.emit("pipeline-success", event);
|
|
881
|
-
console.log(`\u2705 Pipeline success: #${pipeline.id}`);
|
|
882
|
-
} else {
|
|
883
|
-
this.emit("pipeline-failure", event);
|
|
884
|
-
console.log(`\u274C Pipeline failure: #${pipeline.id}`);
|
|
270
|
+
async set(key, value) {
|
|
271
|
+
await this.getDB().setConfig(key, value);
|
|
272
|
+
}
|
|
273
|
+
async getAll() {
|
|
274
|
+
const dbConfig = await this.getDB().getAllConfig();
|
|
275
|
+
const merged = { ...this.envConfig };
|
|
276
|
+
for (const [key, value] of Object.entries(dbConfig)) {
|
|
277
|
+
const keys = key.split(".");
|
|
278
|
+
let obj = merged;
|
|
279
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
280
|
+
if (!obj[keys[i]]) obj[keys[i]] = {};
|
|
281
|
+
obj = obj[keys[i]];
|
|
885
282
|
}
|
|
283
|
+
obj[keys[keys.length - 1]] = value;
|
|
886
284
|
}
|
|
285
|
+
return merged;
|
|
887
286
|
}
|
|
888
|
-
|
|
889
|
-
|
|
287
|
+
async getConfig() {
|
|
288
|
+
return await this.getAll();
|
|
890
289
|
}
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
}
|
|
895
|
-
async setupWebhook(webhookUrl) {
|
|
896
|
-
if (this.config.provider === "github") {
|
|
897
|
-
return this.setupGitHubWebhook(webhookUrl);
|
|
898
|
-
} else {
|
|
899
|
-
return this.setupGitLabWebhook(webhookUrl);
|
|
900
|
-
}
|
|
901
|
-
}
|
|
902
|
-
async setupGitHubWebhook(webhookUrl) {
|
|
903
|
-
if (!this.octokit) throw new Error("GitHub not configured");
|
|
904
|
-
const { data } = await this.octokit.repos.createWebhook({
|
|
905
|
-
owner: this.config.owner,
|
|
906
|
-
repo: this.config.repo,
|
|
907
|
-
config: {
|
|
908
|
-
url: webhookUrl,
|
|
909
|
-
content_type: "json"
|
|
910
|
-
},
|
|
911
|
-
events: ["push", "pull_request", "workflow_run"]
|
|
912
|
-
});
|
|
913
|
-
return data.id.toString();
|
|
914
|
-
}
|
|
915
|
-
async setupGitLabWebhook(webhookUrl) {
|
|
916
|
-
const headers = {
|
|
917
|
-
"PRIVATE-TOKEN": this.config.token,
|
|
918
|
-
"Content-Type": "application/json"
|
|
919
|
-
};
|
|
920
|
-
const projectPath = encodeURIComponent(`${this.config.owner}/${this.config.repo}`);
|
|
921
|
-
const res = await fetch(
|
|
922
|
-
`${this.config.gitlabUrl}/api/v4/projects/${projectPath}/hooks`,
|
|
923
|
-
{
|
|
924
|
-
method: "POST",
|
|
925
|
-
headers,
|
|
926
|
-
body: JSON.stringify({
|
|
927
|
-
url: webhookUrl,
|
|
928
|
-
push_events: true,
|
|
929
|
-
merge_requests_events: true,
|
|
930
|
-
pipeline_events: true
|
|
931
|
-
})
|
|
290
|
+
// Synchronous version that only uses env vars (no DB)
|
|
291
|
+
getConfigSync() {
|
|
292
|
+
return this.envConfig;
|
|
932
293
|
}
|
|
933
|
-
|
|
934
|
-
const data = await res.json();
|
|
935
|
-
return data.id.toString();
|
|
936
|
-
}
|
|
937
|
-
};
|
|
938
|
-
|
|
939
|
-
// agent/specialists/index.ts
|
|
940
|
-
import { ReActAgent } from "@orka-js/agent";
|
|
941
|
-
import { OpenAIAdapter } from "@orka-js/openai";
|
|
942
|
-
import { AnthropicAdapter } from "@orka-js/anthropic";
|
|
943
|
-
import { EventEmitter as EventEmitter2 } from "events";
|
|
944
|
-
var SPECIALIST_PROMPTS = {
|
|
945
|
-
"form-tester": `You are a Form Testing Specialist. Your mission:
|
|
946
|
-
- Find all forms on the page (login, signup, contact, search, etc.)
|
|
947
|
-
- Test form validation (empty fields, invalid formats, boundary values)
|
|
948
|
-
- Test error messages and user feedback
|
|
949
|
-
- Test form submission success/failure scenarios
|
|
950
|
-
- Check for proper field types (email, password, phone)
|
|
951
|
-
- Test autofill behavior
|
|
952
|
-
- Report any form-related bugs with clear reproduction steps`,
|
|
953
|
-
"security-scanner": `You are a Security Scanner Specialist. Your mission:
|
|
954
|
-
- Identify potential security vulnerabilities
|
|
955
|
-
- Check for exposed sensitive data in page source
|
|
956
|
-
- Look for insecure HTTP resources on HTTPS pages
|
|
957
|
-
- Check for missing security headers
|
|
958
|
-
- Identify potential CSRF vulnerabilities
|
|
959
|
-
- Check for information disclosure in error messages
|
|
960
|
-
- Look for hardcoded credentials or API keys
|
|
961
|
-
- Report security issues with severity ratings`,
|
|
962
|
-
"sql-injection": `You are a SQL Injection Testing Specialist. Your mission:
|
|
963
|
-
- Identify input fields that might interact with databases
|
|
964
|
-
- Test common SQL injection payloads (', ", --, ;, OR 1=1, etc.)
|
|
965
|
-
- Test for blind SQL injection (time-based, boolean-based)
|
|
966
|
-
- Check URL parameters for injection vulnerabilities
|
|
967
|
-
- Test search fields, login forms, and filters
|
|
968
|
-
- Document any successful injections with exact payloads
|
|
969
|
-
- Rate severity based on data exposure risk`,
|
|
970
|
-
"xss-tester": `You are an XSS (Cross-Site Scripting) Testing Specialist. Your mission:
|
|
971
|
-
- Find all user input fields that reflect content
|
|
972
|
-
- Test for reflected XSS (<script>, onerror, onload, etc.)
|
|
973
|
-
- Test for stored XSS in comments, profiles, messages
|
|
974
|
-
- Check for DOM-based XSS vulnerabilities
|
|
975
|
-
- Test various encoding bypasses
|
|
976
|
-
- Check if Content-Security-Policy is properly configured
|
|
977
|
-
- Document successful XSS with exact payloads`,
|
|
978
|
-
"component-tester": `You are a UI Component Testing Specialist. Your mission:
|
|
979
|
-
- Test all interactive components (buttons, dropdowns, modals, tabs)
|
|
980
|
-
- Verify component states (hover, active, disabled, loading)
|
|
981
|
-
- Test responsive behavior at different viewport sizes
|
|
982
|
-
- Check for broken layouts or overlapping elements
|
|
983
|
-
- Test keyboard navigation and focus management
|
|
984
|
-
- Verify animations and transitions work correctly
|
|
985
|
-
- Report visual bugs with screenshots`,
|
|
986
|
-
"accessibility-tester": `You are an Accessibility Testing Specialist. Your mission:
|
|
987
|
-
- Check for proper ARIA labels and roles
|
|
988
|
-
- Verify keyboard navigation works for all interactive elements
|
|
989
|
-
- Check color contrast ratios
|
|
990
|
-
- Verify images have alt text
|
|
991
|
-
- Test screen reader compatibility
|
|
992
|
-
- Check for proper heading hierarchy
|
|
993
|
-
- Verify focus indicators are visible
|
|
994
|
-
- Report WCAG violations with severity`,
|
|
995
|
-
"performance-tester": `You are a Performance Testing Specialist. Your mission:
|
|
996
|
-
- Measure page load times
|
|
997
|
-
- Identify slow-loading resources
|
|
998
|
-
- Check for render-blocking resources
|
|
999
|
-
- Monitor network requests and response times
|
|
1000
|
-
- Identify memory leaks or excessive DOM nodes
|
|
1001
|
-
- Check for unnecessary re-renders
|
|
1002
|
-
- Test under simulated slow network conditions
|
|
1003
|
-
- Report performance issues with metrics`,
|
|
1004
|
-
"api-tester": `You are an API Testing Specialist. Your mission:
|
|
1005
|
-
- Monitor network requests made by the application
|
|
1006
|
-
- Test API error handling
|
|
1007
|
-
- Check for proper authentication on API calls
|
|
1008
|
-
- Verify API response formats
|
|
1009
|
-
- Test rate limiting behavior
|
|
1010
|
-
- Check for exposed internal APIs
|
|
1011
|
-
- Verify proper HTTP methods are used
|
|
1012
|
-
- Report API issues with request/response details`,
|
|
1013
|
-
"auth-tester": `You are an Authentication Testing Specialist. Your mission:
|
|
1014
|
-
- Test login with valid/invalid credentials
|
|
1015
|
-
- Test password reset flow
|
|
1016
|
-
- Check session management (timeout, persistence)
|
|
1017
|
-
- Test logout functionality
|
|
1018
|
-
- Check for session fixation vulnerabilities
|
|
1019
|
-
- Test remember me functionality
|
|
1020
|
-
- Verify proper access control on protected pages
|
|
1021
|
-
- Test multi-factor authentication if present`,
|
|
1022
|
-
"navigation-tester": `You are a Navigation Testing Specialist. Your mission:
|
|
1023
|
-
- Test all navigation links and menus
|
|
1024
|
-
- Verify breadcrumbs work correctly
|
|
1025
|
-
- Test browser back/forward behavior
|
|
1026
|
-
- Check for broken links (404s)
|
|
1027
|
-
- Test deep linking and URL sharing
|
|
1028
|
-
- Verify redirects work properly
|
|
1029
|
-
- Test pagination and infinite scroll
|
|
1030
|
-
- Report navigation issues with affected URLs`
|
|
1031
|
-
};
|
|
1032
|
-
var SpecialistAgentManager = class extends EventEmitter2 {
|
|
1033
|
-
agents = /* @__PURE__ */ new Map();
|
|
1034
|
-
agentStatuses = /* @__PURE__ */ new Map();
|
|
1035
|
-
db;
|
|
1036
|
-
sessionId;
|
|
1037
|
-
llmConfig;
|
|
1038
|
-
browserTools;
|
|
1039
|
-
constructor(db, sessionId, llmConfig, browserTools) {
|
|
1040
|
-
super();
|
|
1041
|
-
this.db = db;
|
|
1042
|
-
this.sessionId = sessionId;
|
|
1043
|
-
this.llmConfig = llmConfig;
|
|
1044
|
-
this.browserTools = browserTools;
|
|
1045
|
-
}
|
|
1046
|
-
createLLMAdapter() {
|
|
1047
|
-
if (this.llmConfig.provider === "anthropic") {
|
|
1048
|
-
return new AnthropicAdapter({
|
|
1049
|
-
apiKey: this.llmConfig.apiKey,
|
|
1050
|
-
model: this.llmConfig.model || "claude-3-5-sonnet-20241022"
|
|
1051
|
-
});
|
|
1052
|
-
}
|
|
1053
|
-
return new OpenAIAdapter({
|
|
1054
|
-
apiKey: this.llmConfig.apiKey,
|
|
1055
|
-
model: this.llmConfig.model || "gpt-4"
|
|
1056
|
-
});
|
|
294
|
+
};
|
|
1057
295
|
}
|
|
1058
|
-
|
|
1059
|
-
const agentId = `${type}_${Date.now()}`;
|
|
1060
|
-
const systemPrompt = customPrompt || SPECIALIST_PROMPTS[type];
|
|
1061
|
-
const agent = new ReActAgent({
|
|
1062
|
-
llm: this.createLLMAdapter(),
|
|
1063
|
-
tools: this.browserTools.getTools(),
|
|
1064
|
-
maxIterations: 15,
|
|
1065
|
-
systemPrompt: `${systemPrompt}
|
|
296
|
+
});
|
|
1066
297
|
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
298
|
+
// cli/server.ts
|
|
299
|
+
var server_exports = {};
|
|
300
|
+
__export(server_exports, {
|
|
301
|
+
startWebServer: () => startWebServer
|
|
302
|
+
});
|
|
303
|
+
import express from "express";
|
|
304
|
+
import { WebSocketServer } from "ws";
|
|
305
|
+
import chalk from "chalk";
|
|
306
|
+
async function startWebServer() {
|
|
307
|
+
const config = new ConfigManager();
|
|
308
|
+
const cfg = config.getConfigSync();
|
|
309
|
+
const db = new OpenQADatabase("./data/openqa.json");
|
|
310
|
+
const app = express();
|
|
311
|
+
app.use(express.json());
|
|
312
|
+
const wss = new WebSocketServer({ noServer: true });
|
|
313
|
+
app.get("/api/status", async (req, res) => {
|
|
314
|
+
res.json({
|
|
315
|
+
isRunning: true,
|
|
316
|
+
target: cfg.saas.url || "Not configured"
|
|
1073
317
|
});
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
318
|
+
});
|
|
319
|
+
app.get("/api/sessions", async (req, res) => {
|
|
320
|
+
const limit = parseInt(req.query.limit) || 10;
|
|
321
|
+
const sessions = await db.getRecentSessions(limit);
|
|
322
|
+
res.json(sessions);
|
|
323
|
+
});
|
|
324
|
+
app.get("/api/sessions/:id/actions", async (req, res) => {
|
|
325
|
+
const actions = await db.getSessionActions(req.params.id);
|
|
326
|
+
res.json(actions);
|
|
327
|
+
});
|
|
328
|
+
app.get("/api/bugs", async (req, res) => {
|
|
329
|
+
const status = req.query.status;
|
|
330
|
+
const bugs = status ? await db.getBugsByStatus(status) : await db.getAllBugs();
|
|
331
|
+
res.json(bugs);
|
|
332
|
+
});
|
|
333
|
+
app.get("/api/kanban/tickets", async (req, res) => {
|
|
334
|
+
const column = req.query.column;
|
|
335
|
+
const tickets = column ? await db.getKanbanTicketsByColumn(column) : await db.getKanbanTickets();
|
|
336
|
+
res.json(tickets);
|
|
337
|
+
});
|
|
338
|
+
app.patch("/api/kanban/tickets/:id", async (req, res) => {
|
|
339
|
+
const { id } = req.params;
|
|
340
|
+
const updates = req.body;
|
|
341
|
+
await db.updateKanbanTicket(id, updates);
|
|
342
|
+
res.json({ success: true });
|
|
343
|
+
});
|
|
344
|
+
app.get("/api/config", (req, res) => {
|
|
345
|
+
res.json(cfg);
|
|
346
|
+
});
|
|
347
|
+
app.post("/api/config", async (req, res) => {
|
|
1097
348
|
try {
|
|
1098
|
-
const
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
status.progress = 100;
|
|
1104
|
-
this.emit("agent-completed", { ...status, result });
|
|
349
|
+
const configData = req.body;
|
|
350
|
+
for (const [key, value] of Object.entries(configData)) {
|
|
351
|
+
await config.set(key, String(value));
|
|
352
|
+
}
|
|
353
|
+
res.json({ success: true });
|
|
1105
354
|
} catch (error) {
|
|
1106
|
-
status.
|
|
1107
|
-
status.completedAt = /* @__PURE__ */ new Date();
|
|
1108
|
-
this.emit("agent-failed", { ...status, error: error.message });
|
|
1109
|
-
}
|
|
1110
|
-
}
|
|
1111
|
-
async runAllSpecialists(targetUrl, types) {
|
|
1112
|
-
const agentTypes = types || [
|
|
1113
|
-
"form-tester",
|
|
1114
|
-
"security-scanner",
|
|
1115
|
-
"component-tester",
|
|
1116
|
-
"navigation-tester"
|
|
1117
|
-
];
|
|
1118
|
-
const agentIds = agentTypes.map((type) => this.createSpecialist(type));
|
|
1119
|
-
for (const agentId of agentIds) {
|
|
1120
|
-
await this.runSpecialist(agentId, targetUrl);
|
|
1121
|
-
}
|
|
1122
|
-
}
|
|
1123
|
-
async runSecuritySuite(targetUrl) {
|
|
1124
|
-
const securityTypes = [
|
|
1125
|
-
"security-scanner",
|
|
1126
|
-
"sql-injection",
|
|
1127
|
-
"xss-tester",
|
|
1128
|
-
"auth-tester"
|
|
1129
|
-
];
|
|
1130
|
-
await this.runAllSpecialists(targetUrl, securityTypes);
|
|
1131
|
-
}
|
|
1132
|
-
getAgentStatus(agentId) {
|
|
1133
|
-
return this.agentStatuses.get(agentId);
|
|
1134
|
-
}
|
|
1135
|
-
getAllStatuses() {
|
|
1136
|
-
return Array.from(this.agentStatuses.values());
|
|
1137
|
-
}
|
|
1138
|
-
stopAgent(agentId) {
|
|
1139
|
-
const status = this.agentStatuses.get(agentId);
|
|
1140
|
-
if (status && status.status === "running") {
|
|
1141
|
-
status.status = "failed";
|
|
1142
|
-
status.completedAt = /* @__PURE__ */ new Date();
|
|
1143
|
-
this.emit("agent-stopped", status);
|
|
355
|
+
res.status(500).json({ success: false, error: error.message });
|
|
1144
356
|
}
|
|
1145
|
-
}
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
};
|
|
357
|
+
});
|
|
358
|
+
app.post("/api/config/reset", async (req, res) => {
|
|
359
|
+
try {
|
|
360
|
+
await db.clearAllConfig();
|
|
361
|
+
res.json({ success: true });
|
|
362
|
+
} catch (error) {
|
|
363
|
+
res.status(500).json({ success: false, error: error.message });
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
app.get("/", (req, res) => {
|
|
367
|
+
res.send(`
|
|
368
|
+
<!DOCTYPE html>
|
|
369
|
+
<html>
|
|
370
|
+
<head>
|
|
371
|
+
<title>OpenQA - Dashboard</title>
|
|
372
|
+
<style>
|
|
373
|
+
body { font-family: system-ui; max-width: 1200px; margin: 40px auto; padding: 20px; background: #0f172a; color: #e2e8f0; }
|
|
374
|
+
h1 { color: #38bdf8; }
|
|
375
|
+
.card { background: #1e293b; border: 1px solid #334155; border-radius: 8px; padding: 20px; margin: 20px 0; }
|
|
376
|
+
.status { display: inline-block; padding: 4px 12px; border-radius: 4px; font-size: 14px; }
|
|
377
|
+
.status.running { background: #10b981; color: white; }
|
|
378
|
+
.status.idle { background: #f59e0b; color: white; }
|
|
379
|
+
a { color: #38bdf8; text-decoration: none; }
|
|
380
|
+
a:hover { text-decoration: underline; }
|
|
381
|
+
nav { margin: 20px 0; }
|
|
382
|
+
nav a { margin-right: 20px; }
|
|
383
|
+
</style>
|
|
384
|
+
</head>
|
|
385
|
+
<body>
|
|
386
|
+
<h1>\u{1F916} OpenQA Dashboard</h1>
|
|
387
|
+
<nav>
|
|
388
|
+
<a href="/">Dashboard</a>
|
|
389
|
+
<a href="/kanban">Kanban</a>
|
|
390
|
+
<a href="/config">Config</a>
|
|
391
|
+
</nav>
|
|
392
|
+
<div class="card">
|
|
393
|
+
<h2>Status</h2>
|
|
394
|
+
<p>Agent: <span class="status idle">Idle</span></p>
|
|
395
|
+
<p>Target: ${cfg.saas.url || "Not configured"}</p>
|
|
396
|
+
<p>Auto-start: ${cfg.agent.autoStart ? "Enabled" : "Disabled"}</p>
|
|
397
|
+
</div>
|
|
398
|
+
<div class="card">
|
|
399
|
+
<h2>Quick Links</h2>
|
|
400
|
+
<ul>
|
|
401
|
+
<li><a href="/kanban">View Kanban Board</a></li>
|
|
402
|
+
<li><a href="/config">Configure OpenQA</a></li>
|
|
403
|
+
</ul>
|
|
404
|
+
</div>
|
|
405
|
+
<div class="card">
|
|
406
|
+
<h2>Getting Started</h2>
|
|
407
|
+
<p>Configure your SaaS application target and start testing:</p>
|
|
408
|
+
<ol>
|
|
409
|
+
<li>Set SAAS_URL environment variable or use the <a href="/config">Config page</a></li>
|
|
410
|
+
<li>Enable auto-start: <code>export AGENT_AUTO_START=true</code></li>
|
|
411
|
+
<li>Restart OpenQA</li>
|
|
412
|
+
</ol>
|
|
413
|
+
</div>
|
|
414
|
+
</body>
|
|
415
|
+
</html>
|
|
416
|
+
`);
|
|
417
|
+
});
|
|
418
|
+
app.get("/kanban", (req, res) => {
|
|
419
|
+
res.send(`
|
|
420
|
+
<!DOCTYPE html>
|
|
421
|
+
<html>
|
|
422
|
+
<head>
|
|
423
|
+
<title>OpenQA - Kanban Board</title>
|
|
424
|
+
<style>
|
|
425
|
+
body { font-family: system-ui; max-width: 1400px; margin: 40px auto; padding: 20px; background: #0f172a; color: #e2e8f0; }
|
|
426
|
+
h1 { color: #38bdf8; }
|
|
427
|
+
nav { margin: 20px 0; }
|
|
428
|
+
nav a { color: #38bdf8; text-decoration: none; margin-right: 20px; }
|
|
429
|
+
nav a:hover { text-decoration: underline; }
|
|
430
|
+
.board { display: grid; grid-template-columns: repeat(4, 1fr); gap: 20px; margin-top: 30px; }
|
|
431
|
+
.column { background: #1e293b; border: 1px solid #334155; border-radius: 8px; padding: 15px; }
|
|
432
|
+
.column h3 { margin-top: 0; color: #38bdf8; }
|
|
433
|
+
.ticket { background: #334155; padding: 12px; margin: 10px 0; border-radius: 6px; border-left: 3px solid #38bdf8; }
|
|
434
|
+
.ticket h4 { margin: 0 0 8px 0; font-size: 14px; }
|
|
435
|
+
.ticket p { margin: 0; font-size: 12px; color: #94a3b8; }
|
|
436
|
+
</style>
|
|
437
|
+
</head>
|
|
438
|
+
<body>
|
|
439
|
+
<h1>\u{1F4CB} Kanban Board</h1>
|
|
440
|
+
<nav>
|
|
441
|
+
<a href="/">Dashboard</a>
|
|
442
|
+
<a href="/kanban">Kanban</a>
|
|
443
|
+
<a href="/config">Config</a>
|
|
444
|
+
</nav>
|
|
445
|
+
<div class="board">
|
|
446
|
+
<div class="column">
|
|
447
|
+
<h3>Backlog</h3>
|
|
448
|
+
<p style="color: #64748b;">No tickets yet</p>
|
|
449
|
+
</div>
|
|
450
|
+
<div class="column">
|
|
451
|
+
<h3>To Do</h3>
|
|
452
|
+
<p style="color: #64748b;">No tickets yet</p>
|
|
453
|
+
</div>
|
|
454
|
+
<div class="column">
|
|
455
|
+
<h3>In Progress</h3>
|
|
456
|
+
<p style="color: #64748b;">No tickets yet</p>
|
|
457
|
+
</div>
|
|
458
|
+
<div class="column">
|
|
459
|
+
<h3>Done</h3>
|
|
460
|
+
<p style="color: #64748b;">No tickets yet</p>
|
|
461
|
+
</div>
|
|
462
|
+
</div>
|
|
463
|
+
<p style="margin-top: 40px; color: #64748b;">Tickets will appear here when the agent starts finding bugs and creating tasks.</p>
|
|
464
|
+
</body>
|
|
465
|
+
</html>
|
|
466
|
+
`);
|
|
467
|
+
});
|
|
468
|
+
app.get("/config", (req, res) => {
|
|
469
|
+
res.send(`
|
|
470
|
+
<!DOCTYPE html>
|
|
471
|
+
<html>
|
|
472
|
+
<head>
|
|
473
|
+
<title>OpenQA - Configuration</title>
|
|
474
|
+
<style>
|
|
475
|
+
body { font-family: system-ui; max-width: 800px; margin: 40px auto; padding: 20px; background: #0f172a; color: #e2e8f0; }
|
|
476
|
+
h1 { color: #38bdf8; }
|
|
477
|
+
nav { margin: 20px 0; }
|
|
478
|
+
nav a { color: #38bdf8; text-decoration: none; margin-right: 20px; }
|
|
479
|
+
nav a:hover { text-decoration: underline; }
|
|
480
|
+
.section { background: #1e293b; border: 1px solid #334155; border-radius: 8px; padding: 20px; margin: 20px 0; }
|
|
481
|
+
.section h2 { margin-top: 0; color: #38bdf8; font-size: 18px; }
|
|
482
|
+
.config-item { margin: 15px 0; }
|
|
483
|
+
.config-item label { display: block; margin-bottom: 5px; color: #94a3b8; font-size: 14px; }
|
|
484
|
+
.config-item input, .config-item select {
|
|
485
|
+
background: #334155;
|
|
486
|
+
border: 1px solid #475569;
|
|
487
|
+
color: #e2e8f0;
|
|
488
|
+
padding: 8px 12px;
|
|
489
|
+
border-radius: 4px;
|
|
490
|
+
font-family: monospace;
|
|
491
|
+
font-size: 14px;
|
|
492
|
+
width: 100%;
|
|
493
|
+
max-width: 400px;
|
|
494
|
+
}
|
|
495
|
+
.config-item input:focus, .config-item select:focus {
|
|
496
|
+
outline: none;
|
|
497
|
+
border-color: #38bdf8;
|
|
498
|
+
box-shadow: 0 0 0 2px rgba(56, 189, 248, 0.1);
|
|
499
|
+
}
|
|
500
|
+
.btn {
|
|
501
|
+
background: #38bdf8;
|
|
502
|
+
color: white;
|
|
503
|
+
border: none;
|
|
504
|
+
padding: 10px 20px;
|
|
505
|
+
border-radius: 6px;
|
|
506
|
+
cursor: pointer;
|
|
507
|
+
font-size: 14px;
|
|
508
|
+
margin-right: 10px;
|
|
509
|
+
}
|
|
510
|
+
.btn:hover { background: #0ea5e9; }
|
|
511
|
+
.btn-secondary { background: #64748b; }
|
|
512
|
+
.btn-secondary:hover { background: #475569; }
|
|
513
|
+
.success { color: #10b981; margin-left: 10px; }
|
|
514
|
+
.error { color: #ef4444; margin-left: 10px; }
|
|
515
|
+
code { background: #334155; padding: 2px 6px; border-radius: 3px; font-size: 13px; }
|
|
516
|
+
.checkbox { margin-right: 8px; }
|
|
517
|
+
</style>
|
|
518
|
+
</head>
|
|
519
|
+
<body>
|
|
520
|
+
<h1>\u2699\uFE0F Configuration</h1>
|
|
521
|
+
<nav>
|
|
522
|
+
<a href="/">Dashboard</a>
|
|
523
|
+
<a href="/kanban">Kanban</a>
|
|
524
|
+
<a href="/config">Config</a>
|
|
525
|
+
</nav>
|
|
526
|
+
|
|
527
|
+
<div class="section">
|
|
528
|
+
<h2>SaaS Target</h2>
|
|
529
|
+
<form id="configForm">
|
|
530
|
+
<div class="config-item">
|
|
531
|
+
<label>URL</label>
|
|
532
|
+
<input type="url" id="saas_url" name="saas.url" value="${cfg.saas.url || ""}" placeholder="https://your-app.com">
|
|
533
|
+
</div>
|
|
534
|
+
<div class="config-item">
|
|
535
|
+
<label>Auth Type</label>
|
|
536
|
+
<select id="saas_authType" name="saas.authType">
|
|
537
|
+
<option value="none" ${cfg.saas.authType === "none" ? "selected" : ""}>None</option>
|
|
538
|
+
<option value="basic" ${cfg.saas.authType === "basic" ? "selected" : ""}>Basic Auth</option>
|
|
539
|
+
<option value="bearer" ${cfg.saas.authType === "bearer" ? "selected" : ""}>Bearer Token</option>
|
|
540
|
+
<option value="session" ${cfg.saas.authType === "session" ? "selected" : ""}>Session</option>
|
|
541
|
+
</select>
|
|
542
|
+
</div>
|
|
543
|
+
<div class="config-item">
|
|
544
|
+
<label>Username (for Basic Auth)</label>
|
|
545
|
+
<input type="text" id="saas_username" name="saas.username" value="${cfg.saas.username || ""}" placeholder="username">
|
|
546
|
+
</div>
|
|
547
|
+
<div class="config-item">
|
|
548
|
+
<label>Password (for Basic Auth)</label>
|
|
549
|
+
<input type="password" id="saas_password" name="saas.password" value="${cfg.saas.password || ""}" placeholder="password">
|
|
550
|
+
</div>
|
|
551
|
+
</form>
|
|
552
|
+
</div>
|
|
1152
553
|
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
- Check for horizontal scrolling issues
|
|
1179
|
-
- Verify touch targets are large enough
|
|
1180
|
-
- Check navigation menu behavior on mobile
|
|
1181
|
-
- Report any responsive design issues`,
|
|
1182
|
-
triggers: ["mobile", "responsive", "viewport"]
|
|
1183
|
-
},
|
|
1184
|
-
{
|
|
1185
|
-
name: "E-commerce Flow",
|
|
1186
|
-
description: "Test complete e-commerce purchase flow",
|
|
1187
|
-
type: "workflow",
|
|
1188
|
-
enabled: false,
|
|
1189
|
-
priority: 3,
|
|
1190
|
-
prompt: `Test e-commerce flow:
|
|
1191
|
-
- Browse products
|
|
1192
|
-
- Add items to cart
|
|
1193
|
-
- Verify cart updates correctly
|
|
1194
|
-
- Test checkout process
|
|
1195
|
-
- Test payment form validation
|
|
1196
|
-
- Verify order confirmation
|
|
1197
|
-
- Report any issues in the purchase flow`,
|
|
1198
|
-
triggers: ["shop", "cart", "checkout", "payment", "ecommerce"]
|
|
1199
|
-
},
|
|
1200
|
-
{
|
|
1201
|
-
name: "Dark Mode Testing",
|
|
1202
|
-
description: "Test dark mode if available",
|
|
1203
|
-
type: "custom-check",
|
|
1204
|
-
enabled: true,
|
|
1205
|
-
priority: 4,
|
|
1206
|
-
prompt: `Test dark mode:
|
|
1207
|
-
- Look for dark mode toggle
|
|
1208
|
-
- Switch between light and dark modes
|
|
1209
|
-
- Check for contrast issues in dark mode
|
|
1210
|
-
- Verify all text is readable
|
|
1211
|
-
- Check images and icons visibility
|
|
1212
|
-
- Report any dark mode specific bugs`,
|
|
1213
|
-
triggers: ["dark", "theme", "mode"]
|
|
1214
|
-
},
|
|
1215
|
-
{
|
|
1216
|
-
name: "Error Handling",
|
|
1217
|
-
description: "Test application error handling",
|
|
1218
|
-
type: "test-scenario",
|
|
1219
|
-
enabled: true,
|
|
1220
|
-
priority: 1,
|
|
1221
|
-
prompt: `Test error handling:
|
|
1222
|
-
- Try accessing non-existent pages (404)
|
|
1223
|
-
- Submit forms with invalid data
|
|
1224
|
-
- Test with network errors (if possible)
|
|
1225
|
-
- Check error message clarity
|
|
1226
|
-
- Verify errors don't expose sensitive info
|
|
1227
|
-
- Test recovery from error states
|
|
1228
|
-
- Report poor error handling`,
|
|
1229
|
-
triggers: ["error", "404", "exception"]
|
|
1230
|
-
},
|
|
1231
|
-
{
|
|
1232
|
-
name: "Rate Limiting Check",
|
|
1233
|
-
description: "Test for rate limiting on sensitive endpoints",
|
|
1234
|
-
type: "custom-check",
|
|
1235
|
-
enabled: true,
|
|
1236
|
-
priority: 2,
|
|
1237
|
-
prompt: `Test rate limiting:
|
|
1238
|
-
- Attempt multiple rapid login attempts
|
|
1239
|
-
- Test API endpoints for rate limiting
|
|
1240
|
-
- Check for CAPTCHA on repeated failures
|
|
1241
|
-
- Verify account lockout mechanisms
|
|
1242
|
-
- Report missing rate limiting as security issue`,
|
|
1243
|
-
triggers: ["rate", "limit", "brute", "ddos"]
|
|
1244
|
-
}
|
|
1245
|
-
];
|
|
1246
|
-
var SkillManager = class {
|
|
1247
|
-
db;
|
|
1248
|
-
skills = /* @__PURE__ */ new Map();
|
|
1249
|
-
constructor(db) {
|
|
1250
|
-
this.db = db;
|
|
1251
|
-
this.loadSkills();
|
|
1252
|
-
}
|
|
1253
|
-
loadSkills() {
|
|
1254
|
-
DEFAULT_SKILLS.forEach((skill) => {
|
|
1255
|
-
this.createSkill(skill);
|
|
1256
|
-
});
|
|
1257
|
-
}
|
|
1258
|
-
saveSkills() {
|
|
1259
|
-
}
|
|
1260
|
-
createSkill(data) {
|
|
1261
|
-
const skill = {
|
|
1262
|
-
...data,
|
|
1263
|
-
id: `skill_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
|
1264
|
-
createdAt: /* @__PURE__ */ new Date(),
|
|
1265
|
-
updatedAt: /* @__PURE__ */ new Date()
|
|
1266
|
-
};
|
|
1267
|
-
this.skills.set(skill.id, skill);
|
|
1268
|
-
this.saveSkills();
|
|
1269
|
-
return skill;
|
|
1270
|
-
}
|
|
1271
|
-
updateSkill(id, updates) {
|
|
1272
|
-
const skill = this.skills.get(id);
|
|
1273
|
-
if (!skill) return null;
|
|
1274
|
-
const updated = {
|
|
1275
|
-
...skill,
|
|
1276
|
-
...updates,
|
|
1277
|
-
updatedAt: /* @__PURE__ */ new Date()
|
|
1278
|
-
};
|
|
1279
|
-
this.skills.set(id, updated);
|
|
1280
|
-
this.saveSkills();
|
|
1281
|
-
return updated;
|
|
1282
|
-
}
|
|
1283
|
-
deleteSkill(id) {
|
|
1284
|
-
const deleted = this.skills.delete(id);
|
|
1285
|
-
if (deleted) {
|
|
1286
|
-
this.saveSkills();
|
|
1287
|
-
}
|
|
1288
|
-
return deleted;
|
|
1289
|
-
}
|
|
1290
|
-
getSkill(id) {
|
|
1291
|
-
return this.skills.get(id);
|
|
1292
|
-
}
|
|
1293
|
-
getAllSkills() {
|
|
1294
|
-
return Array.from(this.skills.values());
|
|
1295
|
-
}
|
|
1296
|
-
getEnabledSkills() {
|
|
1297
|
-
return this.getAllSkills().filter((s) => s.enabled).sort((a, b) => a.priority - b.priority);
|
|
1298
|
-
}
|
|
1299
|
-
getSkillsByType(type) {
|
|
1300
|
-
return this.getAllSkills().filter((s) => s.type === type);
|
|
1301
|
-
}
|
|
1302
|
-
findSkillsByTrigger(text) {
|
|
1303
|
-
const lowerText = text.toLowerCase();
|
|
1304
|
-
return this.getEnabledSkills().filter(
|
|
1305
|
-
(skill) => skill.triggers?.some((trigger) => lowerText.includes(trigger.toLowerCase()))
|
|
1306
|
-
);
|
|
1307
|
-
}
|
|
1308
|
-
generateSkillPrompt(skills) {
|
|
1309
|
-
if (skills.length === 0) return "";
|
|
1310
|
-
const skillInstructions = skills.map(
|
|
1311
|
-
(skill, index) => `### Skill ${index + 1}: ${skill.name}
|
|
1312
|
-
${skill.prompt}`
|
|
1313
|
-
).join("\n\n");
|
|
1314
|
-
return `
|
|
1315
|
-
## Additional Skills/Directives to Follow
|
|
554
|
+
<div class="section">
|
|
555
|
+
<h2>LLM Configuration</h2>
|
|
556
|
+
<form id="configForm">
|
|
557
|
+
<div class="config-item">
|
|
558
|
+
<label>Provider</label>
|
|
559
|
+
<select id="llm_provider" name="llm.provider">
|
|
560
|
+
<option value="openai" ${cfg.llm.provider === "openai" ? "selected" : ""}>OpenAI</option>
|
|
561
|
+
<option value="anthropic" ${cfg.llm.provider === "anthropic" ? "selected" : ""}>Anthropic</option>
|
|
562
|
+
<option value="ollama" ${cfg.llm.provider === "ollama" ? "selected" : ""}>Ollama</option>
|
|
563
|
+
</select>
|
|
564
|
+
</div>
|
|
565
|
+
<div class="config-item">
|
|
566
|
+
<label>Model</label>
|
|
567
|
+
<input type="text" id="llm_model" name="llm.model" value="${cfg.llm.model || ""}" placeholder="gpt-4, claude-3-sonnet, etc.">
|
|
568
|
+
</div>
|
|
569
|
+
<div class="config-item">
|
|
570
|
+
<label>API Key</label>
|
|
571
|
+
<input type="password" id="llm_apiKey" name="llm.apiKey" value="${cfg.llm.apiKey || ""}" placeholder="Your API key">
|
|
572
|
+
</div>
|
|
573
|
+
<div class="config-item">
|
|
574
|
+
<label>Base URL (for Ollama)</label>
|
|
575
|
+
<input type="url" id="llm_baseUrl" name="llm.baseUrl" value="${cfg.llm.baseUrl || ""}" placeholder="http://localhost:11434">
|
|
576
|
+
</div>
|
|
577
|
+
</form>
|
|
578
|
+
</div>
|
|
1316
579
|
|
|
1317
|
-
|
|
580
|
+
<div class="section">
|
|
581
|
+
<h2>Agent Settings</h2>
|
|
582
|
+
<form id="configForm">
|
|
583
|
+
<div class="config-item">
|
|
584
|
+
<label>
|
|
585
|
+
<input type="checkbox" id="agent_autoStart" name="agent.autoStart" class="checkbox" ${cfg.agent.autoStart ? "checked" : ""}>
|
|
586
|
+
Auto-start
|
|
587
|
+
</label>
|
|
588
|
+
</div>
|
|
589
|
+
<div class="config-item">
|
|
590
|
+
<label>Interval (ms)</label>
|
|
591
|
+
<input type="number" id="agent_intervalMs" name="agent.intervalMs" value="${cfg.agent.intervalMs}" min="60000">
|
|
592
|
+
</div>
|
|
593
|
+
<div class="config-item">
|
|
594
|
+
<label>Max Iterations</label>
|
|
595
|
+
<input type="number" id="agent_maxIterations" name="agent.maxIterations" value="${cfg.agent.maxIterations}" min="1" max="100">
|
|
596
|
+
</div>
|
|
597
|
+
</form>
|
|
598
|
+
</div>
|
|
1318
599
|
|
|
1319
|
-
|
|
600
|
+
<div class="section">
|
|
601
|
+
<h2>Actions</h2>
|
|
602
|
+
<button type="button" class="btn" onclick="saveConfig()">Save Configuration</button>
|
|
603
|
+
<button type="button" class="btn btn-secondary" onclick="resetConfig()">Reset to Defaults</button>
|
|
604
|
+
<span id="message"></span>
|
|
605
|
+
</div>
|
|
1320
606
|
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
607
|
+
<div class="section">
|
|
608
|
+
<h2>Environment Variables</h2>
|
|
609
|
+
<p>You can also set these environment variables before starting OpenQA:</p>
|
|
610
|
+
<pre style="background: #334155; padding: 15px; border-radius: 6px; overflow-x: auto;"><code>export SAAS_URL="https://your-app.com"
|
|
611
|
+
export AGENT_AUTO_START=true
|
|
612
|
+
export LLM_PROVIDER=openai
|
|
613
|
+
export OPENAI_API_KEY="your-key"
|
|
614
|
+
|
|
615
|
+
openqa start</code></pre>
|
|
616
|
+
</div>
|
|
617
|
+
|
|
618
|
+
<script>
|
|
619
|
+
async function saveConfig() {
|
|
620
|
+
const form = document.getElementById('configForm');
|
|
621
|
+
const formData = new FormData(form);
|
|
622
|
+
const config = {};
|
|
623
|
+
|
|
624
|
+
for (let [key, value] of formData.entries()) {
|
|
625
|
+
if (value === '') continue;
|
|
626
|
+
|
|
627
|
+
// Handle nested keys like "saas.url"
|
|
628
|
+
const keys = key.split('.');
|
|
629
|
+
let obj = config;
|
|
630
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
631
|
+
if (!obj[keys[i]]) obj[keys[i]] = {};
|
|
632
|
+
obj = obj[keys[i]];
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// Convert checkbox values to boolean
|
|
636
|
+
if (key.includes('autoStart')) {
|
|
637
|
+
obj[keys[keys.length - 1]] = value === 'on';
|
|
638
|
+
} else if (key.includes('intervalMs') || key.includes('maxIterations')) {
|
|
639
|
+
obj[keys[keys.length - 1]] = parseInt(value);
|
|
640
|
+
} else {
|
|
641
|
+
obj[keys[keys.length - 1]] = value;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
try {
|
|
646
|
+
const response = await fetch('/api/config', {
|
|
647
|
+
method: 'POST',
|
|
648
|
+
headers: { 'Content-Type': 'application/json' },
|
|
649
|
+
body: JSON.stringify(config)
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
const result = await response.json();
|
|
653
|
+
if (result.success) {
|
|
654
|
+
showMessage('Configuration saved successfully!', 'success');
|
|
655
|
+
setTimeout(() => location.reload(), 1500);
|
|
656
|
+
} else {
|
|
657
|
+
showMessage('Failed to save configuration', 'error');
|
|
658
|
+
}
|
|
659
|
+
} catch (error) {
|
|
660
|
+
showMessage('Error: ' + error.message, 'error');
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
async function resetConfig() {
|
|
665
|
+
if (confirm('Are you sure you want to reset all configuration to defaults?')) {
|
|
666
|
+
try {
|
|
667
|
+
const response = await fetch('/api/config/reset', { method: 'POST' });
|
|
668
|
+
const result = await response.json();
|
|
669
|
+
if (result.success) {
|
|
670
|
+
showMessage('Configuration reset to defaults', 'success');
|
|
671
|
+
setTimeout(() => location.reload(), 1500);
|
|
672
|
+
}
|
|
673
|
+
} catch (error) {
|
|
674
|
+
showMessage('Error: ' + error.message, 'error');
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
function showMessage(text, type) {
|
|
680
|
+
const messageEl = document.getElementById('message');
|
|
681
|
+
messageEl.textContent = text;
|
|
682
|
+
messageEl.className = type;
|
|
683
|
+
setTimeout(() => {
|
|
684
|
+
messageEl.textContent = '';
|
|
685
|
+
messageEl.className = '';
|
|
686
|
+
}, 3000);
|
|
687
|
+
}
|
|
688
|
+
</script>
|
|
689
|
+
</body>
|
|
690
|
+
</html>
|
|
691
|
+
`);
|
|
692
|
+
});
|
|
693
|
+
const server = app.listen(cfg.web.port, cfg.web.host, () => {
|
|
694
|
+
console.log(chalk.cyan("\n\u{1F4CA} OpenQA Status:"));
|
|
695
|
+
console.log(chalk.white(` Agent: ${cfg.agent.autoStart ? "Auto-start enabled" : "Idle"}`));
|
|
696
|
+
console.log(chalk.white(` Target: ${cfg.saas.url || "Not configured"}`));
|
|
697
|
+
console.log(chalk.white(` Dashboard: http://localhost:${cfg.web.port}`));
|
|
698
|
+
console.log(chalk.white(` Kanban: http://localhost:${cfg.web.port}/kanban`));
|
|
699
|
+
console.log(chalk.white(` Config: http://localhost:${cfg.web.port}/config`));
|
|
700
|
+
console.log(chalk.gray("\nPress Ctrl+C to stop\n"));
|
|
701
|
+
if (!cfg.agent.autoStart) {
|
|
702
|
+
console.log(chalk.yellow("\u{1F4A1} Auto-start disabled. Agent is idle."));
|
|
703
|
+
console.log(chalk.cyan(" Set AGENT_AUTO_START=true to enable autonomous mode\n"));
|
|
704
|
+
}
|
|
705
|
+
});
|
|
706
|
+
server.on("upgrade", (request, socket, head) => {
|
|
707
|
+
wss.handleUpgrade(request, socket, head, (ws) => {
|
|
708
|
+
wss.emit("connection", ws, request);
|
|
1336
709
|
});
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
importSkills(json) {
|
|
1343
|
-
const imported = JSON.parse(json);
|
|
1344
|
-
let count = 0;
|
|
1345
|
-
imported.forEach((skill) => {
|
|
1346
|
-
const newSkill = this.createSkill({
|
|
1347
|
-
name: skill.name,
|
|
1348
|
-
description: skill.description,
|
|
1349
|
-
type: skill.type,
|
|
1350
|
-
enabled: skill.enabled,
|
|
1351
|
-
priority: skill.priority,
|
|
1352
|
-
prompt: skill.prompt,
|
|
1353
|
-
triggers: skill.triggers
|
|
1354
|
-
});
|
|
1355
|
-
if (newSkill) count++;
|
|
710
|
+
});
|
|
711
|
+
wss.on("connection", (ws) => {
|
|
712
|
+
console.log("WebSocket client connected");
|
|
713
|
+
ws.on("close", () => {
|
|
714
|
+
console.log("WebSocket client disconnected");
|
|
1356
715
|
});
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
var OpenQAAgent = class extends EventEmitter3 {
|
|
1363
|
-
agent = null;
|
|
1364
|
-
db;
|
|
1365
|
-
config;
|
|
1366
|
-
browserTools = null;
|
|
1367
|
-
sessionId = "";
|
|
1368
|
-
isRunning = false;
|
|
1369
|
-
intervalId = null;
|
|
1370
|
-
// New v2 features
|
|
1371
|
-
gitListener = null;
|
|
1372
|
-
specialistManager = null;
|
|
1373
|
-
skillManager;
|
|
1374
|
-
constructor(configPath) {
|
|
1375
|
-
super();
|
|
1376
|
-
this.config = new ConfigManager(configPath);
|
|
1377
|
-
this.db = new OpenQADatabase("./data/openqa.json");
|
|
1378
|
-
this.skillManager = new SkillManager(this.db);
|
|
1379
|
-
}
|
|
1380
|
-
createLLMAdapter() {
|
|
1381
|
-
const cfg = this.config.getConfigSync();
|
|
1382
|
-
switch (cfg.llm.provider) {
|
|
1383
|
-
case "anthropic":
|
|
1384
|
-
return new AnthropicAdapter2({
|
|
1385
|
-
apiKey: cfg.llm.apiKey || process.env.ANTHROPIC_API_KEY,
|
|
1386
|
-
model: cfg.llm.model || "claude-3-5-sonnet-20241022"
|
|
1387
|
-
});
|
|
1388
|
-
case "openai":
|
|
1389
|
-
default:
|
|
1390
|
-
return new OpenAIAdapter2({
|
|
1391
|
-
apiKey: cfg.llm.apiKey || process.env.OPENAI_API_KEY,
|
|
1392
|
-
model: cfg.llm.model || "gpt-4"
|
|
1393
|
-
});
|
|
1394
|
-
}
|
|
1395
|
-
}
|
|
1396
|
-
async initialize(triggerType = "manual", triggerData) {
|
|
1397
|
-
const cfg = this.config.getConfigSync();
|
|
1398
|
-
this.sessionId = `session_${Date.now()}`;
|
|
1399
|
-
await this.db.createSession(this.sessionId, {
|
|
1400
|
-
config: cfg,
|
|
1401
|
-
started_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1402
|
-
trigger_type: triggerType,
|
|
1403
|
-
trigger_data: triggerData ? JSON.stringify(triggerData) : null
|
|
716
|
+
});
|
|
717
|
+
process.on("SIGTERM", () => {
|
|
718
|
+
console.log("Received SIGTERM, shutting down gracefully...");
|
|
719
|
+
server.close(() => {
|
|
720
|
+
process.exit(0);
|
|
1404
721
|
});
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
goal: "Test the SaaS application comprehensively and identify bugs",
|
|
1420
|
-
tools: allTools,
|
|
1421
|
-
tracer,
|
|
1422
|
-
maxIterations: cfg.agent.maxIterations,
|
|
1423
|
-
systemPrompt: `You are OpenQA, an autonomous QA testing agent - intelligent and thorough like a senior QA engineer.
|
|
1424
|
-
|
|
1425
|
-
Your mission:
|
|
1426
|
-
1. **Systematically test the SaaS application** at ${cfg.saas.url}
|
|
1427
|
-
2. **Create comprehensive test flows** - think like a real user AND a security expert
|
|
1428
|
-
3. **Identify bugs and issues** - UI bugs, console errors, broken flows, UX issues, security vulnerabilities
|
|
1429
|
-
4. **Report findings appropriately**:
|
|
1430
|
-
- Use create_github_issue for critical bugs requiring developer attention
|
|
1431
|
-
- Use create_kanban_ticket for QA tracking, minor issues, or improvements
|
|
1432
|
-
- You can create BOTH for critical bugs
|
|
1433
|
-
5. **Learn from previous sessions** - avoid repeating the same tests
|
|
1434
|
-
6. **Spawn specialist agents** when needed for deep testing (security, forms, etc.)
|
|
1435
|
-
|
|
1436
|
-
Testing strategy:
|
|
1437
|
-
- Start with core user flows (signup, login, main features)
|
|
1438
|
-
- Test edge cases and error handling
|
|
1439
|
-
- Check for console errors and network issues
|
|
1440
|
-
- Test security (SQL injection, XSS, auth bypass)
|
|
1441
|
-
- Test forms thoroughly (validation, edge cases)
|
|
1442
|
-
- Take screenshots as evidence
|
|
1443
|
-
- Document steps to reproduce clearly
|
|
1444
|
-
|
|
1445
|
-
Reporting guidelines:
|
|
1446
|
-
- **Critical/High severity** \u2192 GitHub issue + Kanban ticket
|
|
1447
|
-
- **Medium severity** \u2192 Kanban ticket (optionally GitHub if it blocks users)
|
|
1448
|
-
- **Low severity/Improvements** \u2192 Kanban ticket only
|
|
1449
|
-
|
|
1450
|
-
${skillPrompt}
|
|
1451
|
-
|
|
1452
|
-
Always provide clear, actionable information with steps to reproduce. Think step by step like a human QA expert.`
|
|
1453
|
-
};
|
|
1454
|
-
this.agent = new ReActAgent2(agentConfig, llm, memory);
|
|
1455
|
-
this.specialistManager = new SpecialistAgentManager(
|
|
1456
|
-
this.db,
|
|
1457
|
-
this.sessionId,
|
|
1458
|
-
{ provider: cfg.llm.provider, apiKey: cfg.llm.apiKey || "" },
|
|
1459
|
-
this.browserTools
|
|
1460
|
-
);
|
|
1461
|
-
this.specialistManager.on("agent-created", (status) => this.emit("specialist-created", status));
|
|
1462
|
-
this.specialistManager.on("agent-started", (status) => this.emit("specialist-started", status));
|
|
1463
|
-
this.specialistManager.on("agent-completed", (data) => this.emit("specialist-completed", data));
|
|
1464
|
-
this.specialistManager.on("agent-failed", (data) => this.emit("specialist-failed", data));
|
|
1465
|
-
console.log(`\u2705 OpenQA Agent initialized (Session: ${this.sessionId})`);
|
|
1466
|
-
}
|
|
1467
|
-
async runSession() {
|
|
1468
|
-
if (!this.agent) {
|
|
1469
|
-
await this.initialize();
|
|
1470
|
-
}
|
|
1471
|
-
const cfg = this.config.getConfig();
|
|
1472
|
-
console.log(`\u{1F680} Starting test session for ${cfg.saas.url}`);
|
|
1473
|
-
try {
|
|
1474
|
-
const result = await this.agent.run(
|
|
1475
|
-
`Continue testing the application at ${cfg.saas.url}. Review previous findings, create new test scenarios, and report any issues discovered. Focus on areas not yet tested.`
|
|
1476
|
-
);
|
|
1477
|
-
this.db.updateSession(this.sessionId, {
|
|
1478
|
-
status: "completed",
|
|
1479
|
-
ended_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1480
|
-
});
|
|
1481
|
-
console.log("\u2705 Test session completed:", result);
|
|
1482
|
-
return result;
|
|
1483
|
-
} catch (error) {
|
|
1484
|
-
console.error("\u274C Session error:", error);
|
|
1485
|
-
this.db.updateSession(this.sessionId, {
|
|
1486
|
-
status: "failed",
|
|
1487
|
-
ended_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1488
|
-
});
|
|
1489
|
-
throw error;
|
|
1490
|
-
} finally {
|
|
1491
|
-
if (this.browserTools) {
|
|
1492
|
-
await this.browserTools.close();
|
|
1493
|
-
}
|
|
1494
|
-
}
|
|
1495
|
-
}
|
|
1496
|
-
async startAutonomous() {
|
|
1497
|
-
if (this.isRunning) {
|
|
1498
|
-
console.log("\u26A0\uFE0F Agent is already running");
|
|
1499
|
-
return;
|
|
1500
|
-
}
|
|
1501
|
-
this.isRunning = true;
|
|
1502
|
-
const cfg = this.config.getConfig();
|
|
1503
|
-
console.log(`\u{1F916} OpenQA Agent starting in autonomous mode`);
|
|
1504
|
-
console.log(`\u{1F4CD} Target: ${cfg.saas.url}`);
|
|
1505
|
-
console.log(`\u23F1\uFE0F Interval: ${cfg.agent.intervalMs}ms (${cfg.agent.intervalMs / 1e3 / 60} minutes)`);
|
|
1506
|
-
await this.startGitListener();
|
|
1507
|
-
const runLoop = async () => {
|
|
1508
|
-
if (!this.isRunning) return;
|
|
1509
|
-
try {
|
|
1510
|
-
await this.runSession();
|
|
1511
|
-
} catch (error) {
|
|
1512
|
-
console.error("Session failed, will retry on next interval");
|
|
1513
|
-
}
|
|
1514
|
-
if (this.isRunning) {
|
|
1515
|
-
this.sessionId = `session_${Date.now()}`;
|
|
1516
|
-
this.agent = null;
|
|
1517
|
-
this.browserTools = null;
|
|
1518
|
-
this.intervalId = setTimeout(runLoop, cfg.agent.intervalMs);
|
|
1519
|
-
}
|
|
1520
|
-
};
|
|
1521
|
-
await runLoop();
|
|
1522
|
-
}
|
|
1523
|
-
stop() {
|
|
1524
|
-
console.log("\u{1F6D1} Stopping OpenQA Agent...");
|
|
1525
|
-
this.isRunning = false;
|
|
1526
|
-
if (this.intervalId) {
|
|
1527
|
-
clearTimeout(this.intervalId);
|
|
1528
|
-
this.intervalId = null;
|
|
1529
|
-
}
|
|
1530
|
-
if (this.gitListener) {
|
|
1531
|
-
this.gitListener.stop();
|
|
1532
|
-
this.gitListener = null;
|
|
1533
|
-
}
|
|
1534
|
-
if (this.specialistManager) {
|
|
1535
|
-
this.specialistManager.stopAll();
|
|
1536
|
-
}
|
|
1537
|
-
if (this.browserTools) {
|
|
1538
|
-
this.browserTools.close();
|
|
1539
|
-
}
|
|
1540
|
-
}
|
|
1541
|
-
// Git integration
|
|
1542
|
-
async startGitListener() {
|
|
1543
|
-
const cfg = this.config.getConfig();
|
|
1544
|
-
if (cfg.github?.token && cfg.github?.owner && cfg.github?.repo) {
|
|
1545
|
-
this.gitListener = new GitListener({
|
|
1546
|
-
provider: "github",
|
|
1547
|
-
token: cfg.github.token,
|
|
1548
|
-
owner: cfg.github.owner,
|
|
1549
|
-
repo: cfg.github.repo,
|
|
1550
|
-
branch: "main",
|
|
1551
|
-
pollIntervalMs: 6e4
|
|
1552
|
-
});
|
|
1553
|
-
} else if (this.config.get("gitlab.token") && this.config.get("gitlab.project")) {
|
|
1554
|
-
const [owner, repo] = (this.config.get("gitlab.project") || "").split("/");
|
|
1555
|
-
this.gitListener = new GitListener({
|
|
1556
|
-
provider: "gitlab",
|
|
1557
|
-
token: this.config.get("gitlab.token") || "",
|
|
1558
|
-
owner,
|
|
1559
|
-
repo,
|
|
1560
|
-
branch: "main",
|
|
1561
|
-
pollIntervalMs: 6e4,
|
|
1562
|
-
gitlabUrl: this.config.get("gitlab.url") || "https://gitlab.com"
|
|
1563
|
-
});
|
|
1564
|
-
}
|
|
1565
|
-
if (this.gitListener) {
|
|
1566
|
-
this.gitListener.on("merge", async (event) => {
|
|
1567
|
-
console.log(`\u{1F500} Merge detected! Starting test session...`);
|
|
1568
|
-
this.emit("git-merge", event);
|
|
1569
|
-
this.sessionId = `session_${Date.now()}`;
|
|
1570
|
-
this.agent = null;
|
|
1571
|
-
this.browserTools = null;
|
|
1572
|
-
await this.runSession();
|
|
1573
|
-
});
|
|
1574
|
-
this.gitListener.on("pipeline-success", async (event) => {
|
|
1575
|
-
console.log(`\u2705 Pipeline success! Starting test session...`);
|
|
1576
|
-
this.emit("git-pipeline-success", event);
|
|
1577
|
-
this.sessionId = `session_${Date.now()}`;
|
|
1578
|
-
this.agent = null;
|
|
1579
|
-
this.browserTools = null;
|
|
1580
|
-
await this.runSession();
|
|
1581
|
-
});
|
|
1582
|
-
await this.gitListener.start();
|
|
1583
|
-
console.log(`\u{1F517} Git listener started for ${this.gitListener ? "repository" : "none"}`);
|
|
1584
|
-
}
|
|
1585
|
-
}
|
|
1586
|
-
// Specialist agents management
|
|
1587
|
-
async runSecurityScan() {
|
|
1588
|
-
if (!this.specialistManager) {
|
|
1589
|
-
await this.initialize();
|
|
1590
|
-
}
|
|
1591
|
-
const cfg = this.config.getConfig();
|
|
1592
|
-
await this.specialistManager.runSecuritySuite(cfg.saas.url);
|
|
1593
|
-
}
|
|
1594
|
-
async runSpecialist(type) {
|
|
1595
|
-
if (!this.specialistManager) {
|
|
1596
|
-
await this.initialize();
|
|
1597
|
-
}
|
|
1598
|
-
const cfg = this.config.getConfig();
|
|
1599
|
-
const agentId = this.specialistManager.createSpecialist(type);
|
|
1600
|
-
await this.specialistManager.runSpecialist(agentId, cfg.saas.url);
|
|
1601
|
-
}
|
|
1602
|
-
getSpecialistStatuses() {
|
|
1603
|
-
return this.specialistManager?.getAllStatuses() || [];
|
|
1604
|
-
}
|
|
1605
|
-
// Skills management
|
|
1606
|
-
getSkills() {
|
|
1607
|
-
return this.skillManager.getAllSkills();
|
|
1608
|
-
}
|
|
1609
|
-
createSkill(data) {
|
|
1610
|
-
return this.skillManager.createSkill(data);
|
|
1611
|
-
}
|
|
1612
|
-
updateSkill(id, updates) {
|
|
1613
|
-
return this.skillManager.updateSkill(id, updates);
|
|
1614
|
-
}
|
|
1615
|
-
deleteSkill(id) {
|
|
1616
|
-
return this.skillManager.deleteSkill(id);
|
|
1617
|
-
}
|
|
1618
|
-
toggleSkill(id) {
|
|
1619
|
-
return this.skillManager.toggleSkill(id);
|
|
1620
|
-
}
|
|
1621
|
-
getStatus() {
|
|
1622
|
-
return {
|
|
1623
|
-
isRunning: this.isRunning,
|
|
1624
|
-
sessionId: this.sessionId,
|
|
1625
|
-
config: this.config.getConfig(),
|
|
1626
|
-
gitListenerActive: !!this.gitListener,
|
|
1627
|
-
specialists: this.getSpecialistStatuses(),
|
|
1628
|
-
skills: this.skillManager.getEnabledSkills().length
|
|
1629
|
-
};
|
|
722
|
+
});
|
|
723
|
+
process.on("SIGINT", () => {
|
|
724
|
+
console.log("\nShutting down gracefully...");
|
|
725
|
+
server.close(() => {
|
|
726
|
+
process.exit(0);
|
|
727
|
+
});
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
var init_server = __esm({
|
|
731
|
+
"cli/server.ts"() {
|
|
732
|
+
"use strict";
|
|
733
|
+
init_esm_shims();
|
|
734
|
+
init_config();
|
|
735
|
+
init_database();
|
|
1630
736
|
}
|
|
1631
|
-
};
|
|
737
|
+
});
|
|
1632
738
|
|
|
1633
739
|
// cli/index.ts
|
|
740
|
+
init_esm_shims();
|
|
741
|
+
init_config();
|
|
742
|
+
init_database();
|
|
743
|
+
import { Command } from "commander";
|
|
1634
744
|
import { spawn } from "child_process";
|
|
1635
745
|
import { writeFileSync as writeFileSync2, readFileSync as readFileSync2, existsSync, unlinkSync } from "fs";
|
|
1636
|
-
import { join as
|
|
1637
|
-
import
|
|
746
|
+
import { join as join2 } from "path";
|
|
747
|
+
import chalk2 from "chalk";
|
|
1638
748
|
import ora from "ora";
|
|
1639
749
|
var program = new Command();
|
|
1640
750
|
var PID_FILE = "./data/openqa.pid";
|
|
@@ -1646,46 +756,31 @@ program.command("start").description("Start the OpenQA agent and web UI").option
|
|
|
1646
756
|
const pid = readFileSync2(PID_FILE, "utf-8").trim();
|
|
1647
757
|
try {
|
|
1648
758
|
process.kill(parseInt(pid), 0);
|
|
1649
|
-
spinner.fail(
|
|
1650
|
-
console.log(
|
|
1651
|
-
console.log(
|
|
759
|
+
spinner.fail(chalk2.red("OpenQA is already running"));
|
|
760
|
+
console.log(chalk2.yellow(`PID: ${pid}`));
|
|
761
|
+
console.log(chalk2.cyan('Run "openqa stop" to stop it first'));
|
|
1652
762
|
process.exit(1);
|
|
1653
763
|
} catch {
|
|
1654
764
|
unlinkSync(PID_FILE);
|
|
1655
765
|
}
|
|
1656
766
|
}
|
|
1657
767
|
if (options.daemon) {
|
|
1658
|
-
const child = spawn("node", [
|
|
768
|
+
const child = spawn("node", [join2(process.cwd(), "dist/cli/daemon.js")], {
|
|
1659
769
|
detached: true,
|
|
1660
770
|
stdio: "ignore"
|
|
1661
771
|
});
|
|
1662
772
|
child.unref();
|
|
1663
773
|
writeFileSync2(PID_FILE, child.pid.toString());
|
|
1664
|
-
spinner.succeed(
|
|
1665
|
-
console.log(
|
|
774
|
+
spinner.succeed(chalk2.green("OpenQA started in daemon mode"));
|
|
775
|
+
console.log(chalk2.cyan(`PID: ${child.pid}`));
|
|
1666
776
|
} else {
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
const cfg = config.getConfigSync();
|
|
1671
|
-
console.log(chalk.cyan("\n\u{1F4CA} OpenQA Status:"));
|
|
1672
|
-
console.log(chalk.white(` Agent: Running`));
|
|
1673
|
-
console.log(chalk.white(` Target: ${cfg.saas.url || "Not configured"}`));
|
|
1674
|
-
console.log(chalk.white(` Web UI: http://localhost:${cfg.web.port}`));
|
|
1675
|
-
console.log(chalk.white(` DevTools: http://localhost:${cfg.web.port}`));
|
|
1676
|
-
console.log(chalk.white(` Kanban: http://localhost:${cfg.web.port}/kanban`));
|
|
1677
|
-
console.log(chalk.white(` Config: http://localhost:${cfg.web.port}/config`));
|
|
1678
|
-
console.log(chalk.gray("\nPress Ctrl+C to stop\n"));
|
|
1679
|
-
if (cfg.agent.autoStart) {
|
|
1680
|
-
await agent.startAutonomous();
|
|
1681
|
-
} else {
|
|
1682
|
-
console.log(chalk.yellow("Auto-start disabled. Agent is idle."));
|
|
1683
|
-
console.log(chalk.cyan("Set AGENT_AUTO_START=true to enable autonomous mode"));
|
|
1684
|
-
}
|
|
777
|
+
const { startWebServer: startWebServer2 } = await Promise.resolve().then(() => (init_server(), server_exports));
|
|
778
|
+
await startWebServer2();
|
|
779
|
+
spinner.succeed(chalk2.green("OpenQA started"));
|
|
1685
780
|
}
|
|
1686
781
|
} catch (error) {
|
|
1687
|
-
spinner.fail(
|
|
1688
|
-
console.error(
|
|
782
|
+
spinner.fail(chalk2.red("Failed to start OpenQA"));
|
|
783
|
+
console.error(chalk2.red(error.message));
|
|
1689
784
|
process.exit(1);
|
|
1690
785
|
}
|
|
1691
786
|
});
|
|
@@ -1693,78 +788,81 @@ program.command("stop").description("Stop the OpenQA agent").action(() => {
|
|
|
1693
788
|
const spinner = ora("Stopping OpenQA...").start();
|
|
1694
789
|
try {
|
|
1695
790
|
if (!existsSync(PID_FILE)) {
|
|
1696
|
-
spinner.fail(
|
|
791
|
+
spinner.fail(chalk2.red("No OpenQA daemon process found"));
|
|
792
|
+
console.log(chalk2.yellow("Note: OpenQA might be running in foreground mode"));
|
|
793
|
+
console.log(chalk2.cyan("Use Ctrl+C to stop foreground processes"));
|
|
1697
794
|
process.exit(1);
|
|
1698
795
|
}
|
|
1699
796
|
const pid = readFileSync2(PID_FILE, "utf-8").trim();
|
|
1700
797
|
try {
|
|
1701
798
|
process.kill(parseInt(pid), "SIGTERM");
|
|
1702
799
|
unlinkSync(PID_FILE);
|
|
1703
|
-
spinner.succeed(
|
|
800
|
+
spinner.succeed(chalk2.green("OpenQA daemon stopped"));
|
|
1704
801
|
} catch (error) {
|
|
1705
|
-
spinner.fail(
|
|
1706
|
-
console.error(
|
|
802
|
+
spinner.fail(chalk2.red("Failed to stop OpenQA"));
|
|
803
|
+
console.error(chalk2.red("Process not found. Cleaning up stale PID file..."));
|
|
1707
804
|
unlinkSync(PID_FILE);
|
|
805
|
+
console.log(chalk2.yellow("The daemon process was already stopped"));
|
|
1708
806
|
}
|
|
1709
807
|
} catch (error) {
|
|
1710
|
-
spinner.fail(
|
|
1711
|
-
console.error(
|
|
808
|
+
spinner.fail(chalk2.red("Failed to stop OpenQA"));
|
|
809
|
+
console.error(chalk2.red(error.message));
|
|
1712
810
|
process.exit(1);
|
|
1713
811
|
}
|
|
1714
812
|
});
|
|
1715
813
|
program.command("status").description("Show OpenQA status").action(() => {
|
|
1716
|
-
console.log(
|
|
814
|
+
console.log(chalk2.cyan.bold("\n\u{1F4CA} OpenQA Status\n"));
|
|
1717
815
|
if (existsSync(PID_FILE)) {
|
|
1718
816
|
const pid = readFileSync2(PID_FILE, "utf-8").trim();
|
|
1719
817
|
try {
|
|
1720
818
|
process.kill(parseInt(pid), 0);
|
|
1721
|
-
console.log(
|
|
1722
|
-
console.log(
|
|
1723
|
-
console.log(
|
|
819
|
+
console.log(chalk2.green("\u2713 Agent: Running"));
|
|
820
|
+
console.log(chalk2.white(` PID: ${pid}`));
|
|
821
|
+
console.log(chalk2.yellow("\n\u{1F4CB} Detailed status temporarily disabled"));
|
|
1724
822
|
} catch {
|
|
1725
|
-
console.log(
|
|
823
|
+
console.log(chalk2.red("\u2717 Agent: Not running (stale PID file)"));
|
|
1726
824
|
unlinkSync(PID_FILE);
|
|
1727
825
|
}
|
|
1728
826
|
} else {
|
|
1729
|
-
console.log(
|
|
827
|
+
console.log(chalk2.yellow("\u2717 Agent: Not running"));
|
|
1730
828
|
}
|
|
1731
829
|
});
|
|
1732
830
|
program.command("config").description("Manage configuration").argument("[action]", "Action: get, set, list").argument("[key]", "Configuration key (e.g., llm.provider)").argument("[value]", "Configuration value").action((action, key, value) => {
|
|
1733
831
|
const config = new ConfigManager();
|
|
1734
832
|
if (!action || action === "list") {
|
|
1735
833
|
const cfg = config.getConfigSync();
|
|
1736
|
-
console.log(
|
|
834
|
+
console.log(chalk2.cyan.bold("\n\u2699\uFE0F OpenQA Configuration\n"));
|
|
1737
835
|
console.log(JSON.stringify(cfg, null, 2));
|
|
1738
836
|
console.log("");
|
|
1739
837
|
return;
|
|
1740
838
|
}
|
|
1741
839
|
if (action === "get") {
|
|
1742
840
|
if (!key) {
|
|
1743
|
-
console.error(
|
|
841
|
+
console.error(chalk2.red('Error: key is required for "get" action'));
|
|
1744
842
|
process.exit(1);
|
|
1745
843
|
}
|
|
1746
844
|
const val = config.get(key);
|
|
1747
|
-
console.log(
|
|
845
|
+
console.log(chalk2.cyan(`${key}:`), chalk2.white(val || "Not set"));
|
|
1748
846
|
return;
|
|
1749
847
|
}
|
|
1750
848
|
if (action === "set") {
|
|
1751
849
|
if (!key || !value) {
|
|
1752
|
-
console.error(
|
|
850
|
+
console.error(chalk2.red('Error: key and value are required for "set" action'));
|
|
1753
851
|
process.exit(1);
|
|
1754
852
|
}
|
|
1755
853
|
config.set(key, value);
|
|
1756
|
-
console.log(
|
|
854
|
+
console.log(chalk2.green(`\u2713 Set ${key} = ${value}`));
|
|
1757
855
|
return;
|
|
1758
856
|
}
|
|
1759
|
-
console.error(
|
|
1760
|
-
console.log(
|
|
857
|
+
console.error(chalk2.red(`Unknown action: ${action}`));
|
|
858
|
+
console.log(chalk2.cyan("Available actions: get, set, list"));
|
|
1761
859
|
process.exit(1);
|
|
1762
860
|
});
|
|
1763
861
|
program.command("logs").description("Show agent logs").option("-f, --follow", "Follow log output").option("-n, --lines <number>", "Number of lines to show", "50").action((options) => {
|
|
1764
862
|
const config = new ConfigManager();
|
|
1765
863
|
const cfg = config.getConfigSync();
|
|
1766
864
|
const db = new OpenQADatabase(cfg.database.path);
|
|
1767
|
-
console.log(
|
|
865
|
+
console.log(chalk2.yellow("Logs feature temporarily disabled"));
|
|
1768
866
|
return;
|
|
1769
867
|
});
|
|
1770
868
|
program.parse();
|