@openqa/cli 1.3.3 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +1 -1
  2. package/dist/agent/brain/diff-analyzer.js +140 -0
  3. package/dist/agent/brain/diff-analyzer.js.map +1 -0
  4. package/dist/agent/brain/llm-cache.js +47 -0
  5. package/dist/agent/brain/llm-cache.js.map +1 -0
  6. package/dist/agent/brain/llm-resilience.js +252 -0
  7. package/dist/agent/brain/llm-resilience.js.map +1 -0
  8. package/dist/agent/config/index.js +588 -0
  9. package/dist/agent/config/index.js.map +1 -0
  10. package/dist/agent/coverage/index.js +74 -0
  11. package/dist/agent/coverage/index.js.map +1 -0
  12. package/dist/agent/export/index.js +158 -0
  13. package/dist/agent/export/index.js.map +1 -0
  14. package/dist/agent/index-v2.js +2795 -0
  15. package/dist/agent/index-v2.js.map +1 -0
  16. package/dist/agent/index.js +387 -55
  17. package/dist/agent/index.js.map +1 -1
  18. package/dist/agent/logger.js +41 -0
  19. package/dist/agent/logger.js.map +1 -0
  20. package/dist/agent/metrics.js +39 -0
  21. package/dist/agent/metrics.js.map +1 -0
  22. package/dist/agent/notifications/index.js +106 -0
  23. package/dist/agent/notifications/index.js.map +1 -0
  24. package/dist/agent/openapi/spec.js +338 -0
  25. package/dist/agent/openapi/spec.js.map +1 -0
  26. package/dist/agent/tools/project-runner.js +481 -0
  27. package/dist/agent/tools/project-runner.js.map +1 -0
  28. package/dist/cli/config.html.js +454 -0
  29. package/dist/cli/daemon.js +7572 -0
  30. package/dist/cli/dashboard.html.js +1619 -0
  31. package/dist/cli/index.js +3624 -1675
  32. package/dist/cli/kanban.html.js +577 -0
  33. package/dist/cli/routes.js +895 -0
  34. package/dist/cli/routes.js.map +1 -0
  35. package/dist/cli/server.js +3564 -1646
  36. package/dist/database/index.js +503 -10
  37. package/dist/database/index.js.map +1 -1
  38. package/dist/database/sqlite.js +281 -0
  39. package/dist/database/sqlite.js.map +1 -0
  40. package/package.json +18 -5
