@openqa/cli 1.3.4 → 2.1.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 (44) hide show
  1. package/README.md +203 -6
  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 +369 -105
  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 +8810 -0
  30. package/dist/cli/dashboard.html.js +1622 -0
  31. package/dist/cli/env-config.js +391 -0
  32. package/dist/cli/env-routes.js +820 -0
  33. package/dist/cli/env.html.js +679 -0
  34. package/dist/cli/index.js +5980 -1896
  35. package/dist/cli/kanban.html.js +577 -0
  36. package/dist/cli/routes.js +895 -0
  37. package/dist/cli/routes.js.map +1 -0
  38. package/dist/cli/server.js +5855 -1860
  39. package/dist/database/index.js +485 -60
  40. package/dist/database/index.js.map +1 -1
  41. package/dist/database/sqlite.js +281 -0
  42. package/dist/database/sqlite.js.map +1 -0
  43. package/install.sh +19 -10
  44. package/package.json +19 -5
@@ -0,0 +1,895 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropNames = Object.getOwnPropertyNames;
3
+ var __esm = (fn, res) => function __init() {
4
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
5
+ };
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+
11
+ // node_modules/tsup/assets/esm_shims.js
12
+ import path from "path";
13
+ import { fileURLToPath } from "url";
14
+ var init_esm_shims = __esm({
15
+ "node_modules/tsup/assets/esm_shims.js"() {
16
+ "use strict";
17
+ }
18
+ });
19
+
20
+ // agent/coverage/index.ts
21
+ var coverage_exports = {};
22
+ __export(coverage_exports, {
23
+ CoverageTracker: () => CoverageTracker
24
+ });
25
+ var CoverageTracker;
26
+ var init_coverage = __esm({
27
+ "agent/coverage/index.ts"() {
28
+ "use strict";
29
+ init_esm_shims();
30
+ CoverageTracker = class {
31
+ routes = /* @__PURE__ */ new Map();
32
+ /**
33
+ * Register a known route (from code analysis or sitemap)
34
+ */
35
+ registerRoute(method, url) {
36
+ const key = `${method.toUpperCase()} ${url}`;
37
+ if (!this.routes.has(key)) {
38
+ this.routes.set(key, { url, method: method.toUpperCase(), tested: false, testCount: 0 });
39
+ }
40
+ }
41
+ /**
42
+ * Record that a route was exercised during a test session
43
+ */
44
+ recordTest(method, url) {
45
+ const key = `${method.toUpperCase()} ${url}`;
46
+ const existing = this.routes.get(key);
47
+ if (existing) {
48
+ existing.tested = true;
49
+ existing.lastTestedAt = (/* @__PURE__ */ new Date()).toISOString();
50
+ existing.testCount++;
51
+ } else {
52
+ this.routes.set(key, {
53
+ url,
54
+ method: method.toUpperCase(),
55
+ tested: true,
56
+ lastTestedAt: (/* @__PURE__ */ new Date()).toISOString(),
57
+ testCount: 1
58
+ });
59
+ }
60
+ }
61
+ /**
62
+ * Build a coverage report
63
+ */
64
+ getReport() {
65
+ const entries = Array.from(this.routes.values());
66
+ const tested = entries.filter((e) => e.tested).length;
67
+ const total = entries.length;
68
+ return {
69
+ total,
70
+ tested,
71
+ untested: total - tested,
72
+ coveragePercent: total === 0 ? 0 : Math.round(tested / total * 100),
73
+ entries: entries.sort((a, b) => a.tested === b.tested ? 0 : a.tested ? 1 : -1)
74
+ };
75
+ }
76
+ /**
77
+ * Populate routes from an OpenAPI-style spec or action history
78
+ */
79
+ async loadFromDatabase(db, sessionId) {
80
+ const actions = await db.getSessionActions(sessionId);
81
+ for (const action of actions) {
82
+ if (action.type === "navigate" || action.type === "api_call" || action.type === "request") {
83
+ const urlMatch = (action.input || action.description).match(/(GET|POST|PUT|DELETE|PATCH|HEAD)\s+(https?:\/\/[^\s]+|\/[^\s]*)/i);
84
+ if (urlMatch) {
85
+ this.recordTest(urlMatch[1], urlMatch[2]);
86
+ } else {
87
+ const simpleUrl = (action.input || "").match(/https?:\/\/[^\s]+/);
88
+ if (simpleUrl) {
89
+ this.recordTest("GET", simpleUrl[0]);
90
+ }
91
+ }
92
+ }
93
+ }
94
+ }
95
+ reset() {
96
+ this.routes.clear();
97
+ }
98
+ };
99
+ }
100
+ });
101
+
102
+ // cli/routes.ts
103
+ init_esm_shims();
104
+ import { Router } from "express";
105
+ import { z } from "zod";
106
+
107
+ // agent/export/index.ts
108
+ init_esm_shims();
109
+ var ExportService = class {
110
+ constructor(db) {
111
+ this.db = db;
112
+ }
113
+ async exportSession(sessionId, format) {
114
+ const session = await this.db.getSession(sessionId);
115
+ if (!session) {
116
+ throw new Error(`Session ${sessionId} not found`);
117
+ }
118
+ const actions = await this.db.getSessionActions(sessionId);
119
+ const allBugs = await this.db.getAllBugs();
120
+ const bugs = allBugs.filter((b) => b.session_id === sessionId);
121
+ const data = { session, actions, bugs };
122
+ switch (format) {
123
+ case "json":
124
+ return {
125
+ content: JSON.stringify(data, null, 2),
126
+ contentType: "application/json",
127
+ filename: `openqa-session-${sessionId}.json`
128
+ };
129
+ case "csv":
130
+ return {
131
+ content: this.toCSV(data),
132
+ contentType: "text/csv",
133
+ filename: `openqa-session-${sessionId}.csv`
134
+ };
135
+ case "html":
136
+ return {
137
+ content: this.toHTML(data),
138
+ contentType: "text/html",
139
+ filename: `openqa-session-${sessionId}.html`
140
+ };
141
+ }
142
+ }
143
+ toCSV(data) {
144
+ const lines = [];
145
+ lines.push("# Session Summary");
146
+ lines.push("id,started_at,ended_at,status,total_actions,bugs_found");
147
+ lines.push([
148
+ data.session.id,
149
+ data.session.started_at,
150
+ data.session.ended_at || "",
151
+ data.session.status,
152
+ data.session.total_actions,
153
+ data.session.bugs_found
154
+ ].join(","));
155
+ lines.push("");
156
+ lines.push("# Actions");
157
+ lines.push("id,timestamp,type,description");
158
+ for (const a of data.actions) {
159
+ lines.push([
160
+ a.id,
161
+ a.timestamp,
162
+ this.csvEscape(a.type),
163
+ this.csvEscape(a.description)
164
+ ].join(","));
165
+ }
166
+ lines.push("");
167
+ lines.push("# Bugs");
168
+ lines.push("id,title,severity,status,created_at");
169
+ for (const b of data.bugs) {
170
+ lines.push([
171
+ b.id,
172
+ this.csvEscape(b.title),
173
+ b.severity,
174
+ b.status,
175
+ b.created_at
176
+ ].join(","));
177
+ }
178
+ return lines.join("\n");
179
+ }
180
+ csvEscape(value) {
181
+ if (value.includes(",") || value.includes('"') || value.includes("\n")) {
182
+ return `"${value.replace(/"/g, '""')}"`;
183
+ }
184
+ return value;
185
+ }
186
+ toHTML(data) {
187
+ const severityColor = {
188
+ critical: "#dc2626",
189
+ high: "#ea580c",
190
+ medium: "#ca8a04",
191
+ low: "#16a34a"
192
+ };
193
+ return `<!DOCTYPE html>
194
+ <html lang="en">
195
+ <head>
196
+ <meta charset="UTF-8">
197
+ <title>OpenQA Report \u2014 Session ${data.session.id}</title>
198
+ <style>
199
+ * { margin: 0; padding: 0; box-sizing: border-box; }
200
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; max-width: 960px; margin: 0 auto; padding: 2rem; color: #1a1a2e; }
201
+ h1 { font-size: 1.5rem; margin-bottom: 0.5rem; }
202
+ h2 { font-size: 1.2rem; margin: 2rem 0 0.5rem; border-bottom: 2px solid #e2e8f0; padding-bottom: 0.25rem; }
203
+ .meta { color: #64748b; font-size: 0.9rem; margin-bottom: 1.5rem; }
204
+ .stats { display: flex; gap: 1.5rem; margin-bottom: 1.5rem; }
205
+ .stat { background: #f8fafc; border-radius: 8px; padding: 1rem; flex: 1; text-align: center; }
206
+ .stat-value { font-size: 1.5rem; font-weight: 700; }
207
+ .stat-label { color: #64748b; font-size: 0.8rem; }
208
+ table { width: 100%; border-collapse: collapse; margin-top: 0.5rem; }
209
+ th, td { text-align: left; padding: 0.5rem; border-bottom: 1px solid #e2e8f0; font-size: 0.85rem; }
210
+ th { background: #f8fafc; font-weight: 600; }
211
+ .badge { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 0.75rem; color: white; }
212
+ .footer { margin-top: 3rem; color: #94a3b8; font-size: 0.8rem; text-align: center; }
213
+ </style>
214
+ </head>
215
+ <body>
216
+ <h1>OpenQA Session Report</h1>
217
+ <p class="meta">${data.session.id} &mdash; ${data.session.started_at}${data.session.ended_at ? ` to ${data.session.ended_at}` : " (in progress)"}</p>
218
+
219
+ <div class="stats">
220
+ <div class="stat"><div class="stat-value">${data.session.total_actions}</div><div class="stat-label">Actions</div></div>
221
+ <div class="stat"><div class="stat-value">${data.bugs.length}</div><div class="stat-label">Bugs Found</div></div>
222
+ <div class="stat"><div class="stat-value">${data.session.status}</div><div class="stat-label">Status</div></div>
223
+ <div class="stat"><div class="stat-value">${data.session.total_actions > 0 ? Math.round((data.session.total_actions - data.session.bugs_found) / data.session.total_actions * 100) : 0}%</div><div class="stat-label">Success Rate</div></div>
224
+ </div>
225
+
226
+ ${data.bugs.length > 0 ? `
227
+ <h2>Bugs (${data.bugs.length})</h2>
228
+ <table>
229
+ <thead><tr><th>Title</th><th>Severity</th><th>Status</th><th>Date</th></tr></thead>
230
+ <tbody>
231
+ ${data.bugs.map((b) => `<tr>
232
+ <td>${this.htmlEscape(b.title)}</td>
233
+ <td><span class="badge" style="background:${severityColor[b.severity] || "#64748b"}">${b.severity}</span></td>
234
+ <td>${b.status}</td>
235
+ <td>${b.created_at}</td>
236
+ </tr>`).join("\n ")}
237
+ </tbody>
238
+ </table>` : ""}
239
+
240
+ ${data.actions.length > 0 ? `
241
+ <h2>Actions (${data.actions.length})</h2>
242
+ <table>
243
+ <thead><tr><th>Type</th><th>Description</th><th>Timestamp</th></tr></thead>
244
+ <tbody>
245
+ ${data.actions.slice(0, 100).map((a) => `<tr>
246
+ <td>${this.htmlEscape(a.type)}</td>
247
+ <td>${this.htmlEscape(a.description)}</td>
248
+ <td>${a.timestamp}</td>
249
+ </tr>`).join("\n ")}
250
+ </tbody>
251
+ </table>
252
+ ${data.actions.length > 100 ? `<p class="meta">&hellip; and ${data.actions.length - 100} more actions</p>` : ""}` : ""}
253
+
254
+ <p class="footer">Generated by OpenQA on ${(/* @__PURE__ */ new Date()).toISOString()}</p>
255
+ </body>
256
+ </html>`;
257
+ }
258
+ htmlEscape(str) {
259
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
260
+ }
261
+ };
262
+
263
+ // agent/openapi/spec.ts
264
+ init_esm_shims();
265
+ function getOpenAPISpec() {
266
+ return {
267
+ openapi: "3.0.3",
268
+ info: {
269
+ title: "OpenQA API",
270
+ description: "Autonomous QA testing agent \u2014 REST API",
271
+ version: "1.3.4",
272
+ contact: { url: "https://openqa.orkajs.com" },
273
+ license: { name: "MIT" }
274
+ },
275
+ servers: [{ url: "/api", description: "OpenQA server" }],
276
+ tags: [
277
+ { name: "health", description: "Health & status" },
278
+ { name: "sessions", description: "Test sessions" },
279
+ { name: "bugs", description: "Bug reports" },
280
+ { name: "kanban", description: "Kanban board" },
281
+ { name: "config", description: "Configuration" },
282
+ { name: "agent", description: "Agent control" },
283
+ { name: "project", description: "Project runner" },
284
+ { name: "export", description: "Result export" },
285
+ { name: "coverage", description: "Test coverage" },
286
+ { name: "storage", description: "Storage management" }
287
+ ],
288
+ paths: {
289
+ "/health": {
290
+ get: {
291
+ tags: ["health"],
292
+ summary: "Health check",
293
+ responses: {
294
+ "200": {
295
+ description: "Service is healthy",
296
+ content: { "application/json": { schema: { $ref: "#/components/schemas/Health" } } }
297
+ }
298
+ }
299
+ }
300
+ },
301
+ "/status": {
302
+ get: {
303
+ tags: ["health"],
304
+ summary: "Agent status",
305
+ responses: {
306
+ "200": {
307
+ description: "Agent running state",
308
+ content: { "application/json": { schema: { $ref: "#/components/schemas/Status" } } }
309
+ }
310
+ }
311
+ }
312
+ },
313
+ "/sessions": {
314
+ get: {
315
+ tags: ["sessions"],
316
+ summary: "List recent sessions",
317
+ parameters: [{ name: "limit", in: "query", schema: { type: "integer", default: 10 } }],
318
+ responses: {
319
+ "200": {
320
+ description: "Array of sessions",
321
+ content: { "application/json": { schema: { type: "array", items: { $ref: "#/components/schemas/Session" } } } }
322
+ }
323
+ }
324
+ }
325
+ },
326
+ "/sessions/{id}/actions": {
327
+ get: {
328
+ tags: ["sessions"],
329
+ summary: "Get actions for a session",
330
+ parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
331
+ responses: {
332
+ "200": {
333
+ description: "Array of actions",
334
+ content: { "application/json": { schema: { type: "array", items: { $ref: "#/components/schemas/Action" } } } }
335
+ }
336
+ }
337
+ }
338
+ },
339
+ "/bugs": {
340
+ get: {
341
+ tags: ["bugs"],
342
+ summary: "List bugs",
343
+ parameters: [{ name: "status", in: "query", schema: { type: "string", enum: ["open", "in-progress", "resolved", "closed"] } }],
344
+ responses: {
345
+ "200": {
346
+ description: "Array of bugs",
347
+ content: { "application/json": { schema: { type: "array", items: { $ref: "#/components/schemas/Bug" } } } }
348
+ }
349
+ }
350
+ }
351
+ },
352
+ "/kanban": {
353
+ get: {
354
+ tags: ["kanban"],
355
+ summary: "List all kanban tickets",
356
+ responses: {
357
+ "200": {
358
+ description: "Array of tickets",
359
+ content: { "application/json": { schema: { type: "array", items: { $ref: "#/components/schemas/KanbanTicket" } } } }
360
+ }
361
+ }
362
+ },
363
+ post: {
364
+ tags: ["kanban"],
365
+ summary: "Create a kanban ticket",
366
+ requestBody: {
367
+ required: true,
368
+ content: { "application/json": { schema: { $ref: "#/components/schemas/KanbanTicketInput" } } }
369
+ },
370
+ responses: {
371
+ "200": { description: "Created ticket", content: { "application/json": { schema: { $ref: "#/components/schemas/KanbanTicket" } } } }
372
+ }
373
+ }
374
+ },
375
+ "/kanban/{id}": {
376
+ put: {
377
+ tags: ["kanban"],
378
+ summary: "Update a kanban ticket",
379
+ parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
380
+ requestBody: { required: true, content: { "application/json": { schema: { $ref: "#/components/schemas/KanbanTicketInput" } } } },
381
+ responses: { "200": { description: "Success", content: { "application/json": { schema: { $ref: "#/components/schemas/SuccessResponse" } } } } }
382
+ },
383
+ delete: {
384
+ tags: ["kanban"],
385
+ summary: "Delete a kanban ticket",
386
+ parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
387
+ responses: { "200": { description: "Success", content: { "application/json": { schema: { $ref: "#/components/schemas/SuccessResponse" } } } } }
388
+ }
389
+ },
390
+ "/config": {
391
+ get: {
392
+ tags: ["config"],
393
+ summary: "Get current configuration",
394
+ responses: { "200": { description: "Configuration object" } }
395
+ },
396
+ post: {
397
+ tags: ["config"],
398
+ summary: "Update configuration",
399
+ requestBody: { required: true, content: { "application/json": { schema: { type: "object" } } } },
400
+ responses: { "200": { description: "Success", content: { "application/json": { schema: { $ref: "#/components/schemas/SuccessResponse" } } } } }
401
+ }
402
+ },
403
+ "/agent/start": {
404
+ post: {
405
+ tags: ["agent"],
406
+ summary: "Start autonomous agent session",
407
+ responses: { "200": { description: "Started", content: { "application/json": { schema: { $ref: "#/components/schemas/SuccessResponse" } } } } }
408
+ }
409
+ },
410
+ "/agent/stop": {
411
+ post: {
412
+ tags: ["agent"],
413
+ summary: "Stop the running agent",
414
+ responses: { "200": { description: "Stopped", content: { "application/json": { schema: { $ref: "#/components/schemas/SuccessResponse" } } } } }
415
+ }
416
+ },
417
+ "/project/setup": {
418
+ post: {
419
+ tags: ["project"],
420
+ summary: "Setup project \u2014 detect, install deps, optionally start dev server",
421
+ requestBody: {
422
+ required: true,
423
+ content: {
424
+ "application/json": {
425
+ schema: {
426
+ type: "object",
427
+ required: ["repoPath"],
428
+ properties: {
429
+ repoPath: { type: "string", description: "Absolute path to the project" },
430
+ startServer: { type: "boolean", description: "Start dev server after install" }
431
+ }
432
+ }
433
+ }
434
+ }
435
+ },
436
+ responses: { "200": { description: "Project status", content: { "application/json": { schema: { $ref: "#/components/schemas/ProjectStatus" } } } } }
437
+ }
438
+ },
439
+ "/project/status": {
440
+ get: {
441
+ tags: ["project"],
442
+ summary: "Get project runner status",
443
+ responses: { "200": { description: "Status", content: { "application/json": { schema: { $ref: "#/components/schemas/ProjectStatus" } } } } }
444
+ }
445
+ },
446
+ "/project/test": {
447
+ post: {
448
+ tags: ["project"],
449
+ summary: "Run existing project tests",
450
+ requestBody: {
451
+ required: true,
452
+ content: { "application/json": { schema: { type: "object", required: ["repoPath"], properties: { repoPath: { type: "string" } } } } }
453
+ },
454
+ responses: { "200": { description: "Test results", content: { "application/json": { schema: { $ref: "#/components/schemas/TestRunResult" } } } } }
455
+ }
456
+ },
457
+ "/export/{sessionId}": {
458
+ get: {
459
+ tags: ["export"],
460
+ summary: "Export session results",
461
+ parameters: [
462
+ { name: "sessionId", in: "path", required: true, schema: { type: "string" } },
463
+ { name: "format", in: "query", schema: { type: "string", enum: ["json", "csv", "html"], default: "json" } }
464
+ ],
465
+ responses: {
466
+ "200": { description: "File download (json / csv / html)" },
467
+ "404": { description: "Session not found" }
468
+ }
469
+ }
470
+ },
471
+ "/coverage/{sessionId}": {
472
+ get: {
473
+ tags: ["coverage"],
474
+ summary: "Get coverage report for a session",
475
+ parameters: [{ name: "sessionId", in: "path", required: true, schema: { type: "string" } }],
476
+ responses: { "200": { description: "Coverage report", content: { "application/json": { schema: { $ref: "#/components/schemas/CoverageReport" } } } } }
477
+ }
478
+ },
479
+ "/storage": {
480
+ get: {
481
+ tags: ["storage"],
482
+ summary: "Get storage statistics",
483
+ responses: { "200": { description: "Storage stats" } }
484
+ }
485
+ },
486
+ "/cleanup": {
487
+ post: {
488
+ tags: ["storage"],
489
+ summary: "Prune old sessions",
490
+ parameters: [{ name: "maxAgeDays", in: "query", schema: { type: "integer", default: 30 } }],
491
+ responses: { "200": { description: "Cleanup result" } }
492
+ }
493
+ }
494
+ },
495
+ components: {
496
+ schemas: {
497
+ Health: {
498
+ type: "object",
499
+ properties: { status: { type: "string" }, uptime: { type: "number" }, version: { type: "string" } }
500
+ },
501
+ Status: {
502
+ type: "object",
503
+ properties: { isRunning: { type: "boolean" }, sessionId: { type: "string" } }
504
+ },
505
+ Session: {
506
+ type: "object",
507
+ properties: {
508
+ id: { type: "string" },
509
+ started_at: { type: "string", format: "date-time" },
510
+ ended_at: { type: "string", format: "date-time", nullable: true },
511
+ status: { type: "string", enum: ["running", "completed", "failed"] },
512
+ total_actions: { type: "integer" },
513
+ bugs_found: { type: "integer" }
514
+ }
515
+ },
516
+ Action: {
517
+ type: "object",
518
+ properties: {
519
+ id: { type: "string" },
520
+ session_id: { type: "string" },
521
+ timestamp: { type: "string", format: "date-time" },
522
+ type: { type: "string" },
523
+ description: { type: "string" }
524
+ }
525
+ },
526
+ Bug: {
527
+ type: "object",
528
+ properties: {
529
+ id: { type: "string" },
530
+ session_id: { type: "string" },
531
+ title: { type: "string" },
532
+ description: { type: "string" },
533
+ severity: { type: "string", enum: ["low", "medium", "high", "critical"] },
534
+ status: { type: "string", enum: ["open", "in-progress", "resolved", "closed"] },
535
+ github_issue_url: { type: "string", nullable: true },
536
+ created_at: { type: "string", format: "date-time" }
537
+ }
538
+ },
539
+ KanbanTicket: {
540
+ type: "object",
541
+ properties: {
542
+ id: { type: "string" },
543
+ title: { type: "string" },
544
+ description: { type: "string" },
545
+ priority: { type: "string", enum: ["low", "medium", "high", "critical"] },
546
+ column: { type: "string", enum: ["backlog", "to-do", "in-progress", "done"] },
547
+ created_at: { type: "string", format: "date-time" }
548
+ }
549
+ },
550
+ KanbanTicketInput: {
551
+ type: "object",
552
+ properties: {
553
+ title: { type: "string" },
554
+ description: { type: "string" },
555
+ priority: { type: "string", enum: ["low", "medium", "high", "critical"] },
556
+ column: { type: "string", enum: ["backlog", "to-do", "in-progress", "done"] }
557
+ }
558
+ },
559
+ ProjectStatus: {
560
+ type: "object",
561
+ properties: {
562
+ repoPath: { type: "string" },
563
+ installed: { type: "boolean" },
564
+ serverRunning: { type: "boolean" },
565
+ serverUrl: { type: "string", nullable: true },
566
+ serverPid: { type: "integer", nullable: true }
567
+ }
568
+ },
569
+ TestRunResult: {
570
+ type: "object",
571
+ properties: {
572
+ runner: { type: "string" },
573
+ passed: { type: "integer" },
574
+ failed: { type: "integer" },
575
+ skipped: { type: "integer" },
576
+ total: { type: "integer" },
577
+ durationMs: { type: "integer" }
578
+ }
579
+ },
580
+ CoverageReport: {
581
+ type: "object",
582
+ properties: {
583
+ total: { type: "integer" },
584
+ tested: { type: "integer" },
585
+ untested: { type: "integer" },
586
+ coveragePercent: { type: "integer" },
587
+ entries: { type: "array", items: { type: "object", properties: { url: { type: "string" }, method: { type: "string" }, tested: { type: "boolean" }, testCount: { type: "integer" } } } }
588
+ }
589
+ },
590
+ SuccessResponse: {
591
+ type: "object",
592
+ properties: { success: { type: "boolean" } }
593
+ }
594
+ }
595
+ }
596
+ };
597
+ }
598
+
599
+ // agent/metrics.ts
600
+ init_esm_shims();
601
+ var startedAt = Date.now();
602
+ var counters = {
603
+ llm_calls: 0,
604
+ llm_cache_hits: 0,
605
+ llm_retries: 0,
606
+ llm_fallbacks: 0,
607
+ llm_circuit_opens: 0,
608
+ tests_generated: 0,
609
+ tests_run: 0,
610
+ tests_passed: 0,
611
+ tests_failed: 0,
612
+ bugs_found: 0,
613
+ sessions_started: 0,
614
+ ws_connections: 0,
615
+ http_requests: 0
616
+ };
617
+ var metrics = {
618
+ inc(key, by = 1) {
619
+ if (key in counters) counters[key] += by;
620
+ },
621
+ snapshot() {
622
+ const memMB = process.memoryUsage();
623
+ return {
624
+ uptimeSeconds: Math.floor((Date.now() - startedAt) / 1e3),
625
+ memory: {
626
+ heapUsedMB: Math.round(memMB.heapUsed / 1024 / 1024),
627
+ heapTotalMB: Math.round(memMB.heapTotal / 1024 / 1024),
628
+ rssMB: Math.round(memMB.rss / 1024 / 1024)
629
+ },
630
+ counters: { ...counters },
631
+ cacheHitRate: counters.llm_calls > 0 ? Math.round(counters.llm_cache_hits / (counters.llm_calls + counters.llm_cache_hits) * 100) : 0
632
+ };
633
+ }
634
+ };
635
+
636
+ // cli/routes.ts
637
+ function validate(schema) {
638
+ return (req, res, next) => {
639
+ const result = schema.safeParse(req.body);
640
+ if (!result.success) {
641
+ return res.status(400).json({
642
+ error: result.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join(", ")
643
+ });
644
+ }
645
+ req.body = result.data;
646
+ next();
647
+ };
648
+ }
649
+ var kanbanCreateSchema = z.object({
650
+ title: z.string().min(1).max(200).optional(),
651
+ description: z.string().max(2e3).optional(),
652
+ priority: z.enum(["low", "medium", "high", "critical"]).optional(),
653
+ column: z.enum(["backlog", "todo", "in_progress", "done"]).optional()
654
+ });
655
+ var kanbanUpdateSchema = z.object({
656
+ title: z.string().min(1).max(200).optional(),
657
+ description: z.string().max(2e3).optional(),
658
+ priority: z.enum(["low", "medium", "high", "critical"]).optional(),
659
+ column: z.enum(["backlog", "todo", "in_progress", "done"]).optional(),
660
+ assignee: z.string().max(100).optional(),
661
+ tags: z.array(z.string().max(50)).max(20).optional()
662
+ }).loose();
663
+ var configUpdateSchema = z.record(
664
+ z.string(),
665
+ z.union([z.string(), z.number(), z.boolean(), z.record(z.string(), z.unknown())])
666
+ );
667
+ var exportFormatSchema = z.enum(["json", "csv", "html"]);
668
+ function createApiRouter(db, config) {
669
+ const router = Router();
670
+ router.use((_req, _res, next) => {
671
+ metrics.inc("http_requests");
672
+ next();
673
+ });
674
+ router.get("/health", async (_req, res) => {
675
+ const snap = metrics.snapshot();
676
+ let dbOk = false;
677
+ try {
678
+ await db.getStorageStats();
679
+ dbOk = true;
680
+ } catch {
681
+ }
682
+ res.json({
683
+ status: dbOk ? "ok" : "degraded",
684
+ version: "1.3.4",
685
+ uptime: snap.uptimeSeconds,
686
+ memory: snap.memory,
687
+ db: dbOk
688
+ });
689
+ });
690
+ router.get("/api/metrics", (_req, res) => {
691
+ res.json(metrics.snapshot());
692
+ });
693
+ router.get("/api/status", (_req, res) => {
694
+ res.json({ isRunning: true });
695
+ });
696
+ router.get("/api/sessions", async (req, res) => {
697
+ const limit = parseInt(req.query.limit) || 10;
698
+ const sessions = await db.getRecentSessions(limit);
699
+ res.json(sessions);
700
+ });
701
+ router.get("/api/sessions/:id/actions", async (req, res) => {
702
+ const actions = await db.getSessionActions(req.params.id);
703
+ res.json(actions);
704
+ });
705
+ router.get("/api/bugs", async (req, res) => {
706
+ const status = req.query.status;
707
+ const bugs = status ? await db.getBugsByStatus(status) : await db.getAllBugs();
708
+ res.json(bugs);
709
+ });
710
+ router.get("/api/kanban/tickets", async (req, res) => {
711
+ const column = req.query.column;
712
+ const tickets = column ? await db.getKanbanTicketsByColumn(column) : await db.getKanbanTickets();
713
+ res.json(tickets);
714
+ });
715
+ router.get("/api/kanban", async (_req, res) => {
716
+ const tickets = await db.getKanbanTickets();
717
+ res.json(tickets);
718
+ });
719
+ router.post("/api/kanban", validate(kanbanCreateSchema), async (req, res) => {
720
+ try {
721
+ const { title, description, priority, column } = req.body;
722
+ const ticket = await db.createKanbanTicket({
723
+ title: title || "New Ticket",
724
+ description: description || "",
725
+ priority: priority || "medium",
726
+ column: column || "backlog"
727
+ });
728
+ res.json(ticket);
729
+ } catch (error) {
730
+ res.status(500).json({ error: error instanceof Error ? error.message : String(error) });
731
+ }
732
+ });
733
+ router.put("/api/kanban/:id", validate(kanbanUpdateSchema), async (req, res) => {
734
+ try {
735
+ const { id } = req.params;
736
+ const updates = req.body;
737
+ await db.updateKanbanTicket(id, updates);
738
+ res.json({ success: true });
739
+ } catch (error) {
740
+ res.status(500).json({ error: error instanceof Error ? error.message : String(error) });
741
+ }
742
+ });
743
+ router.delete("/api/kanban/:id", async (req, res) => {
744
+ try {
745
+ const { id } = req.params;
746
+ await db.deleteKanbanTicket(id);
747
+ res.json({ success: true });
748
+ } catch (error) {
749
+ res.status(500).json({ error: error instanceof Error ? error.message : String(error) });
750
+ }
751
+ });
752
+ router.patch("/api/kanban/tickets/:id", validate(kanbanUpdateSchema), async (req, res) => {
753
+ const { id } = req.params;
754
+ const updates = req.body;
755
+ await db.updateKanbanTicket(id, updates);
756
+ res.json({ success: true });
757
+ });
758
+ router.get("/api/config", async (_req, res) => {
759
+ const cfg = await config.getConfig();
760
+ res.json(JSON.parse(JSON.stringify(cfg)));
761
+ });
762
+ router.post("/api/config", validate(configUpdateSchema), async (req, res) => {
763
+ try {
764
+ const configData = req.body;
765
+ for (const [section, values] of Object.entries(configData)) {
766
+ if (typeof values === "object" && values !== null) {
767
+ for (const [key, value] of Object.entries(values)) {
768
+ await config.set(`${section}.${key}`, String(value));
769
+ }
770
+ } else {
771
+ await config.set(section, String(values));
772
+ }
773
+ }
774
+ res.json({ success: true });
775
+ } catch (error) {
776
+ res.status(500).json({ success: false, error: error instanceof Error ? error.message : String(error) });
777
+ }
778
+ });
779
+ router.post("/api/config/reset", async (_req, res) => {
780
+ try {
781
+ await db.clearAllConfig();
782
+ res.json({ success: true });
783
+ } catch (error) {
784
+ res.status(500).json({ success: false, error: error instanceof Error ? error.message : String(error) });
785
+ }
786
+ });
787
+ router.post("/api/test-connection", async (req, res) => {
788
+ const urlStr = req.body.url;
789
+ if (!urlStr) {
790
+ return res.status(400).json({ success: false, error: "url is required" });
791
+ }
792
+ try {
793
+ const controller = new AbortController();
794
+ const timeoutId = setTimeout(() => controller.abort(), 5e3);
795
+ const response = await fetch(urlStr, {
796
+ method: "HEAD",
797
+ signal: controller.signal,
798
+ redirect: "follow"
799
+ });
800
+ clearTimeout(timeoutId);
801
+ res.json({ success: true, status: response.status, ok: response.ok });
802
+ } catch (err) {
803
+ const message = err instanceof Error ? err.message : String(err);
804
+ res.status(200).json({ success: false, error: message });
805
+ }
806
+ });
807
+ router.get("/api/openapi.json", (_req, res) => {
808
+ res.json(getOpenAPISpec());
809
+ });
810
+ router.get("/api/docs", (_req, res) => {
811
+ const spec = JSON.stringify(getOpenAPISpec());
812
+ res.setHeader("Content-Type", "text/html");
813
+ res.send(`<!DOCTYPE html>
814
+ <html lang="en">
815
+ <head>
816
+ <meta charset="UTF-8">
817
+ <title>OpenQA API Docs</title>
818
+ <meta name="viewport" content="width=device-width, initial-scale=1">
819
+ <link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@5/swagger-ui.css">
820
+ </head>
821
+ <body>
822
+ <div id="swagger-ui"></div>
823
+ <script src="https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js"></script>
824
+ <script>
825
+ SwaggerUIBundle({ spec: ${spec}, dom_id: '#swagger-ui', presets: [SwaggerUIBundle.presets.apis, SwaggerUIBundle.SwaggerUIStandalonePreset], layout: 'BaseLayout' });
826
+ </script>
827
+ </body>
828
+ </html>`);
829
+ });
830
+ router.get("/api/coverage/:sessionId", async (req, res) => {
831
+ try {
832
+ const { CoverageTracker: CoverageTracker2 } = await Promise.resolve().then(() => (init_coverage(), coverage_exports));
833
+ const tracker = new CoverageTracker2();
834
+ await tracker.loadFromDatabase(db, req.params.sessionId);
835
+ res.json(tracker.getReport());
836
+ } catch (error) {
837
+ res.status(500).json({ error: error instanceof Error ? error.message : String(error) });
838
+ }
839
+ });
840
+ router.post("/api/cleanup", async (req, res) => {
841
+ try {
842
+ const maxAgeDays = parseInt(req.query.maxAgeDays) || 30;
843
+ const result = await db.pruneOldSessions(maxAgeDays);
844
+ res.json({ success: true, ...result });
845
+ } catch (error) {
846
+ res.status(500).json({ error: error instanceof Error ? error.message : String(error) });
847
+ }
848
+ });
849
+ router.get("/api/storage", async (_req, res) => {
850
+ try {
851
+ const stats = await db.getStorageStats();
852
+ res.json(stats);
853
+ } catch (error) {
854
+ res.status(500).json({ error: error instanceof Error ? error.message : String(error) });
855
+ }
856
+ });
857
+ router.get("/api/export/:sessionId", async (req, res) => {
858
+ try {
859
+ const { sessionId } = req.params;
860
+ const fmtResult = exportFormatSchema.safeParse(req.query.format || "json");
861
+ if (!fmtResult.success) {
862
+ return res.status(400).json({ error: "Invalid format. Use json, csv, or html." });
863
+ }
864
+ const format = fmtResult.data;
865
+ const exportService = new ExportService(db);
866
+ const result = await exportService.exportSession(sessionId, format);
867
+ res.setHeader("Content-Type", result.contentType);
868
+ res.setHeader("Content-Disposition", `attachment; filename="${result.filename}"`);
869
+ res.send(result.content);
870
+ } catch (error) {
871
+ res.status(404).json({ error: error instanceof Error ? error.message : String(error) });
872
+ }
873
+ });
874
+ router.get("/api/tasks", async (_req, res) => {
875
+ try {
876
+ const tasks = await db.getCurrentTasks();
877
+ res.json(tasks);
878
+ } catch (error) {
879
+ res.status(500).json({ error: error instanceof Error ? error.message : String(error) });
880
+ }
881
+ });
882
+ router.get("/api/issues", async (_req, res) => {
883
+ try {
884
+ const issues = await db.getCurrentIssues();
885
+ res.json(issues);
886
+ } catch (error) {
887
+ res.status(500).json({ error: error instanceof Error ? error.message : String(error) });
888
+ }
889
+ });
890
+ return router;
891
+ }
892
+ export {
893
+ createApiRouter
894
+ };
895
+ //# sourceMappingURL=routes.js.map