argusqa-os 9.4.2 → 9.4.3

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.
@@ -135,7 +135,7 @@ export function formatPrComment(report, diff) {
135
135
  }
136
136
 
137
137
  lines.push('', '---');
138
- lines.push(`_Generated by [Argus](https://github.com/ironclawdevs/GodMode---AI-Dev-Testing-Tool) · ${new Date(report.generatedAt).toISOString()}_`);
138
+ lines.push(`_Generated by [Argus](https://github.com/ironclawdevs27/Argus) · ${new Date(report.generatedAt).toISOString()}_`);
139
139
 
140
140
  return lines.join('\n');
141
141
  }
@@ -1,263 +1,263 @@
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_heapsnapshot: (args) => tool('take_heapsnapshot', 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_network: (args) => tool('emulate_network', args),
261
- close,
262
- };
263
- }
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_heapsnapshot: (args) => tool('take_heapsnapshot', 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_network: (args) => tool('emulate_network', args),
261
+ close,
262
+ };
263
+ }