@@ -0,0 +1,2795 @@
1
+ var __getOwnPropNames = Object.getOwnPropertyNames;
2
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
3
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
4
+ }) : x)(function(x) {
5
+ if (typeof require !== "undefined") return require.apply(this, arguments);
6
+ throw Error('Dynamic require of "' + x + '" is not supported');
7
+ });
8
+ var __esm = (fn, res) => function __init() {
9
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
10
+ };
11
+
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
+ });
20
+
21
+ // database/sqlite.ts
22
+ import Database from "better-sqlite3";
23
+ var init_sqlite = __esm({
24
+ "database/sqlite.ts"() {
25
+ "use strict";
26
+ init_esm_shims();
27
+ }
28
+ });
29
+
30
+ // agent/index-v2.ts
31
+ init_esm_shims();
32
+ import { EventEmitter as EventEmitter5 } from "events";
33
+
34
+ // database/index.ts
35
+ init_esm_shims();
36
+ init_sqlite();
37
+ import { Low } from "lowdb";
38
+ import { JSONFile } from "lowdb/node";
39
+ import { dirname } from "path";
40
+ import { fileURLToPath as fileURLToPath2 } from "url";
41
+ import { mkdirSync } from "fs";
42
+ var __filename2 = fileURLToPath2(import.meta.url);
43
+ var __dirname2 = dirname(__filename2);
44
+ var OpenQADatabase = class {
45
+ constructor(dbPath = "./data/openqa.json") {
46
+ this.dbPath = dbPath;
47
+ this.initialize();
48
+ }
49
+ db = null;
50
+ initialize() {
51
+ const dir = dirname(this.dbPath);
52
+ mkdirSync(dir, { recursive: true });
53
+ const adapter = new JSONFile(this.dbPath);
54
+ this.db = new Low(adapter, {
55
+ config: {},
56
+ test_sessions: [],
57
+ actions: [],
58
+ bugs: [],
59
+ kanban_tickets: [],
60
+ users: []
61
+ });
62
+ this.db.read();
63
+ if (!this.db.data) {
64
+ this.db.data = {
65
+ config: {},
66
+ test_sessions: [],
67
+ actions: [],
68
+ bugs: [],
69
+ kanban_tickets: [],
70
+ users: []
71
+ };
72
+ this.db.write();
73
+ }
74
+ }
75
+ async ensureInitialized() {
76
+ if (!this.db) {
77
+ this.initialize();
78
+ }
79
+ await this.db.read();
80
+ let migrated = false;
81
+ if (!this.db.data.users) {
82
+ this.db.data.users = [];
83
+ migrated = true;
84
+ }
85
+ if (migrated) await this.db.write();
86
+ }
87
+ async getConfig(key) {
88
+ await this.ensureInitialized();
89
+ return this.db.data.config[key] || null;
90
+ }
91
+ async setConfig(key, value) {
92
+ await this.ensureInitialized();
93
+ this.db.data.config[key] = value;
94
+ await this.db.write();
95
+ }
96
+ async getAllConfig() {
97
+ await this.ensureInitialized();
98
+ return this.db.data.config;
99
+ }
100
+ async createSession(id, metadata) {
101
+ await this.ensureInitialized();
102
+ const session = {
103
+ id,
104
+ started_at: (/* @__PURE__ */ new Date()).toISOString(),
105
+ status: "running",
106
+ total_actions: 0,
107
+ bugs_found: 0,
108
+ metadata: metadata ? JSON.stringify(metadata) : void 0
109
+ };
110
+ this.db.data.test_sessions.push(session);
111
+ await this.db.write();
112
+ return session;
113
+ }
114
+ async getSession(id) {
115
+ await this.ensureInitialized();
116
+ return this.db.data.test_sessions.find((s) => s.id === id) || null;
117
+ }
118
+ async updateSession(id, updates) {
119
+ await this.ensureInitialized();
120
+ const index = this.db.data.test_sessions.findIndex((s) => s.id === id);
121
+ if (index !== -1) {
122
+ this.db.data.test_sessions[index] = { ...this.db.data.test_sessions[index], ...updates };
123
+ await this.db.write();
124
+ }
125
+ }
126
+ async getRecentSessions(limit = 10) {
127
+ await this.ensureInitialized();
128
+ return this.db.data.test_sessions.sort((a, b) => new Date(b.started_at).getTime() - new Date(a.started_at).getTime()).slice(0, limit);
129
+ }
130
+ async createAction(action) {
131
+ await this.ensureInitialized();
132
+ const newAction = {
133
+ id: `action_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
134
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
135
+ ...action
136
+ };
137
+ this.db.data.actions.push(newAction);
138
+ await this.db.write();
139
+ return newAction;
140
+ }
141
+ async getSessionActions(sessionId) {
142
+ await this.ensureInitialized();
143
+ return this.db.data.actions.filter((a) => a.session_id === sessionId).sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
144
+ }
145
+ async createBug(bug) {
146
+ await this.ensureInitialized();
147
+ const newBug = {
148
+ id: `bug_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
149
+ created_at: (/* @__PURE__ */ new Date()).toISOString(),
150
+ updated_at: (/* @__PURE__ */ new Date()).toISOString(),
151
+ ...bug
152
+ };
153
+ this.db.data.bugs.push(newBug);
154
+ await this.db.write();
155
+ return newBug;
156
+ }
157
+ async updateBug(id, updates) {
158
+ await this.ensureInitialized();
159
+ const index = this.db.data.bugs.findIndex((b) => b.id === id);
160
+ if (index !== -1) {
161
+ this.db.data.bugs[index] = {
162
+ ...this.db.data.bugs[index],
163
+ ...updates,
164
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
165
+ };
166
+ await this.db.write();
167
+ }
168
+ }
169
+ async getAllBugs() {
170
+ await this.ensureInitialized();
171
+ return this.db.data.bugs.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
172
+ }
173
+ async getBugsByStatus(status) {
174
+ await this.ensureInitialized();
175
+ 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());
176
+ }
177
+ async createKanbanTicket(ticket) {
178
+ await this.ensureInitialized();
179
+ const newTicket = {
180
+ id: `ticket_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
181
+ created_at: (/* @__PURE__ */ new Date()).toISOString(),
182
+ updated_at: (/* @__PURE__ */ new Date()).toISOString(),
183
+ ...ticket
184
+ };
185
+ this.db.data.kanban_tickets.push(newTicket);
186
+ await this.db.write();
187
+ return newTicket;
188
+ }
189
+ async updateKanbanTicket(id, updates) {
190
+ await this.ensureInitialized();
191
+ const index = this.db.data.kanban_tickets.findIndex((t) => t.id === id);
192
+ if (index !== -1) {
193
+ this.db.data.kanban_tickets[index] = {
194
+ ...this.db.data.kanban_tickets[index],
195
+ ...updates,
196
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
197
+ };
198
+ await this.db.write();
199
+ }
200
+ }
201
+ async getKanbanTickets() {
202
+ await this.ensureInitialized();
203
+ return this.db.data.kanban_tickets.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
204
+ }
205
+ async getKanbanTicketsByColumn(column) {
206
+ await this.ensureInitialized();
207
+ 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());
208
+ }
209
+ async deleteKanbanTicket(id) {
210
+ await this.ensureInitialized();
211
+ const index = this.db.data.kanban_tickets.findIndex((t) => t.id === id);
212
+ if (index !== -1) {
213
+ this.db.data.kanban_tickets.splice(index, 1);
214
+ await this.db.write();
215
+ }
216
+ }
217
+ async clearAllConfig() {
218
+ await this.ensureInitialized();
219
+ this.db.data.config = {};
220
+ await this.db.write();
221
+ }
222
+ // Get real data methods - connected to actual database records
223
+ async getActiveAgents() {
224
+ await this.ensureInitialized();
225
+ const sessions = await this.getRecentSessions(1);
226
+ const currentSession = sessions[0];
227
+ const isRunning = currentSession?.status === "running";
228
+ const totalActions = currentSession?.total_actions || 0;
229
+ const agents = [
230
+ {
231
+ name: "Main Agent",
232
+ status: isRunning ? "running" : "idle",
233
+ purpose: "Autonomous QA orchestration",
234
+ performance: totalActions > 0 ? Math.min(100, Math.round(totalActions / 100 * 100)) : 0,
235
+ tasks: totalActions
236
+ }
237
+ ];
238
+ if (currentSession && totalActions > 0) {
239
+ const actions = await this.getSessionActions(currentSession.id);
240
+ const actionTypes = actions.reduce((acc, action) => {
241
+ const type = action.type || "unknown";
242
+ acc[type] = (acc[type] || 0) + 1;
243
+ return acc;
244
+ }, {});
245
+ if (actionTypes["navigate"] || actionTypes["click"] || actionTypes["screenshot"]) {
246
+ agents.push({
247
+ name: "Browser Specialist",
248
+ status: isRunning ? "running" : "idle",
249
+ purpose: "UI navigation and interaction",
250
+ performance: Math.round(((actionTypes["navigate"] || 0) + (actionTypes["click"] || 0)) / totalActions * 100),
251
+ tasks: (actionTypes["navigate"] || 0) + (actionTypes["click"] || 0)
252
+ });
253
+ }
254
+ if (actionTypes["api_call"] || actionTypes["request"]) {
255
+ agents.push({
256
+ name: "API Tester",
257
+ status: isRunning ? "running" : "idle",
258
+ purpose: "API endpoint testing",
259
+ performance: Math.round((actionTypes["api_call"] || actionTypes["request"] || 0) / totalActions * 100),
260
+ tasks: actionTypes["api_call"] || actionTypes["request"] || 0
261
+ });
262
+ }
263
+ if (actionTypes["auth"] || actionTypes["login"]) {
264
+ agents.push({
265
+ name: "Auth Specialist",
266
+ status: isRunning ? "running" : "idle",
267
+ purpose: "Authentication testing",
268
+ performance: Math.round((actionTypes["auth"] || actionTypes["login"] || 0) / totalActions * 100),
269
+ tasks: actionTypes["auth"] || actionTypes["login"] || 0
270
+ });
271
+ }
272
+ }
273
+ return agents;
274
+ }
275
+ async getCurrentTasks() {
276
+ await this.ensureInitialized();
277
+ const sessions = await this.getRecentSessions(1);
278
+ const currentSession = sessions[0];
279
+ if (!currentSession) {
280
+ return [];
281
+ }
282
+ const actions = await this.getSessionActions(currentSession.id);
283
+ const recentActions = actions.slice(-10).reverse();
284
+ return recentActions.map((action, index) => ({
285
+ id: action.id,
286
+ name: action.type || "Unknown Action",
287
+ status: index === 0 && currentSession.status === "running" ? "running" : "completed",
288
+ progress: index === 0 && currentSession.status === "running" ? "65%" : "100%",
289
+ agent: "Main Agent",
290
+ started_at: action.timestamp,
291
+ result: action.output || action.description || "Completed"
292
+ }));
293
+ }
294
+ async getCurrentIssues() {
295
+ await this.ensureInitialized();
296
+ const bugs = await this.getAllBugs();
297
+ return bugs.slice(0, 10).map((bug) => ({
298
+ id: bug.id,
299
+ title: bug.title,
300
+ description: bug.description,
301
+ severity: bug.severity || "medium",
302
+ status: bug.status || "open",
303
+ discovered_at: bug.created_at,
304
+ agent: "Main Agent"
305
+ }));
306
+ }
307
+ async pruneOldSessions(maxAgeDays) {
308
+ await this.ensureInitialized();
309
+ const cutoff = new Date(Date.now() - maxAgeDays * 864e5).toISOString();
310
+ const oldSessions = this.db.data.test_sessions.filter((s) => s.started_at < cutoff);
311
+ const oldSessionIds = new Set(oldSessions.map((s) => s.id));
312
+ const actionsBefore = this.db.data.actions.length;
313
+ this.db.data.actions = this.db.data.actions.filter((a) => !oldSessionIds.has(a.session_id));
314
+ const actionsRemoved = actionsBefore - this.db.data.actions.length;
315
+ this.db.data.test_sessions = this.db.data.test_sessions.filter((s) => s.started_at >= cutoff);
316
+ await this.db.write();
317
+ return { sessionsRemoved: oldSessions.length, actionsRemoved };
318
+ }
319
+ async getStorageStats() {
320
+ await this.ensureInitialized();
321
+ return {
322
+ sessions: this.db.data.test_sessions.length,
323
+ actions: this.db.data.actions.length,
324
+ bugs: this.db.data.bugs.length,
325
+ tickets: this.db.data.kanban_tickets.length
326
+ };
327
+ }
328
+ // ── User management ──────────────────────────────────────────────────────────
329
+ async countUsers() {
330
+ await this.ensureInitialized();
331
+ return this.db.data.users.length;
332
+ }
333
+ async findUserByUsername(username) {
334
+ await this.ensureInitialized();
335
+ return this.db.data.users.find((u) => u.username === username) ?? null;
336
+ }
337
+ async getUserById(id) {
338
+ await this.ensureInitialized();
339
+ return this.db.data.users.find((u) => u.id === id) ?? null;
340
+ }
341
+ async getAllUsers() {
342
+ await this.ensureInitialized();
343
+ return [...this.db.data.users];
344
+ }
345
+ async createUser(data) {
346
+ await this.ensureInitialized();
347
+ const now = (/* @__PURE__ */ new Date()).toISOString();
348
+ const user = {
349
+ id: `user_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`,
350
+ username: data.username,
351
+ passwordHash: data.passwordHash,
352
+ role: data.role,
353
+ createdAt: now,
354
+ updatedAt: now
355
+ };
356
+ this.db.data.users.push(user);
357
+ await this.db.write();
358
+ return user;
359
+ }
360
+ async updateUser(id, updates) {
361
+ await this.ensureInitialized();
362
+ const idx = this.db.data.users.findIndex((u) => u.id === id);
363
+ if (idx !== -1) {
364
+ this.db.data.users[idx] = {
365
+ ...this.db.data.users[idx],
366
+ ...updates,
367
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
368
+ };
369
+ await this.db.write();
370
+ }
371
+ }
372
+ async deleteUser(id) {
373
+ await this.ensureInitialized();
374
+ const idx = this.db.data.users.findIndex((u) => u.id === id);
375
+ if (idx !== -1) {
376
+ this.db.data.users.splice(idx, 1);
377
+ await this.db.write();
378
+ }
379
+ }
380
+ async close() {
381
+ }
382
+ };
383
+
384
+ // agent/logger.ts
385
+ init_esm_shims();
386
+ var LEVELS = { debug: 0, info: 1, warn: 2, error: 3 };
387
+ var MIN_LEVEL = process.env.LOG_LEVEL || "info";
388
+ function shouldLog(level) {
389
+ return LEVELS[level] >= LEVELS[MIN_LEVEL];
390
+ }
391
+ function format(level, message, context) {
392
+ const entry = {
393
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
394
+ level,
395
+ msg: message,
396
+ ...context
397
+ };
398
+ return JSON.stringify(entry);
399
+ }
400
+ var logger = {
401
+ debug(message, context) {
402
+ if (shouldLog("debug")) process.stdout.write(format("debug", message, context) + "\n");
403
+ },
404
+ info(message, context) {
405
+ if (shouldLog("info")) process.stdout.write(format("info", message, context) + "\n");
406
+ },
407
+ warn(message, context) {
408
+ if (shouldLog("warn")) process.stderr.write(format("warn", message, context) + "\n");
409
+ },
410
+ error(message, context) {
411
+ if (shouldLog("error")) process.stderr.write(format("error", message, context) + "\n");
412
+ },
413
+ child(defaults) {
414
+ return {
415
+ debug: (msg, ctx) => logger.debug(msg, { ...defaults, ...ctx }),
416
+ info: (msg, ctx) => logger.info(msg, { ...defaults, ...ctx }),
417
+ warn: (msg, ctx) => logger.warn(msg, { ...defaults, ...ctx }),
418
+ error: (msg, ctx) => logger.error(msg, { ...defaults, ...ctx })
419
+ };
420
+ }
421
+ };
422
+
423
+ // agent/config/index.ts
424
+ init_esm_shims();
425
+ import { config as dotenvConfig } from "dotenv";
426
+
427
+ // agent/config/schema.ts
428
+ init_esm_shims();
429
+ import { z } from "zod";
430
+ var llmConfigSchema = z.object({
431
+ provider: z.enum(["openai", "anthropic", "ollama"]).default("openai"),
432
+ apiKey: z.string().optional(),
433
+ model: z.string().optional(),
434
+ baseUrl: z.string().url().optional()
435
+ });
436
+ var saasConfigSchema = z.object({
437
+ url: z.string().default(""),
438
+ authType: z.enum(["none", "basic", "bearer", "session"]).default("none"),
439
+ username: z.string().optional(),
440
+ password: z.string().optional()
441
+ });
442
+ var githubConfigSchema = z.object({
443
+ token: z.string().min(1, "GITHUB_TOKEN is required when GitHub is configured"),
444
+ owner: z.string().default(""),
445
+ repo: z.string().default("")
446
+ });
447
+ var agentConfigSchema = z.object({
448
+ intervalMs: z.number().int().positive().default(36e5),
449
+ maxIterations: z.number().int().positive().default(20),
450
+ autoStart: z.boolean().default(false)
451
+ });
452
+ var webConfigSchema = z.object({
453
+ port: z.number().int().min(1).max(65535).default(4242),
454
+ host: z.string().default("0.0.0.0")
455
+ });
456
+ var databaseConfigSchema = z.object({
457
+ path: z.string().default("./data/openqa.db")
458
+ });
459
+ var notificationsConfigSchema = z.object({
460
+ slack: z.string().url().optional(),
461
+ discord: z.string().url().optional()
462
+ });
463
+ var openQAConfigSchema = z.object({
464
+ llm: llmConfigSchema,
465
+ saas: saasConfigSchema,
466
+ github: githubConfigSchema.optional(),
467
+ agent: agentConfigSchema,
468
+ web: webConfigSchema,
469
+ database: databaseConfigSchema,
470
+ notifications: notificationsConfigSchema.optional()
471
+ });
472
+ var saasAppConfigSchema = z.object({
473
+ name: z.string().min(1, "SaaS application name is required"),
474
+ description: z.string().min(1, "SaaS application description is required"),
475
+ url: z.string().url("SaaS application URL must be a valid URL"),
476
+ repoUrl: z.string().url().optional(),
477
+ localPath: z.string().optional(),
478
+ techStack: z.array(z.string()).optional(),
479
+ authInfo: z.object({
480
+ type: z.enum(["none", "basic", "oauth", "session"]),
481
+ testCredentials: z.object({
482
+ username: z.string(),
483
+ password: z.string()
484
+ }).optional()
485
+ }).optional(),
486
+ directives: z.array(z.string()).optional()
487
+ });
488
+ function validateSaaSAppConfigSafe(config) {
489
+ const result = saasAppConfigSchema.safeParse(config);
490
+ if (result.success) {
491
+ return { success: true, data: result.data };
492
+ }
493
+ return {
494
+ success: false,
495
+ errors: result.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`)
496
+ };
497
+ }
498
+ function validateConfigSafe(config) {
499
+ const result = openQAConfigSchema.safeParse(config);
500
+ if (result.success) {
501
+ return { success: true, data: result.data };
502
+ }
503
+ return {
504
+ success: false,
505
+ errors: result.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`)
506
+ };
507
+ }
508
+
509
+ // agent/config/index.ts
510
+ dotenvConfig();
511
+ var ConfigManager = class {
512
+ db = null;
513
+ envConfig;
514
+ constructor(dbPath) {
515
+ this.envConfig = this.loadFromEnv();
516
+ }
517
+ loadFromEnv() {
518
+ const raw = {
519
+ llm: {
520
+ provider: process.env.LLM_PROVIDER || "openai",
521
+ apiKey: process.env.OPENAI_API_KEY || process.env.ANTHROPIC_API_KEY,
522
+ model: process.env.LLM_MODEL,
523
+ baseUrl: process.env.OLLAMA_BASE_URL
524
+ },
525
+ saas: {
526
+ url: process.env.SAAS_URL || "",
527
+ authType: process.env.SAAS_AUTH_TYPE || "none",
528
+ username: process.env.SAAS_USERNAME,
529
+ password: process.env.SAAS_PASSWORD
530
+ },
531
+ github: process.env.GITHUB_TOKEN ? {
532
+ token: process.env.GITHUB_TOKEN,
533
+ owner: process.env.GITHUB_OWNER || "",
534
+ repo: process.env.GITHUB_REPO || ""
535
+ } : void 0,
536
+ agent: {
537
+ intervalMs: parseInt(process.env.AGENT_INTERVAL_MS || "3600000"),
538
+ maxIterations: parseInt(process.env.AGENT_MAX_ITERATIONS || "20"),
539
+ autoStart: process.env.AGENT_AUTO_START === "true"
540
+ },
541
+ web: {
542
+ port: parseInt(process.env.WEB_PORT || "4242"),
543
+ host: process.env.WEB_HOST || "0.0.0.0"
544
+ },
545
+ database: {
546
+ path: process.env.DB_PATH || "./data/openqa.db"
547
+ },
548
+ notifications: {
549
+ slack: process.env.SLACK_WEBHOOK_URL,
550
+ discord: process.env.DISCORD_WEBHOOK_URL
551
+ }
552
+ };
553
+ const result = validateConfigSafe(raw);
554
+ if (!result.success) {
555
+ logger.warn("Config validation warnings", { errors: result.errors });
556
+ return raw;
557
+ }
558
+ return result.data;
559
+ }
560
+ getDB() {
561
+ if (!this.db) {
562
+ this.db = new OpenQADatabase("./data/openqa.json");
563
+ }
564
+ return this.db;
565
+ }
566
+ async get(key) {
567
+ const dbValue = await this.getDB().getConfig(key);
568
+ if (dbValue) return dbValue;
569
+ const keys = key.split(".");
570
+ let value = this.envConfig;
571
+ for (const k of keys) {
572
+ if (value && typeof value === "object") {
573
+ value = value[k];
574
+ } else {
575
+ return null;
576
+ }
577
+ }
578
+ return value != null ? String(value) : null;
579
+ }
580
+ async set(key, value) {
581
+ await this.getDB().setConfig(key, value);
582
+ }
583
+ async getAll() {
584
+ const dbConfig = await this.getDB().getAllConfig();
585
+ const merged = { ...this.envConfig };
586
+ for (const [key, value] of Object.entries(dbConfig)) {
587
+ const keys = key.split(".");
588
+ let obj = merged;
589
+ for (let i = 0; i < keys.length - 1; i++) {
590
+ if (!obj[keys[i]] || typeof obj[keys[i]] !== "object") obj[keys[i]] = {};
591
+ obj = obj[keys[i]];
592
+ }
593
+ obj[keys[keys.length - 1]] = value;
594
+ }
595
+ return merged;
596
+ }
597
+ async getConfig() {
598
+ return await this.getAll();
599
+ }
600
+ // Synchronous version that only uses env vars (no DB)
601
+ getConfigSync() {
602
+ return this.envConfig;
603
+ }
604
+ };
605
+
606
+ // agent/config/saas-config.ts
607
+ init_esm_shims();
608
+ import { readFileSync as readFileSync2, existsSync } from "fs";
609
+ import { join as join2 } from "path";
610
+
611
+ // agent/errors.ts
612
+ init_esm_shims();
613
+ var OpenQAError = class extends Error {
614
+ code;
615
+ constructor(message, code) {
616
+ super(message);
617
+ this.name = "OpenQAError";
618
+ this.code = code;
619
+ }
620
+ };
621
+ var ConfigError = class extends OpenQAError {
622
+ constructor(message, code = "CONFIG_ERROR") {
623
+ super(message, code);
624
+ this.name = "ConfigError";
625
+ }
626
+ };
627
+ var BrainError = class extends OpenQAError {
628
+ constructor(message, code = "BRAIN_ERROR") {
629
+ super(message, code);
630
+ this.name = "BrainError";
631
+ }
632
+ };
633
+ var ProjectRunnerError = class extends OpenQAError {
634
+ constructor(message, code = "PROJECT_RUNNER_ERROR") {
635
+ super(message, code);
636
+ this.name = "ProjectRunnerError";
637
+ }
638
+ };
639
+
640
+ // agent/config/saas-config.ts
641
+ var SaaSConfigManager = class {
642
+ db;
643
+ config = null;
644
+ constructor(db) {
645
+ this.db = db;
646
+ this.loadConfig();
647
+ }
648
+ loadConfig() {
649
+ }
650
+ saveConfig() {
651
+ }
652
+ configure(config) {
653
+ const result = validateSaaSAppConfigSafe(config);
654
+ if (!result.success) {
655
+ throw new ConfigError("Invalid SaaS configuration: " + result.errors.join(", "));
656
+ }
657
+ this.config = {
658
+ ...config,
659
+ techStack: config.techStack || this.detectTechStack(config.localPath),
660
+ directives: config.directives || []
661
+ };
662
+ this.saveConfig();
663
+ return this.config;
664
+ }
665
+ detectTechStack(localPath) {
666
+ if (!localPath || !existsSync(localPath)) return [];
667
+ const stack = [];
668
+ try {
669
+ const pkgPath = join2(localPath, "package.json");
670
+ if (existsSync(pkgPath)) {
671
+ const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
672
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
673
+ if (deps["react"]) stack.push("React");
674
+ if (deps["vue"]) stack.push("Vue");
675
+ if (deps["svelte"]) stack.push("Svelte");
676
+ if (deps["angular"]) stack.push("Angular");
677
+ if (deps["next"]) stack.push("Next.js");
678
+ if (deps["nuxt"]) stack.push("Nuxt");
679
+ if (deps["express"]) stack.push("Express");
680
+ if (deps["fastify"]) stack.push("Fastify");
681
+ if (deps["nestjs"] || deps["@nestjs/core"]) stack.push("NestJS");
682
+ if (deps["prisma"] || deps["@prisma/client"]) stack.push("Prisma");
683
+ if (deps["mongoose"]) stack.push("MongoDB");
684
+ if (deps["pg"] || deps["postgres"]) stack.push("PostgreSQL");
685
+ if (deps["mysql"] || deps["mysql2"]) stack.push("MySQL");
686
+ if (deps["redis"] || deps["ioredis"]) stack.push("Redis");
687
+ if (deps["typescript"]) stack.push("TypeScript");
688
+ if (deps["tailwindcss"]) stack.push("TailwindCSS");
689
+ }
690
+ if (existsSync(join2(localPath, "requirements.txt")) || existsSync(join2(localPath, "pyproject.toml"))) {
691
+ stack.push("Python");
692
+ }
693
+ if (existsSync(join2(localPath, "go.mod"))) {
694
+ stack.push("Go");
695
+ }
696
+ if (existsSync(join2(localPath, "Cargo.toml"))) {
697
+ stack.push("Rust");
698
+ }
699
+ } catch (e) {
700
+ }
701
+ return stack;
702
+ }
703
+ getConfig() {
704
+ return this.config;
705
+ }
706
+ addDirective(directive) {
707
+ if (!this.config) return;
708
+ if (!this.config.directives) this.config.directives = [];
709
+ this.config.directives.push(directive);
710
+ this.saveConfig();
711
+ }
712
+ removeDirective(index) {
713
+ if (!this.config?.directives) return;
714
+ this.config.directives.splice(index, 1);
715
+ this.saveConfig();
716
+ }
717
+ updateDirectives(directives) {
718
+ if (!this.config) return;
719
+ this.config.directives = directives;
720
+ this.saveConfig();
721
+ }
722
+ setRepoUrl(url) {
723
+ if (!this.config) return;
724
+ this.config.repoUrl = url;
725
+ this.saveConfig();
726
+ }
727
+ setLocalPath(path2) {
728
+ if (!this.config) return;
729
+ this.config.localPath = path2;
730
+ this.config.techStack = this.detectTechStack(path2);
731
+ this.saveConfig();
732
+ }
733
+ setAuthInfo(authInfo) {
734
+ if (!this.config) return;
735
+ this.config.authInfo = authInfo;
736
+ this.saveConfig();
737
+ }
738
+ isConfigured() {
739
+ return this.config !== null && !!this.config.name && !!this.config.url;
740
+ }
741
+ exportConfig() {
742
+ return JSON.stringify(this.config, null, 2);
743
+ }
744
+ importConfig(json) {
745
+ const config = JSON.parse(json);
746
+ return this.configure(config);
747
+ }
748
+ };
749
+ var DEFAULT_DIRECTIVES = [
750
+ "Test all forms for validation errors and edge cases",
751
+ "Check for security vulnerabilities (XSS, SQL injection, CSRF)",
752
+ "Verify authentication and authorization work correctly",
753
+ "Test responsive design on mobile viewports",
754
+ "Check for accessibility issues (WCAG compliance)",
755
+ "Monitor console for JavaScript errors",
756
+ "Test error handling and user feedback",
757
+ "Verify all links work and navigation is correct"
758
+ ];
759
+ function createQuickConfig(name, description, url, options) {
760
+ return {
761
+ name,
762
+ description,
763
+ url,
764
+ repoUrl: options?.repoUrl,
765
+ localPath: options?.localPath,
766
+ directives: options?.directives || DEFAULT_DIRECTIVES,
767
+ authInfo: { type: "none" }
768
+ };
769
+ }
770
+
771
+ // agent/brain/index.ts
772
+ init_esm_shims();
773
+ import { EventEmitter as EventEmitter2 } from "events";
774
+ import { writeFileSync as writeFileSync2, readFileSync as readFileSync3, existsSync as existsSync2, mkdirSync as mkdirSync2 } from "fs";
775
+ import { join as join3 } from "path";
776
+ import { execSync } from "child_process";
777
+
778
+ // agent/brain/llm-resilience.ts
779
+ init_esm_shims();
780
+ import { OpenAIAdapter } from "@orka-js/openai";
781
+ import { AnthropicAdapter } from "@orka-js/anthropic";
782
+ import { EventEmitter } from "events";
783
+
784
+ // agent/brain/llm-cache.ts
785
+ init_esm_shims();
786
+ import { createHash } from "crypto";
787
+ var LLMCache = class {
788
+ store = /* @__PURE__ */ new Map();
789
+ ttlMs;
790
+ maxSize;
791
+ constructor(options) {
792
+ this.ttlMs = options?.ttlMs ?? 36e5;
793
+ this.maxSize = options?.maxSize ?? 500;
794
+ }
795
+ key(prompt) {
796
+ return createHash("sha256").update(prompt).digest("hex");
797
+ }
798
+ get(prompt) {
799
+ const k = this.key(prompt);
800
+ const entry = this.store.get(k);
801
+ if (!entry) return null;
802
+ if (Date.now() > entry.expiresAt) {
803
+ this.store.delete(k);
804
+ return null;
805
+ }
806
+ return entry.response;
807
+ }
808
+ set(prompt, response) {
809
+ if (this.store.size >= this.maxSize) {
810
+ const oldest = this.store.keys().next().value;
811
+ if (oldest) this.store.delete(oldest);
812
+ }
813
+ this.store.set(this.key(prompt), {
814
+ response,
815
+ expiresAt: Date.now() + this.ttlMs
816
+ });
817
+ }
818
+ clear() {
819
+ this.store.clear();
820
+ }
821
+ get size() {
822
+ return this.store.size;
823
+ }
824
+ stats() {
825
+ return { size: this.store.size, ttlMs: this.ttlMs, maxSize: this.maxSize };
826
+ }
827
+ };
828
+
829
+ // agent/metrics.ts
830
+ init_esm_shims();
831
+ var startedAt = Date.now();
832
+ var counters = {
833
+ llm_calls: 0,
834
+ llm_cache_hits: 0,
835
+ llm_retries: 0,
836
+ llm_fallbacks: 0,
837
+ llm_circuit_opens: 0,
838
+ tests_generated: 0,
839
+ tests_run: 0,
840
+ tests_passed: 0,
841
+ tests_failed: 0,
842
+ bugs_found: 0,
843
+ sessions_started: 0,
844
+ ws_connections: 0,
845
+ http_requests: 0
846
+ };
847
+ var metrics = {
848
+ inc(key, by = 1) {
849
+ if (key in counters) counters[key] += by;
850
+ },
851
+ snapshot() {
852
+ const memMB = process.memoryUsage();
853
+ return {
854
+ uptimeSeconds: Math.floor((Date.now() - startedAt) / 1e3),
855
+ memory: {
856
+ heapUsedMB: Math.round(memMB.heapUsed / 1024 / 1024),
857
+ heapTotalMB: Math.round(memMB.heapTotal / 1024 / 1024),
858
+ rssMB: Math.round(memMB.rss / 1024 / 1024)
859
+ },
860
+ counters: { ...counters },
861
+ cacheHitRate: counters.llm_calls > 0 ? Math.round(counters.llm_cache_hits / (counters.llm_calls + counters.llm_cache_hits) * 100) : 0
862
+ };
863
+ }
864
+ };
865
+
866
+ // agent/brain/llm-resilience.ts
867
+ var ResilientLLM = class extends EventEmitter {
868
+ config;
869
+ circuit = { failures: 0, lastFailure: 0, isOpen: false };
870
+ maxRetries;
871
+ circuitThreshold;
872
+ circuitResetMs;
873
+ cache;
874
+ constructor(config) {
875
+ super();
876
+ this.config = config;
877
+ this.maxRetries = config.maxRetries ?? 3;
878
+ this.circuitThreshold = config.circuitThreshold ?? 5;
879
+ this.circuitResetMs = config.circuitResetMs ?? 3e4;
880
+ this.cache = new LLMCache({
881
+ ttlMs: config.cacheTtlMs,
882
+ maxSize: config.cacheMaxSize
883
+ });
884
+ }
885
+ createAdapter(provider, apiKey, model) {
886
+ if (provider === "anthropic") {
887
+ return new AnthropicAdapter({
888
+ apiKey,
889
+ model: model || "claude-3-5-sonnet-20241022"
890
+ });
891
+ }
892
+ return new OpenAIAdapter({
893
+ apiKey,
894
+ model: model || "gpt-4"
895
+ });
896
+ }
897
+ isCircuitOpen() {
898
+ if (!this.circuit.isOpen) return false;
899
+ if (Date.now() - this.circuit.lastFailure >= this.circuitResetMs) {
900
+ this.circuit.isOpen = false;
901
+ this.emit("llm-circuit-half-open", { provider: this.config.provider });
902
+ return false;
903
+ }
904
+ return true;
905
+ }
906
+ recordFailure() {
907
+ this.circuit.failures++;
908
+ this.circuit.lastFailure = Date.now();
909
+ if (this.circuit.failures >= this.circuitThreshold) {
910
+ this.circuit.isOpen = true;
911
+ metrics.inc("llm_circuit_opens");
912
+ this.emit("llm-circuit-open", {
913
+ provider: this.config.provider,
914
+ failures: this.circuit.failures
915
+ });
916
+ }
917
+ }
918
+ recordSuccess() {
919
+ this.circuit.failures = 0;
920
+ this.circuit.isOpen = false;
921
+ }
922
+ /** Generate text, returning the string content */
923
+ async generate(prompt) {
924
+ const cached = this.cache.get(prompt);
925
+ if (cached !== null) {
926
+ metrics.inc("llm_cache_hits");
927
+ this.emit("llm-cache-hit", { promptLength: prompt.length });
928
+ return cached;
929
+ }
930
+ metrics.inc("llm_calls");
931
+ if (!this.isCircuitOpen()) {
932
+ try {
933
+ return await this.generateWithRetry(
934
+ this.config.provider,
935
+ this.config.apiKey,
936
+ this.config.model,
937
+ prompt
938
+ );
939
+ } catch (e) {
940
+ this.recordFailure();
941
+ this.emit("llm-primary-failed", {
942
+ provider: this.config.provider,
943
+ error: e instanceof Error ? e.message : String(e)
944
+ });
945
+ }
946
+ }
947
+ if (this.config.fallbackProvider && this.config.fallbackApiKey) {
948
+ try {
949
+ metrics.inc("llm_fallbacks");
950
+ this.emit("llm-fallback", {
951
+ from: this.config.provider,
952
+ to: this.config.fallbackProvider
953
+ });
954
+ const result = await this.generateWithRetry(
955
+ this.config.fallbackProvider,
956
+ this.config.fallbackApiKey,
957
+ this.config.fallbackModel,
958
+ prompt
959
+ );
960
+ this.cache.set(prompt, result);
961
+ return result;
962
+ } catch (e) {
963
+ throw new BrainError(
964
+ `Both LLM providers failed. Primary: ${this.config.provider}, Fallback: ${this.config.fallbackProvider}`
965
+ );
966
+ }
967
+ }
968
+ throw new BrainError(
969
+ `LLM provider ${this.config.provider} failed and no fallback is configured`
970
+ );
971
+ }
972
+ async generateWithRetry(provider, apiKey, model, prompt) {
973
+ const adapter = this.createAdapter(provider, apiKey, model);
974
+ let lastError;
975
+ for (let attempt = 1; attempt <= this.maxRetries; attempt++) {
976
+ try {
977
+ const result = await adapter.generate(prompt);
978
+ const text = result.content;
979
+ this.recordSuccess();
980
+ this.cache.set(prompt, text);
981
+ return text;
982
+ } catch (e) {
983
+ lastError = e;
984
+ if (attempt < this.maxRetries) {
985
+ const delayMs = Math.pow(2, attempt - 1) * 1e3;
986
+ metrics.inc("llm_retries");
987
+ this.emit("llm-retry", {
988
+ provider,
989
+ attempt,
990
+ maxRetries: this.maxRetries,
991
+ delayMs,
992
+ error: e instanceof Error ? e.message : String(e)
993
+ });
994
+ await new Promise((resolve2) => setTimeout(resolve2, delayMs));
995
+ }
996
+ }
997
+ }
998
+ throw lastError;
999
+ }
1000
+ getCircuitState() {
1001
+ return {
1002
+ isOpen: this.isCircuitOpen(),
1003
+ failures: this.circuit.failures
1004
+ };
1005
+ }
1006
+ getCacheStats() {
1007
+ return this.cache.stats();
1008
+ }
1009
+ clearCache() {
1010
+ this.cache.clear();
1011
+ }
1012
+ };
1013
+
1014
+ // agent/brain/index.ts
1015
+ var OpenQABrain = class extends EventEmitter2 {
1016
+ db;
1017
+ llm;
1018
+ saasConfig;
1019
+ generatedTests = /* @__PURE__ */ new Map();
1020
+ dynamicAgents = /* @__PURE__ */ new Map();
1021
+ workDir = "./data/workspace";
1022
+ testsDir = "./data/generated-tests";
1023
+ constructor(db, llmConfig, saasConfig) {
1024
+ super();
1025
+ this.db = db;
1026
+ this.saasConfig = saasConfig;
1027
+ this.llm = new ResilientLLM({
1028
+ provider: llmConfig.provider,
1029
+ apiKey: llmConfig.apiKey,
1030
+ model: llmConfig.model,
1031
+ fallbackProvider: llmConfig.fallbackProvider,
1032
+ fallbackApiKey: llmConfig.fallbackApiKey,
1033
+ fallbackModel: llmConfig.fallbackModel
1034
+ });
1035
+ for (const event of ["llm-retry", "llm-fallback", "llm-circuit-open", "llm-circuit-half-open", "llm-primary-failed"]) {
1036
+ this.llm.on(event, (data) => this.emit(event, data));
1037
+ }
1038
+ mkdirSync2(this.workDir, { recursive: true });
1039
+ mkdirSync2(this.testsDir, { recursive: true });
1040
+ }
1041
+ async analyze() {
1042
+ let codeContext = "";
1043
+ if (this.saasConfig.repoUrl || this.saasConfig.localPath) {
1044
+ codeContext = await this.analyzeCodebase();
1045
+ }
1046
+ const prompt = `You are OpenQA Brain, an autonomous QA system that thinks like a senior QA engineer.
1047
+
1048
+ ## SaaS Application to Test
1049
+ - **Name**: ${this.saasConfig.name}
1050
+ - **Description**: ${this.saasConfig.description}
1051
+ - **URL**: ${this.saasConfig.url}
1052
+ - **Tech Stack**: ${this.saasConfig.techStack?.join(", ") || "Unknown"}
1053
+ - **Auth Type**: ${this.saasConfig.authInfo?.type || "none"}
1054
+
1055
+ ## User Directives
1056
+ ${this.saasConfig.directives?.map((d) => `- ${d}`).join("\n") || "None specified"}
1057
+
1058
+ ${codeContext ? `## Code Analysis
1059
+ ${codeContext}` : ""}
1060
+
1061
+ ## Your Task
1062
+ Analyze this application and provide:
1063
+
1064
+ 1. **Understanding**: A brief summary of what this application does and its critical paths
1065
+ 2. **Suggested Tests**: List specific tests you would create (be concrete, not generic)
1066
+ 3. **Suggested Agents**: Custom agents you would create for this specific app
1067
+ 4. **Risks**: Potential issues or vulnerabilities to focus on
1068
+
1069
+ Think deeply about what could go wrong with THIS specific application.
1070
+
1071
+ Respond in JSON format:
1072
+ {
1073
+ "understanding": "...",
1074
+ "suggestedTests": ["Test 1: ...", "Test 2: ..."],
1075
+ "suggestedAgents": ["Agent for X", "Agent for Y"],
1076
+ "risks": ["Risk 1", "Risk 2"]
1077
+ }`;
1078
+ const response = await this.llm.generate(prompt);
1079
+ try {
1080
+ const jsonMatch = response.match(/\{[\s\S]*\}/);
1081
+ if (jsonMatch) {
1082
+ return JSON.parse(jsonMatch[0]);
1083
+ }
1084
+ } catch (e) {
1085
+ logger.warn("Failed to parse analysis", { error: e instanceof Error ? e.message : String(e) });
1086
+ }
1087
+ return {
1088
+ understanding: response,
1089
+ suggestedTests: [],
1090
+ suggestedAgents: [],
1091
+ risks: []
1092
+ };
1093
+ }
1094
+ async analyzeCodebase() {
1095
+ let repoPath = this.saasConfig.localPath;
1096
+ if (this.saasConfig.repoUrl && !this.saasConfig.localPath) {
1097
+ repoPath = join3(this.workDir, "repo");
1098
+ if (!existsSync2(repoPath)) {
1099
+ logger.info("Cloning repository", { url: this.saasConfig.repoUrl });
1100
+ try {
1101
+ execSync(`git clone --depth 1 ${this.saasConfig.repoUrl} ${repoPath}`, {
1102
+ stdio: "pipe"
1103
+ });
1104
+ } catch (e) {
1105
+ logger.error("Failed to clone repository", { error: e instanceof Error ? e.message : String(e) });
1106
+ return "";
1107
+ }
1108
+ }
1109
+ }
1110
+ if (!repoPath || !existsSync2(repoPath)) {
1111
+ return "";
1112
+ }
1113
+ const analysis = [];
1114
+ try {
1115
+ const packageJsonPath = join3(repoPath, "package.json");
1116
+ if (existsSync2(packageJsonPath)) {
1117
+ const pkg = JSON.parse(readFileSync3(packageJsonPath, "utf-8"));
1118
+ analysis.push(`### Package Info`);
1119
+ analysis.push(`- Name: ${pkg.name}`);
1120
+ analysis.push(`- Dependencies: ${Object.keys(pkg.dependencies || {}).slice(0, 20).join(", ")}`);
1121
+ analysis.push(`- Scripts: ${Object.keys(pkg.scripts || {}).join(", ")}`);
1122
+ }
1123
+ const srcFiles = this.findFiles(repoPath, [".ts", ".tsx", ".js", ".jsx", ".vue", ".svelte"], 50);
1124
+ if (srcFiles.length > 0) {
1125
+ analysis.push(`
1126
+ ### Source Files (${srcFiles.length} found)`);
1127
+ srcFiles.slice(0, 20).forEach((f) => {
1128
+ analysis.push(`- ${f.replace(repoPath, "")}`);
1129
+ });
1130
+ }
1131
+ const testFiles = this.findFiles(repoPath, [".test.ts", ".test.js", ".spec.ts", ".spec.js"], 20);
1132
+ if (testFiles.length > 0) {
1133
+ analysis.push(`
1134
+ ### Existing Tests (${testFiles.length} found)`);
1135
+ testFiles.slice(0, 10).forEach((f) => {
1136
+ analysis.push(`- ${f.replace(repoPath, "")}`);
1137
+ });
1138
+ }
1139
+ const routePatterns = ["routes", "pages", "views", "controllers", "api"];
1140
+ for (const pattern of routePatterns) {
1141
+ const routeDir = join3(repoPath, "src", pattern);
1142
+ if (existsSync2(routeDir)) {
1143
+ const routes = this.findFiles(routeDir, [".ts", ".tsx", ".js", ".jsx"], 20);
1144
+ if (routes.length > 0) {
1145
+ analysis.push(`
1146
+ ### Routes/Pages`);
1147
+ routes.forEach((f) => {
1148
+ analysis.push(`- ${f.replace(repoPath, "")}`);
1149
+ });
1150
+ }
1151
+ break;
1152
+ }
1153
+ }
1154
+ } catch (e) {
1155
+ logger.error("Error analyzing codebase", { error: e instanceof Error ? e.message : String(e) });
1156
+ }
1157
+ return analysis.join("\n");
1158
+ }
1159
+ findFiles(dir, extensions, limit) {
1160
+ const results = [];
1161
+ try {
1162
+ const find = (d) => {
1163
+ if (results.length >= limit) return;
1164
+ const { readdirSync, statSync: statSync2 } = __require("fs");
1165
+ const items = readdirSync(d);
1166
+ for (const item of items) {
1167
+ if (results.length >= limit) return;
1168
+ if (item.startsWith(".") || item === "node_modules" || item === "dist" || item === "build") continue;
1169
+ const fullPath = join3(d, item);
1170
+ const stat = statSync2(fullPath);
1171
+ if (stat.isDirectory()) {
1172
+ find(fullPath);
1173
+ } else if (extensions.some((ext) => item.endsWith(ext))) {
1174
+ results.push(fullPath);
1175
+ }
1176
+ }
1177
+ };
1178
+ find(dir);
1179
+ } catch (e) {
1180
+ }
1181
+ return results;
1182
+ }
1183
+ async generateTest(type, target, context) {
1184
+ const prompt = `You are OpenQA, an autonomous QA engineer. Generate a ${type} test.
1185
+
1186
+ ## Application
1187
+ - **Name**: ${this.saasConfig.name}
1188
+ - **Description**: ${this.saasConfig.description}
1189
+ - **URL**: ${this.saasConfig.url}
1190
+
1191
+ ## Test Target
1192
+ ${target}
1193
+
1194
+ ${context ? `## Additional Context
1195
+ ${context}` : ""}
1196
+
1197
+ ## Instructions
1198
+ Generate a complete, runnable test. Use Playwright for E2E/functional tests.
1199
+
1200
+ For ${type} tests:
1201
+ ${type === "unit" ? "- Test isolated functions/components\n- Mock dependencies\n- Use Jest or Vitest syntax" : ""}
1202
+ ${type === "functional" ? "- Test user workflows\n- Use Playwright\n- Include assertions" : ""}
1203
+ ${type === "e2e" ? "- Test complete user journeys\n- Use Playwright\n- Handle auth if needed" : ""}
1204
+ ${type === "regression" ? "- Test previously broken functionality\n- Verify bug fixes\n- Include edge cases" : ""}
1205
+ ${type === "security" ? "- Test for vulnerabilities\n- SQL injection, XSS, auth bypass\n- Use safe payloads" : ""}
1206
+ ${type === "performance" ? "- Measure load times\n- Check resource usage\n- Set thresholds" : ""}
1207
+
1208
+ Respond with JSON:
1209
+ {
1210
+ "name": "descriptive test name",
1211
+ "description": "what this test verifies",
1212
+ "code": "// complete test code here",
1213
+ "priority": 1-5
1214
+ }`;
1215
+ const response = await this.llm.generate(prompt);
1216
+ let testData = { name: target, description: "", code: "", priority: 3 };
1217
+ try {
1218
+ const jsonMatch = response.match(/\{[\s\S]*\}/);
1219
+ if (jsonMatch) {
1220
+ testData = JSON.parse(jsonMatch[0]);
1221
+ }
1222
+ } catch (e) {
1223
+ testData.code = response;
1224
+ }
1225
+ const test = {
1226
+ id: `test_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
1227
+ name: testData.name,
1228
+ type,
1229
+ description: testData.description,
1230
+ code: testData.code,
1231
+ priority: testData.priority,
1232
+ status: "pending",
1233
+ createdAt: /* @__PURE__ */ new Date()
1234
+ };
1235
+ this.generatedTests.set(test.id, test);
1236
+ this.saveTest(test);
1237
+ this.emit("test-generated", test);
1238
+ return test;
1239
+ }
1240
+ saveTest(test) {
1241
+ const filename = `${test.type}_${test.id}.ts`;
1242
+ const filepath = join3(this.testsDir, filename);
1243
+ const content = `/**
1244
+ * Generated by OpenQA
1245
+ * Type: ${test.type}
1246
+ * Name: ${test.name}
1247
+ * Description: ${test.description}
1248
+ * Created: ${test.createdAt.toISOString()}
1249
+ */
1250
+
1251
+ ${test.code}
1252
+ `;
1253
+ writeFileSync2(filepath, content);
1254
+ test.targetFile = filepath;
1255
+ }
1256
+ async createDynamicAgent(purpose) {
1257
+ const prompt = `You are OpenQA Brain. Create a specialized testing agent.
1258
+
1259
+ ## Application Context
1260
+ - **Name**: ${this.saasConfig.name}
1261
+ - **Description**: ${this.saasConfig.description}
1262
+ - **URL**: ${this.saasConfig.url}
1263
+
1264
+ ## Agent Purpose
1265
+ ${purpose}
1266
+
1267
+ ## Instructions
1268
+ Design a specialized agent for this specific purpose. The agent should:
1269
+ 1. Have a clear, focused mission
1270
+ 2. Know exactly what to test
1271
+ 3. Know how to report findings
1272
+
1273
+ Respond with JSON:
1274
+ {
1275
+ "name": "Agent Name",
1276
+ "purpose": "Clear purpose statement",
1277
+ "prompt": "Complete system prompt for this agent (be specific and detailed)",
1278
+ "tools": ["tool1", "tool2"] // from: navigate, click, fill, screenshot, check_console, create_issue, create_ticket
1279
+ }`;
1280
+ const response = await this.llm.generate(prompt);
1281
+ let agentData = { name: purpose, purpose, prompt: "", tools: [] };
1282
+ try {
1283
+ const jsonMatch = response.match(/\{[\s\S]*\}/);
1284
+ if (jsonMatch) {
1285
+ agentData = JSON.parse(jsonMatch[0]);
1286
+ }
1287
+ } catch (_e) {
1288
+ agentData.prompt = response;
1289
+ }
1290
+ const agent = {
1291
+ id: `agent_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
1292
+ name: agentData.name,
1293
+ purpose: agentData.purpose,
1294
+ prompt: agentData.prompt,
1295
+ tools: agentData.tools,
1296
+ createdAt: /* @__PURE__ */ new Date(),
1297
+ executionCount: 0,
1298
+ successRate: 0
1299
+ };
1300
+ this.dynamicAgents.set(agent.id, agent);
1301
+ this.emit("agent-created", agent);
1302
+ return agent;
1303
+ }
1304
+ async executeTest(testId) {
1305
+ const test = this.generatedTests.get(testId);
1306
+ if (!test) throw new Error(`Test ${testId} not found`);
1307
+ test.status = "running";
1308
+ test.executedAt = /* @__PURE__ */ new Date();
1309
+ this.emit("test-started", test);
1310
+ try {
1311
+ if (test.targetFile && existsSync2(test.targetFile)) {
1312
+ const result = execSync(`npx playwright test ${test.targetFile} --reporter=json`, {
1313
+ cwd: this.testsDir,
1314
+ stdio: "pipe",
1315
+ timeout: 12e4
1316
+ });
1317
+ test.status = "passed";
1318
+ test.result = result.toString();
1319
+ } else {
1320
+ test.status = "passed";
1321
+ test.result = "Test code generated (execution skipped - no test runner configured)";
1322
+ }
1323
+ } catch (e) {
1324
+ test.status = "failed";
1325
+ test.error = e instanceof Error ? e.message : String(e);
1326
+ }
1327
+ this.emit("test-completed", test);
1328
+ return test;
1329
+ }
1330
+ async think() {
1331
+ const recentTests = Array.from(this.generatedTests.values()).slice(-10);
1332
+ const recentAgents = Array.from(this.dynamicAgents.values()).slice(-5);
1333
+ const prompt = `You are OpenQA Brain, an autonomous QA system. Think about what to do next.
1334
+
1335
+ ## Application
1336
+ - **Name**: ${this.saasConfig.name}
1337
+ - **Description**: ${this.saasConfig.description}
1338
+ - **URL**: ${this.saasConfig.url}
1339
+
1340
+ ## User Directives
1341
+ ${this.saasConfig.directives?.map((d) => `- ${d}`).join("\n") || "None"}
1342
+
1343
+ ## Recent Tests (${recentTests.length})
1344
+ ${recentTests.map((t) => `- [${t.status}] ${t.type}: ${t.name}`).join("\n") || "None yet"}
1345
+
1346
+ ## Active Agents (${recentAgents.length})
1347
+ ${recentAgents.map((a) => `- ${a.name}: ${a.purpose}`).join("\n") || "None yet"}
1348
+
1349
+ ## Your Task
1350
+ Decide what to do next. Consider:
1351
+ 1. What areas haven't been tested yet?
1352
+ 2. Are there any failed tests that need investigation?
1353
+ 3. Should you create new specialized agents?
1354
+ 4. What tests would be most valuable right now?
1355
+
1356
+ Respond with JSON:
1357
+ {
1358
+ "decision": "Brief explanation of your reasoning",
1359
+ "actions": [
1360
+ { "type": "generate_test", "target": "what to test", "reason": "why" },
1361
+ { "type": "create_agent", "target": "agent purpose", "reason": "why" },
1362
+ { "type": "run_test", "target": "test_id", "reason": "why" },
1363
+ { "type": "analyze", "target": "what to analyze", "reason": "why" }
1364
+ ]
1365
+ }`;
1366
+ const response = await this.llm.generate(prompt);
1367
+ try {
1368
+ const jsonMatch = response.match(/\{[\s\S]*\}/);
1369
+ if (jsonMatch) {
1370
+ return JSON.parse(jsonMatch[0]);
1371
+ }
1372
+ } catch (e) {
1373
+ }
1374
+ return {
1375
+ decision: response,
1376
+ actions: []
1377
+ };
1378
+ }
1379
+ async runAutonomously(maxIterations = 10) {
1380
+ const log = logger.child({ app: this.saasConfig.name });
1381
+ log.info("Brain starting autonomous mode", { maxIterations });
1382
+ const analysis = await this.analyze();
1383
+ log.info("Analysis complete", { suggestedTests: analysis.suggestedTests.length });
1384
+ this.emit("analysis-complete", analysis);
1385
+ for (let i = 0; i < maxIterations; i++) {
1386
+ log.info("Iteration", { iteration: i + 1, maxIterations });
1387
+ const thought = await this.think();
1388
+ log.debug("Decision", { decision: thought.decision });
1389
+ this.emit("thinking", thought);
1390
+ for (const action of thought.actions) {
1391
+ log.debug("Action", { type: action.type, target: action.target });
1392
+ try {
1393
+ switch (action.type) {
1394
+ case "generate_test":
1395
+ const testType = this.inferTestType(action.target);
1396
+ await this.generateTest(testType, action.target, action.reason);
1397
+ break;
1398
+ case "create_agent":
1399
+ await this.createDynamicAgent(action.target);
1400
+ break;
1401
+ case "run_test":
1402
+ if (this.generatedTests.has(action.target)) {
1403
+ await this.executeTest(action.target);
1404
+ }
1405
+ break;
1406
+ case "analyze":
1407
+ break;
1408
+ }
1409
+ } catch (e) {
1410
+ log.error("Action failed", { type: action.type, error: e instanceof Error ? e.message : String(e) });
1411
+ }
1412
+ }
1413
+ if (thought.actions.length === 0) {
1414
+ log.info("No more actions needed");
1415
+ break;
1416
+ }
1417
+ await new Promise((resolve2) => setTimeout(resolve2, 2e3));
1418
+ }
1419
+ log.info("Autonomous session complete");
1420
+ this.emit("session-complete", {
1421
+ testsGenerated: this.generatedTests.size,
1422
+ agentsCreated: this.dynamicAgents.size
1423
+ });
1424
+ }
1425
+ inferTestType(target) {
1426
+ const lower = target.toLowerCase();
1427
+ if (lower.includes("security") || lower.includes("injection") || lower.includes("xss")) return "security";
1428
+ if (lower.includes("performance") || lower.includes("load") || lower.includes("speed")) return "performance";
1429
+ if (lower.includes("regression") || lower.includes("bug") || lower.includes("fix")) return "regression";
1430
+ if (lower.includes("unit") || lower.includes("function") || lower.includes("component")) return "unit";
1431
+ if (lower.includes("e2e") || lower.includes("journey") || lower.includes("flow")) return "e2e";
1432
+ return "functional";
1433
+ }
1434
+ getGeneratedTests() {
1435
+ return Array.from(this.generatedTests.values());
1436
+ }
1437
+ getDynamicAgents() {
1438
+ return Array.from(this.dynamicAgents.values());
1439
+ }
1440
+ getStats() {
1441
+ const tests = this.getGeneratedTests();
1442
+ return {
1443
+ totalTests: tests.length,
1444
+ passed: tests.filter((t) => t.status === "passed").length,
1445
+ failed: tests.filter((t) => t.status === "failed").length,
1446
+ pending: tests.filter((t) => t.status === "pending").length,
1447
+ agents: this.dynamicAgents.size,
1448
+ byType: {
1449
+ unit: tests.filter((t) => t.type === "unit").length,
1450
+ functional: tests.filter((t) => t.type === "functional").length,
1451
+ e2e: tests.filter((t) => t.type === "e2e").length,
1452
+ regression: tests.filter((t) => t.type === "regression").length,
1453
+ security: tests.filter((t) => t.type === "security").length,
1454
+ performance: tests.filter((t) => t.type === "performance").length
1455
+ }
1456
+ };
1457
+ }
1458
+ };
1459
+
1460
+ // agent/tools/browser.ts
1461
+ init_esm_shims();
1462
+ import { chromium } from "playwright";
1463
+ import { mkdirSync as mkdirSync3 } from "fs";
1464
+ import { join as join4 } from "path";
1465
+ var BrowserTools = class {
1466
+ browser = null;
1467
+ page = null;
1468
+ db;
1469
+ sessionId;
1470
+ screenshotDir = "./data/screenshots";
1471
+ constructor(db, sessionId) {
1472
+ this.db = db;
1473
+ this.sessionId = sessionId;
1474
+ mkdirSync3(this.screenshotDir, { recursive: true });
1475
+ }
1476
+ async initialize() {
1477
+ this.browser = await chromium.launch({ headless: true });
1478
+ const context = await this.browser.newContext({
1479
+ viewport: { width: 1920, height: 1080 },
1480
+ userAgent: "OpenQA/1.0 (Automated Testing Agent)"
1481
+ });
1482
+ this.page = await context.newPage();
1483
+ }
1484
+ getTools() {
1485
+ return [
1486
+ {
1487
+ name: "navigate_to_page",
1488
+ description: "Navigate to a specific URL in the application",
1489
+ parameters: {
1490
+ type: "object",
1491
+ properties: {
1492
+ url: { type: "string", description: "The URL to navigate to" }
1493
+ },
1494
+ required: ["url"]
1495
+ },
1496
+ execute: async ({ url }) => {
1497
+ if (!this.page) await this.initialize();
1498
+ try {
1499
+ await this.page.goto(url, { waitUntil: "networkidle" });
1500
+ const title = await this.page.title();
1501
+ this.db.createAction({
1502
+ session_id: this.sessionId,
1503
+ type: "navigate",
1504
+ description: `Navigated to ${url}`,
1505
+ input: url,
1506
+ output: `Page title: ${title}`
1507
+ });
1508
+ return `Successfully navigated to ${url}. Page title: "${title}"`;
1509
+ } catch (error) {
1510
+ return `Failed to navigate: ${error instanceof Error ? error.message : String(error)}`;
1511
+ }
1512
+ }
1513
+ },
1514
+ {
1515
+ name: "click_element",
1516
+ description: "Click on an element using a CSS selector",
1517
+ parameters: {
1518
+ type: "object",
1519
+ properties: {
1520
+ selector: { type: "string", description: "CSS selector of the element to click" }
1521
+ },
1522
+ required: ["selector"]
1523
+ },
1524
+ execute: async ({ selector }) => {
1525
+ if (!this.page) return "Browser not initialized. Navigate to a page first.";
1526
+ try {
1527
+ await this.page.click(selector, { timeout: 5e3 });
1528
+ this.db.createAction({
1529
+ session_id: this.sessionId,
1530
+ type: "click",
1531
+ description: `Clicked element: ${selector}`,
1532
+ input: selector
1533
+ });
1534
+ return `Successfully clicked element: ${selector}`;
1535
+ } catch (error) {
1536
+ return `Failed to click element: ${error instanceof Error ? error.message : String(error)}`;
1537
+ }
1538
+ }
1539
+ },
1540
+ {
1541
+ name: "fill_input",
1542
+ description: "Fill an input field with text",
1543
+ parameters: {
1544
+ type: "object",
1545
+ properties: {
1546
+ selector: { type: "string", description: "CSS selector of the input field" },
1547
+ text: { type: "string", description: "Text to fill in the input" }
1548
+ },
1549
+ required: ["selector", "text"]
1550
+ },
1551
+ execute: async ({ selector, text }) => {
1552
+ if (!this.page) return "Browser not initialized. Navigate to a page first.";
1553
+ try {
1554
+ await this.page.fill(selector, text);
1555
+ this.db.createAction({
1556
+ session_id: this.sessionId,
1557
+ type: "fill",
1558
+ description: `Filled input ${selector}`,
1559
+ input: `${selector} = ${text}`
1560
+ });
1561
+ return `Successfully filled input ${selector} with text`;
1562
+ } catch (error) {
1563
+ return `Failed to fill input: ${error instanceof Error ? error.message : String(error)}`;
1564
+ }
1565
+ }
1566
+ },
1567
+ {
1568
+ name: "take_screenshot",
1569
+ description: "Take a screenshot of the current page for evidence",
1570
+ parameters: {
1571
+ type: "object",
1572
+ properties: {
1573
+ name: { type: "string", description: "Name for the screenshot file" }
1574
+ },
1575
+ required: ["name"]
1576
+ },
1577
+ execute: async ({ name }) => {
1578
+ if (!this.page) return "Browser not initialized. Navigate to a page first.";
1579
+ try {
1580
+ const filename = `${Date.now()}_${name}.png`;
1581
+ const path2 = join4(this.screenshotDir, filename);
1582
+ await this.page.screenshot({ path: path2, fullPage: true });
1583
+ this.db.createAction({
1584
+ session_id: this.sessionId,
1585
+ type: "screenshot",
1586
+ description: `Screenshot: ${name}`,
1587
+ screenshot_path: path2
1588
+ });
1589
+ return `Screenshot saved: ${path2}`;
1590
+ } catch (error) {
1591
+ return `Failed to take screenshot: ${error instanceof Error ? error.message : String(error)}`;
1592
+ }
1593
+ }
1594
+ },
1595
+ {
1596
+ name: "get_page_content",
1597
+ description: "Get the text content of the current page",
1598
+ parameters: {
1599
+ type: "object",
1600
+ properties: {}
1601
+ },
1602
+ execute: async () => {
1603
+ if (!this.page) return "Browser not initialized. Navigate to a page first.";
1604
+ try {
1605
+ const content = await this.page.textContent("body");
1606
+ return content?.slice(0, 1e3) || "No content found";
1607
+ } catch (error) {
1608
+ return `Failed to get content: ${error instanceof Error ? error.message : String(error)}`;
1609
+ }
1610
+ }
1611
+ },
1612
+ {
1613
+ name: "check_console_errors",
1614
+ description: "Check for JavaScript console errors on the page",
1615
+ parameters: {
1616
+ type: "object",
1617
+ properties: {}
1618
+ },
1619
+ execute: async () => {
1620
+ if (!this.page) return "Browser not initialized. Navigate to a page first.";
1621
+ const errors = [];
1622
+ this.page.on("console", (msg) => {
1623
+ if (msg.type() === "error") {
1624
+ errors.push(msg.text());
1625
+ }
1626
+ });
1627
+ await this.page.waitForTimeout(2e3);
1628
+ if (errors.length > 0) {
1629
+ return `Found ${errors.length} console errors:
1630
+ ${errors.join("\n")}`;
1631
+ }
1632
+ return "No console errors detected";
1633
+ }
1634
+ }
1635
+ ];
1636
+ }
1637
+ async close() {
1638
+ if (this.browser) {
1639
+ await this.browser.close();
1640
+ this.browser = null;
1641
+ this.page = null;
1642
+ }
1643
+ }
1644
+ };
1645
+
1646
+ // agent/webhooks/git-listener.ts
1647
+ init_esm_shims();
1648
+ import { EventEmitter as EventEmitter3 } from "events";
1649
+ import { Octokit } from "@octokit/rest";
1650
+ var GitListener = class extends EventEmitter3 {
1651
+ config;
1652
+ octokit = null;
1653
+ lastCommitSha = null;
1654
+ lastPipelineId = null;
1655
+ pollInterval = null;
1656
+ isRunning = false;
1657
+ constructor(config) {
1658
+ super();
1659
+ this.config = {
1660
+ branch: "main",
1661
+ pollIntervalMs: 6e4,
1662
+ gitlabUrl: "https://gitlab.com",
1663
+ ...config
1664
+ };
1665
+ if (config.provider === "github" && config.token) {
1666
+ this.octokit = new Octokit({ auth: config.token });
1667
+ }
1668
+ }
1669
+ async start() {
1670
+ if (this.isRunning) return;
1671
+ this.isRunning = true;
1672
+ logger.info("GitListener started", { provider: this.config.provider, owner: this.config.owner, repo: this.config.repo });
1673
+ await this.checkInitialState();
1674
+ this.pollInterval = setInterval(() => {
1675
+ this.poll().catch((e) => logger.error("Poll error", { error: e instanceof Error ? e.message : String(e) }));
1676
+ }, this.config.pollIntervalMs);
1677
+ }
1678
+ stop() {
1679
+ this.isRunning = false;
1680
+ if (this.pollInterval) {
1681
+ clearInterval(this.pollInterval);
1682
+ this.pollInterval = null;
1683
+ }
1684
+ logger.info("GitListener stopped");
1685
+ }
1686
+ async checkInitialState() {
1687
+ try {
1688
+ if (this.config.provider === "github") {
1689
+ await this.checkGitHubState();
1690
+ } else {
1691
+ await this.checkGitLabState();
1692
+ }
1693
+ } catch (error) {
1694
+ logger.error("Failed to check initial state", { error: error instanceof Error ? error.message : String(error) });
1695
+ }
1696
+ }
1697
+ async poll() {
1698
+ try {
1699
+ if (this.config.provider === "github") {
1700
+ await this.pollGitHub();
1701
+ } else {
1702
+ await this.pollGitLab();
1703
+ }
1704
+ } catch (error) {
1705
+ logger.error("Poll error", { error: error instanceof Error ? error.message : String(error) });
1706
+ }
1707
+ }
1708
+ async checkGitHubState() {
1709
+ if (!this.octokit) return;
1710
+ const { data: commits } = await this.octokit.repos.listCommits({
1711
+ owner: this.config.owner,
1712
+ repo: this.config.repo,
1713
+ sha: this.config.branch,
1714
+ per_page: 1
1715
+ });
1716
+ if (commits.length > 0) {
1717
+ this.lastCommitSha = commits[0].sha;
1718
+ }
1719
+ try {
1720
+ const { data: runs } = await this.octokit.actions.listWorkflowRunsForRepo({
1721
+ owner: this.config.owner,
1722
+ repo: this.config.repo,
1723
+ branch: this.config.branch,
1724
+ per_page: 1
1725
+ });
1726
+ if (runs.workflow_runs.length > 0) {
1727
+ this.lastPipelineId = runs.workflow_runs[0].id.toString();
1728
+ }
1729
+ } catch {
1730
+ }
1731
+ }
1732
+ async pollGitHub() {
1733
+ if (!this.octokit) return;
1734
+ const { data: commits } = await this.octokit.repos.listCommits({
1735
+ owner: this.config.owner,
1736
+ repo: this.config.repo,
1737
+ sha: this.config.branch,
1738
+ per_page: 5
1739
+ });
1740
+ for (const commit of commits) {
1741
+ if (this.lastCommitSha && commit.sha === this.lastCommitSha) break;
1742
+ const isMerge = commit.parents && commit.parents.length > 1;
1743
+ const event = {
1744
+ type: isMerge ? "merge" : "push",
1745
+ provider: "github",
1746
+ branch: this.config.branch,
1747
+ commit: commit.sha,
1748
+ author: commit.commit.author?.name || "unknown",
1749
+ message: commit.commit.message,
1750
+ timestamp: new Date(commit.commit.author?.date || Date.now())
1751
+ };
1752
+ this.emit("git-event", event);
1753
+ if (isMerge) {
1754
+ this.emit("merge", event);
1755
+ logger.info("Merge detected", { branch: this.config.branch, sha: commit.sha.slice(0, 7) });
1756
+ }
1757
+ }
1758
+ if (commits.length > 0) {
1759
+ this.lastCommitSha = commits[0].sha;
1760
+ }
1761
+ try {
1762
+ const { data: runs } = await this.octokit.actions.listWorkflowRunsForRepo({
1763
+ owner: this.config.owner,
1764
+ repo: this.config.repo,
1765
+ branch: this.config.branch,
1766
+ per_page: 5
1767
+ });
1768
+ for (const run of runs.workflow_runs) {
1769
+ if (this.lastPipelineId && run.id.toString() === this.lastPipelineId) break;
1770
+ if (run.status === "completed") {
1771
+ const event = {
1772
+ type: run.conclusion === "success" ? "pipeline_success" : "pipeline_failure",
1773
+ provider: "github",
1774
+ branch: this.config.branch,
1775
+ commit: run.head_sha,
1776
+ author: run.actor?.login || "unknown",
1777
+ message: run.name || "",
1778
+ timestamp: new Date(run.updated_at || Date.now()),
1779
+ pipelineId: run.id.toString(),
1780
+ pipelineStatus: run.conclusion || void 0
1781
+ };
1782
+ this.emit("git-event", event);
1783
+ if (run.conclusion === "success") {
1784
+ this.emit("pipeline-success", event);
1785
+ logger.info("Pipeline success", { name: run.name, id: run.id });
1786
+ } else {
1787
+ this.emit("pipeline-failure", event);
1788
+ logger.warn("Pipeline failure", { name: run.name, id: run.id });
1789
+ }
1790
+ }
1791
+ }
1792
+ if (runs.workflow_runs.length > 0) {
1793
+ this.lastPipelineId = runs.workflow_runs[0].id.toString();
1794
+ }
1795
+ } catch {
1796
+ }
1797
+ }
1798
+ async checkGitLabState() {
1799
+ const headers = { "PRIVATE-TOKEN": this.config.token };
1800
+ const projectPath = encodeURIComponent(`${this.config.owner}/${this.config.repo}`);
1801
+ const baseUrl = this.config.gitlabUrl;
1802
+ try {
1803
+ const commitsRes = await fetch(
1804
+ `${baseUrl}/api/v4/projects/${projectPath}/repository/commits?ref_name=${this.config.branch}&per_page=1`,
1805
+ { headers }
1806
+ );
1807
+ const commits = await commitsRes.json();
1808
+ if (commits.length > 0) {
1809
+ this.lastCommitSha = commits[0].id;
1810
+ }
1811
+ const pipelinesRes = await fetch(
1812
+ `${baseUrl}/api/v4/projects/${projectPath}/pipelines?ref=${this.config.branch}&per_page=1`,
1813
+ { headers }
1814
+ );
1815
+ const pipelines = await pipelinesRes.json();
1816
+ if (pipelines.length > 0) {
1817
+ this.lastPipelineId = pipelines[0].id.toString();
1818
+ }
1819
+ } catch (error) {
1820
+ logger.error("GitLab initial state error", { error: error instanceof Error ? error.message : String(error) });
1821
+ }
1822
+ }
1823
+ async pollGitLab() {
1824
+ const headers = { "PRIVATE-TOKEN": this.config.token };
1825
+ const projectPath = encodeURIComponent(`${this.config.owner}/${this.config.repo}`);
1826
+ const baseUrl = this.config.gitlabUrl;
1827
+ try {
1828
+ const commitsRes = await fetch(
1829
+ `${baseUrl}/api/v4/projects/${projectPath}/repository/commits?ref_name=${this.config.branch}&per_page=5`,
1830
+ { headers }
1831
+ );
1832
+ const commits = await commitsRes.json();
1833
+ for (const commit of commits) {
1834
+ if (this.lastCommitSha && commit.id === this.lastCommitSha) break;
1835
+ const isMerge = commit.parent_ids && commit.parent_ids.length > 1;
1836
+ const event = {
1837
+ type: isMerge ? "merge" : "push",
1838
+ provider: "gitlab",
1839
+ branch: this.config.branch,
1840
+ commit: commit.id,
1841
+ author: commit.author_name,
1842
+ message: commit.message,
1843
+ timestamp: new Date(commit.created_at)
1844
+ };
1845
+ this.emit("git-event", event);
1846
+ if (isMerge) {
1847
+ this.emit("merge", event);
1848
+ logger.info("Merge detected", { branch: this.config.branch, id: commit.id.slice(0, 7) });
1849
+ }
1850
+ }
1851
+ if (commits.length > 0) {
1852
+ this.lastCommitSha = commits[0].id;
1853
+ }
1854
+ const pipelinesRes = await fetch(
1855
+ `${baseUrl}/api/v4/projects/${projectPath}/pipelines?ref=${this.config.branch}&per_page=5`,
1856
+ { headers }
1857
+ );
1858
+ const pipelines = await pipelinesRes.json();
1859
+ for (const pipeline of pipelines) {
1860
+ if (this.lastPipelineId && pipeline.id.toString() === this.lastPipelineId) break;
1861
+ if (pipeline.status === "success" || pipeline.status === "failed") {
1862
+ const event = {
1863
+ type: pipeline.status === "success" ? "pipeline_success" : "pipeline_failure",
1864
+ provider: "gitlab",
1865
+ branch: this.config.branch,
1866
+ commit: pipeline.sha,
1867
+ author: pipeline.user?.name || "unknown",
1868
+ message: `Pipeline #${pipeline.id}`,
1869
+ timestamp: new Date(pipeline.updated_at),
1870
+ pipelineId: pipeline.id.toString(),
1871
+ pipelineStatus: pipeline.status
1872
+ };
1873
+ this.emit("git-event", event);
1874
+ if (pipeline.status === "success") {
1875
+ this.emit("pipeline-success", event);
1876
+ logger.info("Pipeline success", { id: pipeline.id });
1877
+ } else {
1878
+ this.emit("pipeline-failure", event);
1879
+ logger.warn("Pipeline failure", { id: pipeline.id });
1880
+ }
1881
+ }
1882
+ }
1883
+ if (pipelines.length > 0) {
1884
+ this.lastPipelineId = pipelines[0].id.toString();
1885
+ }
1886
+ } catch (error) {
1887
+ logger.error("GitLab poll error", { error: error instanceof Error ? error.message : String(error) });
1888
+ }
1889
+ }
1890
+ async setupWebhook(webhookUrl) {
1891
+ if (this.config.provider === "github") {
1892
+ return this.setupGitHubWebhook(webhookUrl);
1893
+ } else {
1894
+ return this.setupGitLabWebhook(webhookUrl);
1895
+ }
1896
+ }
1897
+ async setupGitHubWebhook(webhookUrl) {
1898
+ if (!this.octokit) throw new Error("GitHub not configured");
1899
+ const { data } = await this.octokit.repos.createWebhook({
1900
+ owner: this.config.owner,
1901
+ repo: this.config.repo,
1902
+ config: {
1903
+ url: webhookUrl,
1904
+ content_type: "json"
1905
+ },
1906
+ events: ["push", "pull_request", "workflow_run"]
1907
+ });
1908
+ return data.id.toString();
1909
+ }
1910
+ async setupGitLabWebhook(webhookUrl) {
1911
+ const headers = {
1912
+ "PRIVATE-TOKEN": this.config.token,
1913
+ "Content-Type": "application/json"
1914
+ };
1915
+ const projectPath = encodeURIComponent(`${this.config.owner}/${this.config.repo}`);
1916
+ const res = await fetch(
1917
+ `${this.config.gitlabUrl}/api/v4/projects/${projectPath}/hooks`,
1918
+ {
1919
+ method: "POST",
1920
+ headers,
1921
+ body: JSON.stringify({
1922
+ url: webhookUrl,
1923
+ push_events: true,
1924
+ merge_requests_events: true,
1925
+ pipeline_events: true
1926
+ })
1927
+ }
1928
+ );
1929
+ const data = await res.json();
1930
+ return data.id.toString();
1931
+ }
1932
+ };
1933
+
1934
+ // agent/tools/project-runner.ts
1935
+ init_esm_shims();
1936
+ import { EventEmitter as EventEmitter4 } from "events";
1937
+ import { spawn } from "child_process";
1938
+ import { existsSync as existsSync3, readFileSync as readFileSync4, statSync } from "fs";
1939
+ import { join as join5, resolve } from "path";
1940
+ function sanitizeRepoPath(inputPath) {
1941
+ if (typeof inputPath !== "string" || !inputPath.trim()) {
1942
+ throw new ProjectRunnerError("repoPath must be a non-empty string");
1943
+ }
1944
+ const resolved = resolve(inputPath);
1945
+ try {
1946
+ const stat = statSync(resolved);
1947
+ if (!stat.isDirectory()) {
1948
+ throw new ProjectRunnerError(`repoPath is not a directory: ${resolved}`);
1949
+ }
1950
+ } catch (err) {
1951
+ if (err instanceof ProjectRunnerError) throw err;
1952
+ throw new ProjectRunnerError(`repoPath does not exist: ${resolved}`);
1953
+ }
1954
+ return resolved;
1955
+ }
1956
+ var ProjectRunner = class extends EventEmitter4 {
1957
+ serverProcess = null;
1958
+ serverUrl = null;
1959
+ installed = false;
1960
+ projectType = null;
1961
+ repoPath = null;
1962
+ /**
1963
+ * Detect what kind of project lives at the given path
1964
+ */
1965
+ detectProjectType(repoPath) {
1966
+ repoPath = sanitizeRepoPath(repoPath);
1967
+ const pkgPath = join5(repoPath, "package.json");
1968
+ if (existsSync3(pkgPath)) {
1969
+ return this.detectNodeProject(repoPath, pkgPath);
1970
+ }
1971
+ if (existsSync3(join5(repoPath, "requirements.txt")) || existsSync3(join5(repoPath, "pyproject.toml"))) {
1972
+ return this.detectPythonProject(repoPath);
1973
+ }
1974
+ if (existsSync3(join5(repoPath, "go.mod"))) {
1975
+ return {
1976
+ language: "go",
1977
+ packageManager: "go",
1978
+ scripts: {},
1979
+ testRunner: "go test",
1980
+ devCommand: "go run ."
1981
+ };
1982
+ }
1983
+ if (existsSync3(join5(repoPath, "Cargo.toml"))) {
1984
+ return {
1985
+ language: "rust",
1986
+ packageManager: "cargo",
1987
+ scripts: {},
1988
+ testRunner: "cargo test",
1989
+ devCommand: "cargo run"
1990
+ };
1991
+ }
1992
+ return { language: "unknown", packageManager: "unknown", scripts: {} };
1993
+ }
1994
+ detectNodeProject(repoPath, pkgPath) {
1995
+ const pkg = JSON.parse(readFileSync4(pkgPath, "utf-8"));
1996
+ const scripts = pkg.scripts || {};
1997
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
1998
+ let packageManager = "npm";
1999
+ if (existsSync3(join5(repoPath, "pnpm-lock.yaml"))) packageManager = "pnpm";
2000
+ else if (existsSync3(join5(repoPath, "yarn.lock"))) packageManager = "yarn";
2001
+ else if (existsSync3(join5(repoPath, "bun.lockb"))) packageManager = "bun";
2002
+ let framework;
2003
+ if (deps["next"]) framework = "next";
2004
+ else if (deps["nuxt"]) framework = "nuxt";
2005
+ else if (deps["@angular/core"]) framework = "angular";
2006
+ else if (deps["svelte"] || deps["@sveltejs/kit"]) framework = "svelte";
2007
+ else if (deps["vue"]) framework = "vue";
2008
+ else if (deps["react"]) framework = "react";
2009
+ else if (deps["express"]) framework = "express";
2010
+ else if (deps["fastify"]) framework = "fastify";
2011
+ else if (deps["@nestjs/core"]) framework = "nestjs";
2012
+ let testRunner;
2013
+ if (deps["vitest"]) testRunner = "vitest";
2014
+ else if (deps["jest"]) testRunner = "jest";
2015
+ else if (deps["mocha"]) testRunner = "mocha";
2016
+ else if (deps["playwright"] || deps["@playwright/test"]) testRunner = "playwright";
2017
+ else if (deps["cypress"]) testRunner = "cypress";
2018
+ let devCommand;
2019
+ if (scripts["dev"]) devCommand = `${packageManager} run dev`;
2020
+ else if (scripts["start"]) devCommand = `${packageManager} run start`;
2021
+ else if (scripts["serve"]) devCommand = `${packageManager} run serve`;
2022
+ let buildCommand;
2023
+ if (scripts["build"]) buildCommand = `${packageManager} run build`;
2024
+ let port;
2025
+ const devScript = scripts["dev"] || scripts["start"] || "";
2026
+ const portMatch = devScript.match(/--port[= ](\d+)|-p[= ]?(\d+)/);
2027
+ if (portMatch) {
2028
+ port = parseInt(portMatch[1] || portMatch[2]);
2029
+ } else if (framework === "next") {
2030
+ port = 3e3;
2031
+ } else if (framework === "nuxt" || framework === "vue") {
2032
+ port = 3e3;
2033
+ } else if (framework === "angular") {
2034
+ port = 4200;
2035
+ } else if (framework === "svelte") {
2036
+ port = 5173;
2037
+ } else if (framework === "react") {
2038
+ port = 3e3;
2039
+ }
2040
+ return {
2041
+ language: "node",
2042
+ framework,
2043
+ packageManager,
2044
+ scripts,
2045
+ testRunner,
2046
+ devCommand,
2047
+ buildCommand,
2048
+ port
2049
+ };
2050
+ }
2051
+ detectPythonProject(repoPath) {
2052
+ let testRunner;
2053
+ const reqPath = join5(repoPath, "requirements.txt");
2054
+ if (existsSync3(reqPath)) {
2055
+ const content = readFileSync4(reqPath, "utf-8");
2056
+ if (content.includes("pytest")) testRunner = "pytest";
2057
+ }
2058
+ const pyprojectPath = join5(repoPath, "pyproject.toml");
2059
+ if (existsSync3(pyprojectPath)) {
2060
+ const content = readFileSync4(pyprojectPath, "utf-8");
2061
+ if (content.includes("pytest")) testRunner = "pytest";
2062
+ }
2063
+ return {
2064
+ language: "python",
2065
+ packageManager: "pip",
2066
+ scripts: {},
2067
+ testRunner: testRunner || "pytest",
2068
+ devCommand: "python -m flask run"
2069
+ };
2070
+ }
2071
+ /**
2072
+ * Install project dependencies
2073
+ */
2074
+ async installDependencies(repoPath) {
2075
+ repoPath = sanitizeRepoPath(repoPath);
2076
+ const projectType = this.detectProjectType(repoPath);
2077
+ this.projectType = projectType;
2078
+ this.repoPath = repoPath;
2079
+ let command;
2080
+ let args;
2081
+ switch (projectType.packageManager) {
2082
+ case "pnpm":
2083
+ command = "pnpm";
2084
+ args = ["install"];
2085
+ break;
2086
+ case "yarn":
2087
+ command = "yarn";
2088
+ args = ["install"];
2089
+ break;
2090
+ case "bun":
2091
+ command = "bun";
2092
+ args = ["install"];
2093
+ break;
2094
+ case "npm":
2095
+ command = "npm";
2096
+ args = ["install"];
2097
+ break;
2098
+ case "pip":
2099
+ command = "pip";
2100
+ args = ["install", "-r", "requirements.txt"];
2101
+ break;
2102
+ case "cargo":
2103
+ command = "cargo";
2104
+ args = ["build"];
2105
+ break;
2106
+ case "go":
2107
+ command = "go";
2108
+ args = ["mod", "download"];
2109
+ break;
2110
+ default:
2111
+ throw new ProjectRunnerError(`Unknown package manager: ${projectType.packageManager}`);
2112
+ }
2113
+ this.emit("install-start", { command, packageManager: projectType.packageManager });
2114
+ return new Promise((resolve2, reject) => {
2115
+ const proc = spawn(command, args, {
2116
+ cwd: repoPath,
2117
+ stdio: "pipe",
2118
+ timeout: 3e5
2119
+ // 5 min
2120
+ });
2121
+ let output = "";
2122
+ proc.stdout?.on("data", (data) => {
2123
+ const text = data.toString();
2124
+ output += text;
2125
+ this.emit("install-progress", { text });
2126
+ });
2127
+ proc.stderr?.on("data", (data) => {
2128
+ const text = data.toString();
2129
+ output += text;
2130
+ this.emit("install-progress", { text });
2131
+ });
2132
+ proc.on("close", (code) => {
2133
+ if (code === 0) {
2134
+ this.installed = true;
2135
+ this.emit("install-complete", { success: true });
2136
+ resolve2();
2137
+ } else {
2138
+ const err = new ProjectRunnerError(
2139
+ `Install failed (exit code ${code}): ${output.slice(-500)}`
2140
+ );
2141
+ this.emit("install-complete", { success: false, error: err.message });
2142
+ reject(err);
2143
+ }
2144
+ });
2145
+ proc.on("error", (err) => {
2146
+ reject(new ProjectRunnerError(`Failed to spawn ${command}: ${err.message}`));
2147
+ });
2148
+ });
2149
+ }
2150
+ /**
2151
+ * Start the project's dev server
2152
+ */
2153
+ async startDevServer(repoPath) {
2154
+ repoPath = sanitizeRepoPath(repoPath);
2155
+ const projectType = this.projectType || this.detectProjectType(repoPath);
2156
+ this.repoPath = repoPath;
2157
+ if (!projectType.devCommand) {
2158
+ throw new ProjectRunnerError("No dev command detected for this project");
2159
+ }
2160
+ const [cmd, ...args] = projectType.devCommand.split(" ");
2161
+ this.emit("server-starting", { command: projectType.devCommand });
2162
+ return new Promise((resolve2, reject) => {
2163
+ const proc = spawn(cmd, args, {
2164
+ cwd: repoPath,
2165
+ stdio: "pipe",
2166
+ detached: true
2167
+ });
2168
+ this.serverProcess = proc;
2169
+ let resolved = false;
2170
+ const readyPatterns = [
2171
+ /ready on\s+(https?:\/\/\S+)/i,
2172
+ /listening on\s+(https?:\/\/\S+)/i,
2173
+ /started on\s+(https?:\/\/\S+)/i,
2174
+ /Local:\s+(https?:\/\/\S+)/i,
2175
+ /http:\/\/localhost:(\d+)/,
2176
+ /http:\/\/127\.0\.0\.1:(\d+)/,
2177
+ /http:\/\/0\.0\.0\.0:(\d+)/,
2178
+ /ready in \d+/i,
2179
+ /compiled successfully/i
2180
+ ];
2181
+ const checkOutput = (text) => {
2182
+ if (resolved) return;
2183
+ for (const pattern of readyPatterns) {
2184
+ const match = text.match(pattern);
2185
+ if (match) {
2186
+ resolved = true;
2187
+ let url;
2188
+ if (match[1]?.startsWith("http")) {
2189
+ url = match[1];
2190
+ } else if (match[1]) {
2191
+ url = `http://localhost:${match[1]}`;
2192
+ } else if (projectType.port) {
2193
+ url = `http://localhost:${projectType.port}`;
2194
+ } else {
2195
+ url = "http://localhost:3000";
2196
+ }
2197
+ this.serverUrl = url;
2198
+ this.emit("server-ready", { url, pid: proc.pid });
2199
+ resolve2({ url, pid: proc.pid });
2200
+ return;
2201
+ }
2202
+ }
2203
+ };
2204
+ proc.stdout?.on("data", (data) => {
2205
+ checkOutput(data.toString());
2206
+ });
2207
+ proc.stderr?.on("data", (data) => {
2208
+ checkOutput(data.toString());
2209
+ });
2210
+ proc.on("error", (err) => {
2211
+ if (!resolved) {
2212
+ reject(new ProjectRunnerError(`Failed to start dev server: ${err.message}`));
2213
+ }
2214
+ });
2215
+ proc.on("close", (code) => {
2216
+ if (!resolved) {
2217
+ reject(new ProjectRunnerError(`Dev server exited with code ${code}`));
2218
+ }
2219
+ });
2220
+ setTimeout(async () => {
2221
+ if (resolved) return;
2222
+ const port = projectType.port || 3e3;
2223
+ const candidatePorts = [port, 5173, 8080, 4200, 3001];
2224
+ for (const p of candidatePorts) {
2225
+ try {
2226
+ const controller = new AbortController();
2227
+ const timeoutId = setTimeout(() => controller.abort(), 2e3);
2228
+ await fetch(`http://localhost:${p}`, { signal: controller.signal });
2229
+ clearTimeout(timeoutId);
2230
+ resolved = true;
2231
+ const url = `http://localhost:${p}`;
2232
+ this.serverUrl = url;
2233
+ this.emit("server-ready", { url, pid: proc.pid });
2234
+ resolve2({ url, pid: proc.pid });
2235
+ return;
2236
+ } catch {
2237
+ }
2238
+ }
2239
+ reject(new ProjectRunnerError("Dev server did not become ready within 60s"));
2240
+ }, 6e4);
2241
+ });
2242
+ }
2243
+ /**
2244
+ * Run the project's existing test suite
2245
+ */
2246
+ async runExistingTests(repoPath) {
2247
+ repoPath = sanitizeRepoPath(repoPath);
2248
+ const projectType = this.projectType || this.detectProjectType(repoPath);
2249
+ if (!projectType.testRunner) {
2250
+ throw new ProjectRunnerError("No test runner detected for this project");
2251
+ }
2252
+ let command;
2253
+ let args;
2254
+ switch (projectType.testRunner) {
2255
+ case "vitest":
2256
+ command = "npx";
2257
+ args = ["vitest", "run", "--reporter=json"];
2258
+ break;
2259
+ case "jest":
2260
+ command = "npx";
2261
+ args = ["jest", "--json", "--forceExit"];
2262
+ break;
2263
+ case "mocha":
2264
+ command = "npx";
2265
+ args = ["mocha", "--reporter", "json"];
2266
+ break;
2267
+ case "playwright":
2268
+ command = "npx";
2269
+ args = ["playwright", "test", "--reporter=json"];
2270
+ break;
2271
+ case "pytest":
2272
+ command = "python";
2273
+ args = ["-m", "pytest", "--tb=short", "-q"];
2274
+ break;
2275
+ case "go test":
2276
+ command = "go";
2277
+ args = ["test", "-json", "./..."];
2278
+ break;
2279
+ case "cargo test":
2280
+ command = "cargo";
2281
+ args = ["test", "--", "--format=json"];
2282
+ break;
2283
+ default:
2284
+ command = "npx";
2285
+ args = [projectType.testRunner, "--json"];
2286
+ }
2287
+ this.emit("test-start", { runner: projectType.testRunner });
2288
+ const startTime = Date.now();
2289
+ return new Promise((resolve2, reject) => {
2290
+ const proc = spawn(command, args, {
2291
+ cwd: repoPath,
2292
+ stdio: "pipe",
2293
+ timeout: 3e5
2294
+ });
2295
+ let stdout = "";
2296
+ let stderr = "";
2297
+ proc.stdout?.on("data", (data) => {
2298
+ stdout += data.toString();
2299
+ this.emit("test-progress", { text: data.toString() });
2300
+ });
2301
+ proc.stderr?.on("data", (data) => {
2302
+ stderr += data.toString();
2303
+ });
2304
+ proc.on("close", (code) => {
2305
+ const durationMs = Date.now() - startTime;
2306
+ const raw = stdout + "\n" + stderr;
2307
+ const result = this.parseTestResults(
2308
+ projectType.testRunner,
2309
+ stdout,
2310
+ stderr,
2311
+ durationMs
2312
+ );
2313
+ this.emit("test-complete", result);
2314
+ resolve2(result);
2315
+ });
2316
+ proc.on("error", (err) => {
2317
+ reject(new ProjectRunnerError(`Failed to run tests: ${err.message}`));
2318
+ });
2319
+ });
2320
+ }
2321
+ parseTestResults(runner, stdout, stderr, durationMs) {
2322
+ const raw = stdout + "\n" + stderr;
2323
+ if (runner === "vitest" || runner === "jest") {
2324
+ try {
2325
+ const jsonMatch = stdout.match(/\{[\s\S]*"numPassedTests"[\s\S]*\}/);
2326
+ if (jsonMatch) {
2327
+ const json = JSON.parse(jsonMatch[0]);
2328
+ return {
2329
+ runner,
2330
+ passed: json.numPassedTests || 0,
2331
+ failed: json.numFailedTests || 0,
2332
+ skipped: json.numPendingTests || 0,
2333
+ total: json.numTotalTests || 0,
2334
+ durationMs,
2335
+ raw
2336
+ };
2337
+ }
2338
+ } catch {
2339
+ }
2340
+ }
2341
+ let passed = 0, failed = 0, skipped = 0;
2342
+ const summaryMatch = raw.match(/(\d+)\s*passed.*?(\d+)\s*failed/i);
2343
+ if (summaryMatch) {
2344
+ passed = parseInt(summaryMatch[1]);
2345
+ failed = parseInt(summaryMatch[2]);
2346
+ }
2347
+ const passedMatch = raw.match(/(\d+)\s*pass(?:ed|ing)/i);
2348
+ if (passedMatch && !summaryMatch) passed = parseInt(passedMatch[1]);
2349
+ const failedMatch = raw.match(/(\d+)\s*fail(?:ed|ing|ure)/i);
2350
+ if (failedMatch && !summaryMatch) failed = parseInt(failedMatch[1]);
2351
+ const skippedMatch = raw.match(/(\d+)\s*(?:skip(?:ped)?|pending|todo)/i);
2352
+ if (skippedMatch) skipped = parseInt(skippedMatch[1]);
2353
+ return {
2354
+ runner,
2355
+ passed,
2356
+ failed,
2357
+ skipped,
2358
+ total: passed + failed + skipped,
2359
+ durationMs,
2360
+ raw
2361
+ };
2362
+ }
2363
+ /**
2364
+ * Stop the dev server and clean up
2365
+ */
2366
+ cleanup() {
2367
+ if (this.serverProcess) {
2368
+ try {
2369
+ if (this.serverProcess.pid) {
2370
+ process.kill(-this.serverProcess.pid, "SIGTERM");
2371
+ }
2372
+ } catch {
2373
+ try {
2374
+ this.serverProcess.kill("SIGTERM");
2375
+ } catch {
2376
+ }
2377
+ }
2378
+ this.serverProcess = null;
2379
+ this.serverUrl = null;
2380
+ this.emit("server-stopped");
2381
+ }
2382
+ }
2383
+ getStatus() {
2384
+ return {
2385
+ repoPath: this.repoPath || "",
2386
+ projectType: this.projectType,
2387
+ installed: this.installed,
2388
+ serverRunning: this.serverProcess !== null && !this.serverProcess.killed,
2389
+ serverUrl: this.serverUrl,
2390
+ serverPid: this.serverProcess?.pid || null
2391
+ };
2392
+ }
2393
+ };
2394
+
2395
+ // agent/notifications/index.ts
2396
+ init_esm_shims();
2397
+ var NotificationService = class {
2398
+ constructor(config) {
2399
+ this.config = config;
2400
+ }
2401
+ async notify(payload) {
2402
+ const promises = [];
2403
+ if (this.config.slack) {
2404
+ promises.push(this.sendSlack(payload));
2405
+ }
2406
+ if (this.config.discord) {
2407
+ promises.push(this.sendDiscord(payload));
2408
+ }
2409
+ await Promise.allSettled(promises);
2410
+ }
2411
+ async notifyBug(bug) {
2412
+ const severityEmoji = {
2413
+ critical: "\u{1F534}",
2414
+ high: "\u{1F7E0}",
2415
+ medium: "\u{1F7E1}",
2416
+ low: "\u{1F7E2}"
2417
+ };
2418
+ await this.notify({
2419
+ title: `${severityEmoji[bug.severity] || "\u26A0\uFE0F"} Bug Found: ${bug.title}`,
2420
+ message: bug.description,
2421
+ severity: bug.severity === "critical" || bug.severity === "high" ? "error" : "warning",
2422
+ fields: [
2423
+ { name: "Severity", value: bug.severity },
2424
+ { name: "Status", value: bug.status },
2425
+ ...bug.github_issue_url ? [{ name: "GitHub Issue", value: bug.github_issue_url }] : []
2426
+ ]
2427
+ });
2428
+ }
2429
+ async notifySessionComplete(stats) {
2430
+ await this.notify({
2431
+ title: "\u2705 OpenQA Session Complete",
2432
+ message: `Session ${stats.sessionId} finished.`,
2433
+ severity: "success",
2434
+ fields: [
2435
+ { name: "Tests Generated", value: String(stats.testsGenerated) },
2436
+ { name: "Agents Created", value: String(stats.agentsCreated) }
2437
+ ]
2438
+ });
2439
+ }
2440
+ async sendSlack(payload) {
2441
+ const colorMap = {
2442
+ info: "#36a64f",
2443
+ warning: "#ff9800",
2444
+ error: "#dc3545",
2445
+ success: "#28a745"
2446
+ };
2447
+ const color = colorMap[payload.severity || "info"];
2448
+ const body = {
2449
+ attachments: [{
2450
+ color,
2451
+ title: payload.title,
2452
+ text: payload.message,
2453
+ fields: payload.fields?.map((f) => ({ title: f.name, value: f.value, short: true })) || [],
2454
+ footer: "OpenQA",
2455
+ ts: Math.floor(Date.now() / 1e3)
2456
+ }]
2457
+ };
2458
+ const res = await fetch(this.config.slack, {
2459
+ method: "POST",
2460
+ headers: { "Content-Type": "application/json" },
2461
+ body: JSON.stringify(body)
2462
+ });
2463
+ if (!res.ok) {
2464
+ throw new Error(`Slack notification failed: ${res.status} ${res.statusText}`);
2465
+ }
2466
+ }
2467
+ async sendDiscord(payload) {
2468
+ const colorMap = {
2469
+ info: 3581519,
2470
+ warning: 16750592,
2471
+ error: 14431557,
2472
+ success: 2664261
2473
+ };
2474
+ const color = colorMap[payload.severity || "info"];
2475
+ const body = {
2476
+ embeds: [{
2477
+ title: payload.title,
2478
+ description: payload.message,
2479
+ color,
2480
+ fields: payload.fields?.map((f) => ({ name: f.name, value: f.value, inline: true })) || [],
2481
+ footer: { text: "OpenQA" },
2482
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2483
+ }]
2484
+ };
2485
+ const res = await fetch(this.config.discord, {
2486
+ method: "POST",
2487
+ headers: { "Content-Type": "application/json" },
2488
+ body: JSON.stringify(body)
2489
+ });
2490
+ if (!res.ok) {
2491
+ throw new Error(`Discord notification failed: ${res.status} ${res.statusText}`);
2492
+ }
2493
+ }
2494
+ isConfigured() {
2495
+ return !!(this.config.slack || this.config.discord);
2496
+ }
2497
+ };
2498
+
2499
+ // agent/index-v2.ts
2500
+ var OpenQAAgentV2 = class extends EventEmitter5 {
2501
+ db;
2502
+ config;
2503
+ saasConfigManager;
2504
+ brain = null;
2505
+ browserTools = null;
2506
+ gitListener = null;
2507
+ projectRunner;
2508
+ notifications = null;
2509
+ sessionId = "";
2510
+ isRunning = false;
2511
+ constructor(configPath) {
2512
+ super();
2513
+ this.config = new ConfigManager(configPath);
2514
+ const cfg = this.config.getConfigSync();
2515
+ this.db = new OpenQADatabase(cfg.database?.path || void 0);
2516
+ this.saasConfigManager = new SaaSConfigManager(this.db);
2517
+ this.projectRunner = new ProjectRunner();
2518
+ for (const event of ["install-start", "install-progress", "install-complete", "server-starting", "server-ready", "server-stopped", "test-start", "test-progress", "test-complete"]) {
2519
+ this.projectRunner.on(event, (data) => this.emit(event, data));
2520
+ }
2521
+ }
2522
+ /**
2523
+ * Configure the SaaS application to test
2524
+ * This is the main entry point for users
2525
+ */
2526
+ configureSaaS(config) {
2527
+ const saasConfig = {
2528
+ name: config.name,
2529
+ description: config.description,
2530
+ url: config.url,
2531
+ repoUrl: config.repoUrl,
2532
+ localPath: config.localPath,
2533
+ directives: config.directives,
2534
+ authInfo: config.auth ? {
2535
+ type: config.auth.type,
2536
+ testCredentials: config.auth.credentials
2537
+ } : { type: "none" }
2538
+ };
2539
+ return this.saasConfigManager.configure(saasConfig);
2540
+ }
2541
+ /**
2542
+ * Quick setup with minimal configuration
2543
+ */
2544
+ quickSetup(name, description, url) {
2545
+ return this.saasConfigManager.configure(
2546
+ createQuickConfig(name, description, url)
2547
+ );
2548
+ }
2549
+ /**
2550
+ * Add a directive (instruction for the agent)
2551
+ */
2552
+ addDirective(directive) {
2553
+ this.saasConfigManager.addDirective(directive);
2554
+ }
2555
+ /**
2556
+ * Set repository URL for code analysis
2557
+ */
2558
+ setRepository(url) {
2559
+ this.saasConfigManager.setRepoUrl(url);
2560
+ }
2561
+ /**
2562
+ * Set local path for code analysis
2563
+ */
2564
+ setLocalPath(path2) {
2565
+ this.saasConfigManager.setLocalPath(path2);
2566
+ }
2567
+ /**
2568
+ * Initialize the brain and start analyzing
2569
+ */
2570
+ async initialize() {
2571
+ const saasConfig = this.saasConfigManager.getConfig();
2572
+ if (!saasConfig) {
2573
+ throw new Error("SaaS not configured. Call configureSaaS() first.");
2574
+ }
2575
+ const cfg = await this.config.getConfig();
2576
+ this.sessionId = `session_${Date.now()}`;
2577
+ await this.db.createSession(this.sessionId, {
2578
+ saas: saasConfig,
2579
+ started_at: (/* @__PURE__ */ new Date()).toISOString()
2580
+ });
2581
+ if (cfg.notifications?.slack || cfg.notifications?.discord) {
2582
+ this.notifications = new NotificationService({
2583
+ slack: cfg.notifications.slack,
2584
+ discord: cfg.notifications.discord
2585
+ });
2586
+ }
2587
+ this.brain = new OpenQABrain(
2588
+ this.db,
2589
+ {
2590
+ provider: cfg.llm.provider,
2591
+ apiKey: cfg.llm.apiKey || process.env.OPENAI_API_KEY || "",
2592
+ model: cfg.llm.model
2593
+ },
2594
+ saasConfig
2595
+ );
2596
+ this.browserTools = new BrowserTools(this.db, this.sessionId);
2597
+ const log = logger.child({ session: this.sessionId, app: saasConfig.name });
2598
+ this.brain.on("test-generated", (test) => {
2599
+ this.emit("test-generated", test);
2600
+ log.info("Test generated", { name: test.name, type: test.type });
2601
+ });
2602
+ this.brain.on("agent-created", (agent) => {
2603
+ this.emit("agent-created", agent);
2604
+ log.info("Agent created", { name: agent.name });
2605
+ });
2606
+ this.brain.on("test-started", (test) => {
2607
+ this.emit("test-started", test);
2608
+ });
2609
+ this.brain.on("test-completed", (test) => {
2610
+ this.emit("test-completed", test);
2611
+ log.info("Test completed", { name: test.name, status: test.status });
2612
+ });
2613
+ this.brain.on("thinking", (thought) => {
2614
+ this.emit("thinking", thought);
2615
+ });
2616
+ this.brain.on("analysis-complete", (analysis) => {
2617
+ this.emit("analysis-complete", analysis);
2618
+ });
2619
+ this.brain.on("session-complete", (stats) => {
2620
+ this.emit("session-complete", stats);
2621
+ if (this.notifications) {
2622
+ this.notifications.notifySessionComplete({
2623
+ sessionId: this.sessionId,
2624
+ testsGenerated: Number(stats.testsGenerated ?? 0),
2625
+ agentsCreated: Number(stats.agentsCreated ?? 0)
2626
+ }).catch((e) => log.error("Notification failed", { error: e instanceof Error ? e.message : String(e) }));
2627
+ }
2628
+ });
2629
+ log.info("OpenQA initialized", {
2630
+ url: saasConfig.url,
2631
+ repoUrl: saasConfig.repoUrl,
2632
+ localPath: saasConfig.localPath,
2633
+ directives: saasConfig.directives?.length ?? 0
2634
+ });
2635
+ }
2636
+ /**
2637
+ * Run the brain in autonomous mode
2638
+ * The agent will analyze, think, generate tests, and execute them
2639
+ */
2640
+ async runAutonomous(maxIterations = 10) {
2641
+ if (!this.brain) {
2642
+ await this.initialize();
2643
+ }
2644
+ this.isRunning = true;
2645
+ logger.info("Starting autonomous QA session", { session: this.sessionId });
2646
+ await this.brain.runAutonomously(maxIterations);
2647
+ this.isRunning = false;
2648
+ }
2649
+ /**
2650
+ * Analyze the application and get suggestions
2651
+ */
2652
+ async analyze() {
2653
+ if (!this.brain) {
2654
+ await this.initialize();
2655
+ }
2656
+ return this.brain.analyze();
2657
+ }
2658
+ /**
2659
+ * Generate a specific test
2660
+ */
2661
+ async generateTest(type, target, context) {
2662
+ if (!this.brain) {
2663
+ await this.initialize();
2664
+ }
2665
+ return this.brain.generateTest(type, target, context);
2666
+ }
2667
+ /**
2668
+ * Create a custom agent for a specific purpose
2669
+ */
2670
+ async createAgent(purpose) {
2671
+ if (!this.brain) {
2672
+ await this.initialize();
2673
+ }
2674
+ return this.brain.createDynamicAgent(purpose);
2675
+ }
2676
+ /**
2677
+ * Execute a generated test
2678
+ */
2679
+ async runTest(testId) {
2680
+ if (!this.brain) {
2681
+ throw new Error("Brain not initialized");
2682
+ }
2683
+ return this.brain.executeTest(testId);
2684
+ }
2685
+ /**
2686
+ * Start listening for Git events (merges, pipelines)
2687
+ */
2688
+ async startGitListener() {
2689
+ const cfg = await this.config.getConfig();
2690
+ const saasConfig = this.saasConfigManager.getConfig();
2691
+ if (cfg.github?.token && cfg.github?.owner && cfg.github?.repo) {
2692
+ this.gitListener = new GitListener({
2693
+ provider: "github",
2694
+ token: cfg.github.token,
2695
+ owner: cfg.github.owner,
2696
+ repo: cfg.github.repo,
2697
+ branch: "main",
2698
+ pollIntervalMs: 6e4
2699
+ });
2700
+ this.gitListener.on("merge", async (event) => {
2701
+ logger.info("Merge detected", { branch: event.branch });
2702
+ this.emit("git-merge", event);
2703
+ await this.runAutonomous();
2704
+ });
2705
+ this.gitListener.on("pipeline-success", async (event) => {
2706
+ logger.info("Pipeline success");
2707
+ this.emit("git-pipeline-success", event);
2708
+ await this.runAutonomous();
2709
+ });
2710
+ await this.gitListener.start();
2711
+ logger.info("Git listener started");
2712
+ }
2713
+ }
2714
+ /**
2715
+ * Setup a project: detect, install deps, optionally start dev server
2716
+ */
2717
+ async setupProject(repoPath, options) {
2718
+ const projectType = this.projectRunner.detectProjectType(repoPath);
2719
+ logger.info("Project detected", { language: projectType.language, framework: projectType.framework, packageManager: projectType.packageManager });
2720
+ await this.projectRunner.installDependencies(repoPath);
2721
+ logger.info("Dependencies installed");
2722
+ if (options?.startServer && projectType.devCommand) {
2723
+ const { url } = await this.projectRunner.startDevServer(repoPath);
2724
+ logger.info("Dev server ready", { url });
2725
+ }
2726
+ return this.projectRunner.getStatus();
2727
+ }
2728
+ /**
2729
+ * Run the project's existing test suite
2730
+ */
2731
+ async runProjectTests(repoPath) {
2732
+ return this.projectRunner.runExistingTests(repoPath);
2733
+ }
2734
+ /**
2735
+ * Get project runner status
2736
+ */
2737
+ getProjectStatus() {
2738
+ return this.projectRunner.getStatus();
2739
+ }
2740
+ /**
2741
+ * Stop the agent
2742
+ */
2743
+ stop() {
2744
+ this.isRunning = false;
2745
+ if (this.gitListener) {
2746
+ this.gitListener.stop();
2747
+ this.gitListener = null;
2748
+ }
2749
+ if (this.browserTools) {
2750
+ this.browserTools.close();
2751
+ }
2752
+ this.projectRunner.cleanup();
2753
+ logger.info("OpenQA stopped");
2754
+ }
2755
+ /**
2756
+ * Get all generated tests
2757
+ */
2758
+ getTests() {
2759
+ return this.brain?.getGeneratedTests() || [];
2760
+ }
2761
+ /**
2762
+ * Get all created agents
2763
+ */
2764
+ getAgents() {
2765
+ return this.brain?.getDynamicAgents() || [];
2766
+ }
2767
+ /**
2768
+ * Get statistics
2769
+ */
2770
+ getStats() {
2771
+ return {
2772
+ isRunning: this.isRunning,
2773
+ sessionId: this.sessionId,
2774
+ saas: this.saasConfigManager.getConfig(),
2775
+ brain: this.brain?.getStats() || null,
2776
+ gitListenerActive: !!this.gitListener
2777
+ };
2778
+ }
2779
+ /**
2780
+ * Get the SaaS configuration
2781
+ */
2782
+ getSaaSConfig() {
2783
+ return this.saasConfigManager.getConfig();
2784
+ }
2785
+ /**
2786
+ * Check if configured
2787
+ */
2788
+ isConfigured() {
2789
+ return this.saasConfigManager.isConfigured();
2790
+ }
2791
+ };
2792
+ export {
2793
+ OpenQAAgentV2
2794
+ };
2795
+ //# sourceMappingURL=index-v2.js.map