@openqa/cli 1.0.13 → 1.1.1

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