argusqa-os 9.2.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 (57) hide show
  1. package/.mcp.json +8 -0
  2. package/LICENSE +21 -0
  3. package/README.md +879 -0
  4. package/package.json +69 -0
  5. package/src/adapters/browser.js +82 -0
  6. package/src/argus.js +8 -0
  7. package/src/batch-runner.js +8 -0
  8. package/src/cli/init.js +314 -0
  9. package/src/config/schema.js +108 -0
  10. package/src/config/targets.js +309 -0
  11. package/src/domain/finding.js +25 -0
  12. package/src/mcp-server.js +156 -0
  13. package/src/orchestration/crawl-and-report.js +16 -0
  14. package/src/orchestration/dispatcher.js +263 -0
  15. package/src/orchestration/env-comparison.js +498 -0
  16. package/src/orchestration/orchestrator.js +1128 -0
  17. package/src/orchestration/report-processor.js +134 -0
  18. package/src/orchestration/slack-notifier.js +337 -0
  19. package/src/orchestration/watch-mode.js +316 -0
  20. package/src/registry.js +18 -0
  21. package/src/server/index.js +94 -0
  22. package/src/server/interaction-handler.js +126 -0
  23. package/src/server/slash-command-handler.js +185 -0
  24. package/src/utils/api-frequency.js +128 -0
  25. package/src/utils/baseline-manager.js +255 -0
  26. package/src/utils/codebase-analyzer.js +299 -0
  27. package/src/utils/content-analyzer.js +155 -0
  28. package/src/utils/contract-validator.js +178 -0
  29. package/src/utils/css-analyzer.js +407 -0
  30. package/src/utils/diff.js +189 -0
  31. package/src/utils/flakiness-detector.js +82 -0
  32. package/src/utils/flow-runner.js +572 -0
  33. package/src/utils/github-reporter.js +310 -0
  34. package/src/utils/hover-analyzer.js +214 -0
  35. package/src/utils/html-reporter.js +301 -0
  36. package/src/utils/issues-analyzer.js +171 -0
  37. package/src/utils/keyboard-analyzer.js +141 -0
  38. package/src/utils/lighthouse-checker.js +120 -0
  39. package/src/utils/logger.js +39 -0
  40. package/src/utils/login-orchestrator.js +99 -0
  41. package/src/utils/mcp-client.js +264 -0
  42. package/src/utils/mcp-parsers.js +57 -0
  43. package/src/utils/memory-analyzer.js +270 -0
  44. package/src/utils/network-timing-analyzer.js +76 -0
  45. package/src/utils/parallel-crawler.js +28 -0
  46. package/src/utils/responsive-analyzer.js +253 -0
  47. package/src/utils/retry.js +36 -0
  48. package/src/utils/route-discoverer.js +306 -0
  49. package/src/utils/security-analyzer.js +302 -0
  50. package/src/utils/seo-analyzer.js +164 -0
  51. package/src/utils/session-manager.js +12 -0
  52. package/src/utils/session-persistence.js +214 -0
  53. package/src/utils/severity-overrides.js +91 -0
  54. package/src/utils/slack-guard.js +18 -0
  55. package/src/utils/slug.js +8 -0
  56. package/src/utils/snapshot-analyzer.js +330 -0
  57. package/src/utils/telemetry.js +190 -0
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Argus Lighthouse Checker (extracted D2.5)
3
+ *
4
+ * Extracted from crawl-and-report.js so test-harness/validate.js can import
5
+ * checkLighthouse directly without pulling in the Slack-initialised orchestrator.
6
+ */
7
+
8
+ import { registerExpensive } from '../registry.js';
9
+ import { thresholds } from '../config/targets.js';
10
+ import { childLogger } from './logger.js';
11
+
12
+ const logger = childLogger('lighthouse-checker');
13
+
14
+ const LIGHTHOUSE_LABELS = {
15
+ accessibility: 'Accessibility',
16
+ performance: 'Performance',
17
+ seo: 'SEO',
18
+ 'best-practices': 'Best Practices',
19
+ };
20
+
21
+ /**
22
+ * Run a full Lighthouse audit (accessibility, performance, SEO, best-practices).
23
+ *
24
+ * Each category is scored:
25
+ * score < threshold.critical → 'critical' violation
26
+ * score < threshold.warning → 'warning' violation
27
+ *
28
+ * Individual failing audit items (score === 0) are also surfaced.
29
+ *
30
+ * @param {object} browser - CdpBrowserAdapter
31
+ * @param {string} url - URL being tested
32
+ * @returns {Promise<object[]>} Lighthouse violation findings
33
+ */
34
+ export async function checkLighthouse(browser, url) {
35
+ const violations = [];
36
+
37
+ // Lighthouse can hang indefinitely on heavy SPAs or when Chrome is under load.
38
+ // 120 s is generous — a real Lighthouse run completes in 15–30 s on most pages.
39
+ const LIGHTHOUSE_TIMEOUT_MS = parseInt(process.env.ARGUS_LIGHTHOUSE_TIMEOUT ?? '120000', 10);
40
+
41
+ try {
42
+ const auditPromise = browser.lighthouse(url, {
43
+ categories: ['accessibility', 'performance', 'seo', 'best-practices'],
44
+ });
45
+ const timeoutPromise = new Promise((_, reject) =>
46
+ setTimeout(() => reject(new Error(`Lighthouse timed out after ${LIGHTHOUSE_TIMEOUT_MS / 1000}s`)), LIGHTHOUSE_TIMEOUT_MS)
47
+ );
48
+ const result = await Promise.race([auditPromise, timeoutPromise]);
49
+
50
+ const categories = result?.categories ?? {};
51
+ const audits = result?.audits ?? {};
52
+
53
+ for (const [catKey, catThresholds] of Object.entries(thresholds.lighthouse)) {
54
+ const catData = categories[catKey]
55
+ ?? categories[catKey.replace('-', '_')]
56
+ ?? categories[catKey.replace(/-([a-z])/g, (_, c) => c.toUpperCase())];
57
+ const score = catData?.score ?? result?.[catKey]?.score ?? null;
58
+ if (score == null) continue;
59
+
60
+ const pct = Math.round(score * 100);
61
+ const label = LIGHTHOUSE_LABELS[catKey];
62
+
63
+ if (pct < catThresholds.critical) {
64
+ violations.push({
65
+ type: 'lighthouse_score',
66
+ category: catKey,
67
+ score: pct,
68
+ threshold: catThresholds.critical,
69
+ message: `Lighthouse ${label} score ${pct}/100 — critical (threshold: ${catThresholds.critical})`,
70
+ severity: 'critical',
71
+ url,
72
+ });
73
+ } else if (pct < catThresholds.warning) {
74
+ violations.push({
75
+ type: 'lighthouse_score',
76
+ category: catKey,
77
+ score: pct,
78
+ threshold: catThresholds.warning,
79
+ message: `Lighthouse ${label} score ${pct}/100 — needs improvement (threshold: ${catThresholds.warning})`,
80
+ severity: 'warning',
81
+ url,
82
+ });
83
+ }
84
+ }
85
+
86
+ for (const [auditId, audit] of Object.entries(audits)) {
87
+ if (audit.score == null || audit.score !== 0) continue;
88
+ if (audit.details?.type === 'manual') continue;
89
+
90
+ const auditCategory = Object.entries(categories).find(([, cat]) =>
91
+ cat?.auditRefs?.some?.(ref => ref.id === auditId)
92
+ )?.[0] ?? 'unknown';
93
+
94
+ const label = LIGHTHOUSE_LABELS[auditCategory] ?? auditCategory;
95
+
96
+ violations.push({
97
+ type: 'lighthouse_audit',
98
+ category: auditCategory,
99
+ auditId,
100
+ title: audit.title,
101
+ message: `[${label}] ${audit.title}${audit.description ? ' — ' + audit.description.slice(0, 120) : ''}`,
102
+ severity: 'warning',
103
+ url,
104
+ });
105
+ }
106
+
107
+ } catch (err) {
108
+ logger.warn(`[ARGUS] Lighthouse audit skipped for ${url}: ${err.message}`);
109
+ }
110
+
111
+ return violations;
112
+ }
113
+
114
+ // ── Self-registration ─────────────────────────────────────────────────────────
115
+ registerExpensive({
116
+ name: 'lighthouse',
117
+ async analyze(browser, url) {
118
+ return checkLighthouse(browser, url);
119
+ },
120
+ });
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Pino structured logger for Argus.
3
+ *
4
+ * Usage in each module:
5
+ * import { childLogger } from '../utils/logger.js';
6
+ * const logger = childLogger('module-name');
7
+ *
8
+ * Environment variables:
9
+ * ARGUS_LOG_LEVEL — log level (default: 'info'). Set to 'debug' for MCP call details.
10
+ * ARGUS_LOG_PRETTY — '1' or any truthy value: force pino-pretty human-readable output.
11
+ * '0' or empty string: force JSON output (useful in CI).
12
+ * Unset: auto-detect — pino-pretty when stdout is a TTY, JSON otherwise.
13
+ *
14
+ * JSON output (default in CI) is compatible with Datadog / Grafana Loki / CloudWatch.
15
+ */
16
+
17
+ import pino from 'pino';
18
+
19
+ function usePrettyOutput() {
20
+ const env = process.env.ARGUS_LOG_PRETTY;
21
+ if (env !== undefined) return env !== '0' && env !== '';
22
+ return process.stdout.isTTY ?? false;
23
+ }
24
+
25
+ function createLogger() {
26
+ const level = process.env.ARGUS_LOG_LEVEL ?? 'info';
27
+ if (usePrettyOutput()) {
28
+ try {
29
+ return pino({ level, transport: { target: 'pino-pretty', options: { colorize: true } } });
30
+ } catch {
31
+ // pino-pretty not installed or failed to load — fall back to JSON
32
+ }
33
+ }
34
+ return pino({ level });
35
+ }
36
+
37
+ export const logger = createLogger();
38
+
39
+ export const childLogger = (module) => logger.child({ module });
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Login Orchestrator — run login flows and manage mid-run session refresh.
3
+ *
4
+ * Extracted from session-manager.js (v9.1.7). Handles:
5
+ * - runLoginFlow: execute a targets.js auth.steps flow
6
+ * - refreshSession: detect expiring sessions and re-login proactively
7
+ *
8
+ * Uses a lock file to prevent concurrent shards from running redundant
9
+ * login flows when ARGUS_CONCURRENCY > 1.
10
+ */
11
+
12
+ import fs from 'fs';
13
+ import { runFlow } from './flow-runner.js';
14
+ import { saveSession } from './session-persistence.js';
15
+ import { childLogger } from './logger.js';
16
+
17
+ const logger = childLogger('login-orchestrator');
18
+
19
+ // ── Login Flow Runner ───────────────────────────────────────────────────────────
20
+
21
+ /**
22
+ * Execute a login flow defined as a steps array in targets.js.
23
+ *
24
+ * Delegates to flow-runner.js runFlow — same step DSL (navigate, fill, click,
25
+ * press_key, waitFor, sleep, handle_dialog, assert).
26
+ *
27
+ * @param {object} browser - CdpBrowserAdapter
28
+ * @param {string} baseUrl - Base URL prepended to path-relative navigate steps
29
+ * @param {object[]} steps - Step definitions (same DSL as flows[] in targets.js)
30
+ */
31
+ export async function runLoginFlow(browser, baseUrl, steps) {
32
+ await runFlow({ name: 'login', steps }, baseUrl, browser);
33
+ }
34
+
35
+ // ── Session Refresh (D7.6) ──────────────────────────────────────────────────────
36
+
37
+ /**
38
+ * Refresh the session mid-run if it is approaching expiry.
39
+ *
40
+ * Called between routes (before restoreSession). When the saved session has
41
+ * less than auth.sessionRefreshWindowMs of validity remaining, the full login
42
+ * flow is re-run and a fresh session is saved.
43
+ *
44
+ * No-ops when:
45
+ * - auth is null or has no steps (public crawl)
46
+ * - no session file exists yet (initial login not done)
47
+ * - the session still has more than refreshWindowMs remaining
48
+ *
49
+ * @param {object} browser - CdpBrowserAdapter
50
+ * @param {object|null} auth - Auth config from targets.js
51
+ * @param {string} baseUrl - Base URL used for the login navigate step
52
+ * @returns {Promise<{ refreshed: boolean }>}
53
+ */
54
+ export async function refreshSession(browser, auth, baseUrl) {
55
+ if (!auth?.steps?.length) return { refreshed: false };
56
+
57
+ const sessionFile = auth.sessionFile ?? '.argus-session.json';
58
+ const maxAgeMs = auth.sessionMaxAgeMs ?? 60 * 60 * 1000;
59
+ const refreshWindowMs = auth.sessionRefreshWindowMs ?? 5 * 60 * 1000;
60
+
61
+ if (!fs.existsSync(sessionFile)) return { refreshed: false };
62
+
63
+ let state;
64
+ try {
65
+ state = JSON.parse(fs.readFileSync(sessionFile, 'utf8'));
66
+ } catch {
67
+ return { refreshed: false };
68
+ }
69
+
70
+ const age = Date.now() - new Date(state.savedAt).getTime();
71
+ if (isNaN(age)) return { refreshed: false };
72
+ const remainingMs = maxAgeMs - age;
73
+
74
+ if (remainingMs > refreshWindowMs) return { refreshed: false };
75
+
76
+ logger.info(
77
+ `[ARGUS] Auth: session expires in ${Math.round(remainingMs / 1000)}s — refreshing login...`
78
+ );
79
+
80
+ const lockFile = sessionFile + '.lock';
81
+ let lockFd = null;
82
+ try {
83
+ lockFd = fs.openSync(lockFile, 'wx');
84
+ } catch (err) {
85
+ if (err.code === 'EEXIST') {
86
+ logger.info('[ARGUS] Auth: refresh lock held by another shard — skipping duplicate login');
87
+ return { refreshed: false };
88
+ }
89
+ throw err;
90
+ }
91
+ try {
92
+ await runLoginFlow(browser, baseUrl, auth.steps);
93
+ await saveSession(browser, sessionFile);
94
+ return { refreshed: true };
95
+ } finally {
96
+ if (lockFd !== null) { try { fs.closeSync(lockFd); } catch {} }
97
+ try { fs.unlinkSync(lockFile); } catch {}
98
+ }
99
+ }
@@ -0,0 +1,264 @@
1
+ /**
2
+ * ARGUS MCP Client — Headless CI Mode
3
+ *
4
+ * In Claude Code (interactive), MCP tools are called natively by the agent.
5
+ * In CI (GitHub Actions, headless), this module spawns the chrome-devtools-mcp
6
+ * process and communicates via JSON-RPC over stdio, wrapping each tool as an
7
+ * async function with the same signature our orchestration scripts expect.
8
+ *
9
+ * Usage:
10
+ * const mcp = await createMcpClient();
11
+ * await mcp.navigate_page({ url: 'http://localhost:3000' });
12
+ * const msgs = await mcp.list_console_messages();
13
+ */
14
+
15
+ import { spawn } from 'child_process';
16
+ import { childLogger } from './logger.js';
17
+
18
+ const logger = childLogger('mcp-client');
19
+
20
+ // Validate MCP_BROWSER_URL before embedding it in a shell:true spawn argument.
21
+ // Two-step defense:
22
+ // 1. new URL() rejects malformed/non-http(s) values.
23
+ // 2. Shell-metacharacter check rejects valid URLs whose query strings contain
24
+ // &, |, ;, backtick, $() etc. — new URL().toString() preserves & in query
25
+ // strings (valid URL syntax), but & is a shell background-operator that
26
+ // would split the spawn command on both bash and cmd.exe.
27
+ // A legitimate Chrome remote-debug URL is always http(s)://host:port with
28
+ // no path or query string, so this check never fires in practice.
29
+ const _rawBrowserUrl = process.env.MCP_BROWSER_URL ?? 'http://127.0.0.1:9222';
30
+ let BROWSER_URL;
31
+ try {
32
+ const _parsed = new URL(_rawBrowserUrl);
33
+ if (_parsed.protocol !== 'http:' && _parsed.protocol !== 'https:') {
34
+ throw new Error('protocol must be http or https');
35
+ }
36
+ BROWSER_URL = _parsed.toString();
37
+ } catch (e) {
38
+ throw new Error(`[ARGUS] Invalid MCP_BROWSER_URL "${_rawBrowserUrl}": ${e.message}`);
39
+ }
40
+ // Shell-metacharacter guard — must run AFTER URL re-serialization.
41
+ const _SHELL_META = /[&|;<>`${}()\n\r!"]/;
42
+ if (_SHELL_META.test(BROWSER_URL)) {
43
+ throw new Error(
44
+ `[ARGUS] MCP_BROWSER_URL contains shell-unsafe characters — ` +
45
+ `use a plain http(s)://host:port URL (got: "${BROWSER_URL}")`
46
+ );
47
+ }
48
+
49
+ /**
50
+ * Unwrap an evaluate_script result to its plain value.
51
+ *
52
+ * MCP clients may return results in different shapes depending on whether they
53
+ * are running in interactive mode (Claude Code native) or headless CI mode:
54
+ * - { result: value } — some interactive-mode responses
55
+ * - value — headless client already extracts the value
56
+ * - null / undefined — script failed or Chrome not connected
57
+ *
58
+ * @param {any} raw - Raw return value from mcp.evaluate_script(...)
59
+ * @returns {any} The unwrapped value
60
+ */
61
+ export function unwrapEval(raw) {
62
+ if (raw == null) return null;
63
+ if (typeof raw === 'object' && !Array.isArray(raw)) return raw.result ?? raw;
64
+ return raw;
65
+ }
66
+ const TOOL_TIMEOUT_MS = parseInt(process.env.MCP_TOOL_TIMEOUT_MS ?? '30000', 10);
67
+
68
+ /**
69
+ * Create an MCP client that wraps chrome-devtools-mcp via JSON-RPC over stdio.
70
+ * @returns {Promise<object>} Object with all MCP tool methods
71
+ */
72
+ export async function createMcpClient() {
73
+ // On Windows, npx is npx.cmd — shell:true resolves this cross-platform.
74
+ const proc = spawn('npx', [
75
+ '-y', 'chrome-devtools-mcp@latest',
76
+ `--browser-url=${BROWSER_URL}`,
77
+ '--headless=true',
78
+ '--viewport=1920x1080',
79
+ ], {
80
+ stdio: ['pipe', 'pipe', 'inherit'],
81
+ shell: true,
82
+ });
83
+
84
+ let messageId = 1;
85
+ const pending = new Map(); // id → { resolve, reject }
86
+ let buffer = '';
87
+
88
+ // Parse newline-delimited JSON-RPC responses from stdout
89
+ // Propagate stdin write errors — if the MCP process closes unexpectedly or the
90
+ // write buffer fills, stdin emits 'error'. Without this listener the error is an
91
+ // unhandled EventEmitter exception that crashes the process. Reject all pending calls
92
+ // so callers get a meaningful error instead of waiting for the 30 s timeout.
93
+ proc.stdin.on('error', err => {
94
+ logger.error('[ARGUS] MCP stdin error:', err.message);
95
+ for (const { reject } of pending.values()) {
96
+ reject(new Error(`MCP stdin error: ${err.message}`));
97
+ }
98
+ pending.clear();
99
+ });
100
+
101
+ const MAX_BUFFER_BYTES = 50 * 1024 * 1024;
102
+ proc.stdout.on('data', (chunk) => {
103
+ if (buffer.length + chunk.length > MAX_BUFFER_BYTES) {
104
+ logger.error('[ARGUS] MCP stdout buffer overflow — discarding buffer');
105
+ buffer = '';
106
+ }
107
+ buffer += chunk.toString();
108
+ const lines = buffer.split(/\r?\n/);
109
+ buffer = lines.pop(); // keep incomplete line in buffer
110
+ for (const line of lines) {
111
+ if (!line.trim()) continue;
112
+ try {
113
+ const msg = JSON.parse(line);
114
+ if (msg.id !== undefined && pending.has(msg.id)) {
115
+ const { resolve, reject } = pending.get(msg.id);
116
+ pending.delete(msg.id);
117
+ if (msg.error) {
118
+ reject(new Error(`MCP error ${msg.error.code}: ${msg.error.message}`));
119
+ } else {
120
+ resolve(msg.result);
121
+ }
122
+ }
123
+ } catch {
124
+ // non-JSON line from process — ignore
125
+ }
126
+ }
127
+ });
128
+
129
+ proc.on('exit', (code, signal) => {
130
+ if (code !== 0 || signal) {
131
+ for (const { reject } of pending.values()) {
132
+ reject(new Error(`MCP process exited: code=${code}, signal=${signal}`));
133
+ }
134
+ pending.clear();
135
+ }
136
+ });
137
+
138
+ // Send JSON-RPC initialize handshake
139
+ await call('initialize', {
140
+ protocolVersion: '2024-11-05',
141
+ capabilities: {},
142
+ clientInfo: { name: 'argus', version: '1.0.0' },
143
+ });
144
+
145
+ /**
146
+ * Call an MCP tool by name with params.
147
+ * @param {string} method - JSON-RPC method name
148
+ * @param {object} params
149
+ * @returns {Promise<any>}
150
+ */
151
+ function call(method, params = {}) {
152
+ return new Promise((resolve, reject) => {
153
+ const id = messageId++;
154
+ const request = JSON.stringify({ jsonrpc: '2.0', id, method, params }) + '\n';
155
+ pending.set(id, { resolve, reject });
156
+
157
+ const timer = setTimeout(() => {
158
+ if (pending.has(id)) {
159
+ pending.delete(id);
160
+ reject(new Error(`MCP tool timeout: ${method} (${TOOL_TIMEOUT_MS}ms)`));
161
+ }
162
+ }, TOOL_TIMEOUT_MS);
163
+
164
+ // Clear timer on resolution
165
+ const { resolve: origResolve, reject: origReject } = pending.get(id);
166
+ pending.set(id, {
167
+ resolve: (v) => { clearTimeout(timer); origResolve(v); },
168
+ reject: (e) => { clearTimeout(timer); origReject(e); },
169
+ });
170
+
171
+ proc.stdin.write(request, (err) => {
172
+ if (err && pending.has(id)) {
173
+ const { reject: rej } = pending.get(id);
174
+ pending.delete(id);
175
+ rej(err);
176
+ }
177
+ });
178
+ });
179
+ }
180
+
181
+ /**
182
+ * Call an MCP tool (tools/call JSON-RPC method).
183
+ */
184
+ function tool(name, args = {}) {
185
+ return call('tools/call', { name, arguments: args })
186
+ .then(result => {
187
+ // MCP returns { content: [{ type, text|data }] } — extract the value
188
+ const content = result?.content;
189
+ if (Array.isArray(content) && content.length > 0) {
190
+ const item = content[0];
191
+ if (item.type === 'image') {
192
+ // take_screenshot returns base64 image data — return in a shape callers expect
193
+ return { data: item.data, mimeType: item.mimeType ?? 'image/png' };
194
+ }
195
+ if (item.type === 'text') {
196
+ const text = item.text;
197
+ // chrome-devtools-mcp wraps evaluate_script results in a markdown code block:
198
+ // "Script ran on page and returned:\n```json\n<value>\n```"
199
+ // \n? before closing fence — responses without a trailing newline
200
+ // before the ``` would not match and fall through to raw JSON.parse, which
201
+ // then fails because the fence characters are still present in the text.
202
+ const mdMatch = text.match(/```(?:json)?\n([\s\S]*?)\n?```/);
203
+ if (mdMatch) {
204
+ try { return JSON.parse(mdMatch[1]); } catch { return mdMatch[1]; }
205
+ }
206
+ try { return JSON.parse(text); } catch { return text; }
207
+ }
208
+ }
209
+ return result;
210
+ });
211
+ }
212
+
213
+ // Graceful shutdown — idempotent and rejects all in-flight calls immediately.
214
+ // Without this, pending promises wait until TOOL_TIMEOUT_MS (30 s) after
215
+ // shutdown, making CI teardown slow and leaving dangling rejections.
216
+ let closed = false;
217
+ function close() {
218
+ if (closed) return;
219
+ closed = true;
220
+ for (const { reject } of pending.values()) {
221
+ reject(new Error('MCP client closed'));
222
+ }
223
+ pending.clear();
224
+ proc.stdin.end();
225
+ proc.kill('SIGTERM');
226
+ }
227
+
228
+ // Build the mcp interface object matching what orchestration scripts expect
229
+ return {
230
+ navigate_page: (args) => tool('navigate_page', args),
231
+ list_pages: (args) => tool('list_pages', args),
232
+ new_page: (args) => tool('new_page', args),
233
+ select_page: (args) => tool('select_page', args),
234
+ close_page: (args) => tool('close_page', args),
235
+ take_screenshot: (args) => tool('take_screenshot', args),
236
+ take_snapshot: (args) => tool('take_snapshot', args),
237
+ list_console_messages: (args) => tool('list_console_messages', args),
238
+ get_console_message: (args) => tool('get_console_message', args),
239
+ list_network_requests: (args) => tool('list_network_requests', args),
240
+ get_network_request: (args) => tool('get_network_request', args),
241
+ evaluate_script: (args) => tool('evaluate_script', args),
242
+ wait_for: (args) => tool('wait_for', args),
243
+ click: (args) => tool('click', args),
244
+ fill: (args) => tool('fill', args),
245
+ fill_form: (args) => tool('fill_form', args),
246
+ hover: (args) => tool('hover', args),
247
+ type_text: (args) => tool('type_text', args),
248
+ press_key: (args) => tool('press_key', args),
249
+ resize_page: (args) => tool('resize_page', args),
250
+ emulate: (args) => tool('emulate', args),
251
+ performance_start_trace: (args) => tool('performance_start_trace', args),
252
+ performance_stop_trace: (args) => tool('performance_stop_trace', args),
253
+ performance_analyze_insight: (args) => tool('performance_analyze_insight', args),
254
+ take_memory_snapshot: (args) => tool('take_memory_snapshot', args),
255
+ lighthouse_audit: (args) => tool('lighthouse_audit', args),
256
+ handle_dialog: (args) => tool('handle_dialog', args),
257
+ drag: (args) => tool('drag', args),
258
+ upload_file: (args) => tool('upload_file', args),
259
+ select_option: (args) => tool('select_option', args),
260
+ emulate_cpu: (args) => tool('emulate_cpu', args),
261
+ emulate_network: (args) => tool('emulate_network', args),
262
+ close,
263
+ };
264
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Text-format parsers for chrome-devtools-mcp responses.
3
+ *
4
+ * chrome-devtools-mcp@latest returns list_console_messages and
5
+ * list_network_requests as human-readable markdown text rather than JSON.
6
+ * These parsers extract structured objects so the rest of the pipeline
7
+ * can work with consistent data shapes regardless of MCP response format.
8
+ *
9
+ * Extracted from watch-mode.js and promoted to a shared module so
10
+ * CdpBrowserAdapter can use them in listConsole() and listNetwork().
11
+ */
12
+
13
+ import { normalizeArray } from './flow-runner.js';
14
+
15
+ /**
16
+ * Parse the text response from list_console_messages.
17
+ * Format: "msgid=N [level] text (N args)\n..."
18
+ * @param {any} raw - Raw value returned by the MCP tool
19
+ * @returns {object[]}
20
+ */
21
+ export function parseConsoleMsgResponse(raw) {
22
+ if (!raw) return [];
23
+ if (Array.isArray(raw)) return raw;
24
+ if (typeof raw === 'object') return normalizeArray(raw);
25
+ if (typeof raw !== 'string') return [];
26
+ const msgs = [];
27
+ const re = /msgid=(\d+)\s+\[(\w+)\]\s+(.*?)(?:\s+\(\d+\s+args?\))?$/gm;
28
+ let m;
29
+ while ((m = re.exec(raw)) !== null) {
30
+ const [, msgid, rawLevel, text] = m;
31
+ const level = rawLevel === 'warn' ? 'warning' : rawLevel.toLowerCase();
32
+ msgs.push({ _msgid: Number(msgid), level, text, message: text });
33
+ }
34
+ return msgs;
35
+ }
36
+
37
+ /**
38
+ * Parse the text response from list_network_requests.
39
+ * Format: "reqid=N METHOD URL [STATUS]\n..."
40
+ * @param {any} raw - Raw value returned by the MCP tool
41
+ * @returns {object[]}
42
+ */
43
+ export function parseNetworkReqResponse(raw) {
44
+ if (!raw) return [];
45
+ if (Array.isArray(raw)) return raw;
46
+ if (typeof raw === 'object') return normalizeArray(raw);
47
+ if (typeof raw !== 'string') return [];
48
+ const reqs = [];
49
+ const re = /reqid=(\d+)\s+(\w+)\s+(\S+)\s+\[(\d+)\]/gm;
50
+ let m;
51
+ while ((m = re.exec(raw)) !== null) {
52
+ const [, reqid, method, url, statusStr] = m;
53
+ const status = parseInt(statusStr, 10);
54
+ reqs.push({ _reqid: Number(reqid), requestId: Number(reqid), method, url, status, statusCode: status });
55
+ }
56
+ return reqs;
57
+ }