argusqa-os 9.5.1 → 9.5.5

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/glama.json CHANGED
@@ -1,32 +1,36 @@
1
- {
2
- "$schema": "https://glama.ai/mcp/schemas/server.json",
3
- "name": "argus",
4
- "description": "AI-powered QA harness that audits web apps via Chrome DevTools Protocol. Catches JS errors, network failures, a11y violations, SEO issues, security headers, CSS regressions, and more — directly from Claude conversations. 6 MCP tools: argus_audit (fast 8-analyzer pass), argus_audit_full (Lighthouse + memory + responsive), argus_compare (dev vs staging diff), argus_last_report (retrieve last JSON report), argus_watch_snapshot (live tab snapshot without navigating), argus_get_context (LLM-optimized context + fix loop with snapshot_id diff). 126 test blocks, 528 hard assertions, 54 detection categories.",
5
- "maintainers": ["ironclawdevs27"],
6
- "tools": [
7
- {
8
- "name": "argus_audit",
9
- "description": "Fast QA audit — JS errors, network failures (4xx/5xx), API frequency loops, CSS cascade issues, SEO violations, security headers, accessibility, and content. Returns { findings, summary }. Supports cache: true to skip re-crawl on repeat calls."
10
- },
11
- {
12
- "name": "argus_audit_full",
13
- "description": "Deep QA audit — extends argus_audit with Lighthouse performance/accessibility scoring, responsive layout checks at 4 viewports, memory leak detection via heap snapshot, and accessibility tree analysis."
14
- },
15
- {
16
- "name": "argus_compare",
17
- "description": "Diffs dev vs staging environments side-by-side. Captures screenshots, runs all analyzers on each, and surfaces regressions — findings present in staging but not dev, or with changed severity."
18
- },
19
- {
20
- "name": "argus_last_report",
21
- "description": "Returns the most recent Argus JSON report from the reports/ directory without re-running a scan."
22
- },
23
- {
24
- "name": "argus_watch_snapshot",
25
- "description": "Snapshots the currently open Chrome tab without navigating — captures console errors, network failures, CORS blocks, and auth failures in one poll. Accepts optional tabId to inspect a specific tab."
26
- },
27
- {
28
- "name": "argus_get_context",
29
- "description": "LLM-optimized diagnostic context for the open Chrome tab. Returns snapshot_id for fix-loop diffing: pass it back on the next call to get resolved/new_issues/persisting arrays. Accepts optional tabId for multi-tab workflows."
30
- }
31
- ]
32
- }
1
+ {
2
+ "$schema": "https://glama.ai/mcp/schemas/server.json",
3
+ "name": "argus",
4
+ "description": "AI-powered QA harness that audits web apps via Chrome DevTools Protocol. Catches JS errors, network failures, a11y violations, SEO issues, security headers, CSS regressions, and more — directly from Claude conversations. 7 MCP tools: argus_audit (fast 8-analyzer pass), argus_audit_full (Lighthouse + memory + responsive), argus_compare (dev vs staging diff), argus_last_report (retrieve last JSON report), argus_watch_snapshot (live tab snapshot without navigating), argus_get_context (LLM-optimized context + fix loop with snapshot_id diff), argus_design_audit (Figma design fidelity — 13 finding types). 130 test blocks, 581 hard assertions, 58 detection categories.",
5
+ "maintainers": ["ironclawdevs27"],
6
+ "tools": [
7
+ {
8
+ "name": "argus_audit",
9
+ "description": "Fast QA audit — JS errors, network failures (4xx/5xx), API frequency loops, CSS cascade issues, SEO violations, security headers, accessibility, and content. Returns { findings, summary }. Supports cache: true to skip re-crawl on repeat calls."
10
+ },
11
+ {
12
+ "name": "argus_audit_full",
13
+ "description": "Deep QA audit — extends argus_audit with Lighthouse performance/accessibility scoring, responsive layout checks at 4 viewports, memory leak detection via heap snapshot, and accessibility tree analysis."
14
+ },
15
+ {
16
+ "name": "argus_compare",
17
+ "description": "Diffs dev vs staging environments side-by-side. Captures screenshots, runs all analyzers on each, and surfaces regressions — findings present in staging but not dev, or with changed severity."
18
+ },
19
+ {
20
+ "name": "argus_last_report",
21
+ "description": "Returns the most recent Argus JSON report from the reports/ directory without re-running a scan."
22
+ },
23
+ {
24
+ "name": "argus_watch_snapshot",
25
+ "description": "Snapshots the currently open Chrome tab without navigating — captures console errors, network failures, CORS blocks, and auth failures in one poll. Accepts optional tabId to inspect a specific tab."
26
+ },
27
+ {
28
+ "name": "argus_get_context",
29
+ "description": "LLM-optimized diagnostic context for the open Chrome tab. Returns snapshot_id for fix-loop diffing: pass it back on the next call to get resolved/new_issues/persisting arrays. Accepts optional tabId for multi-tab workflows."
30
+ },
31
+ {
32
+ "name": "argus_design_audit",
33
+ "description": "Full Figma design-to-implementation fidelity audit. Fetches design spec from a Figma frame URL (requires FIGMA_API_TOKEN) and compares every extracted property against live DOM computed styles. Detects 13 mismatch finding types: CSS token values, component presence, fill/text color (RGB distance), typography (fontSize/fontWeight/lineHeight/fontFamily/letterSpacing), Auto Layout padding and gap, border-radius (per-corner), bounding-box overflow, absolute position drift (scroll-corrected x/y vs Figma bounds), border stroke (color+weight), box-shadow (offset+blur+spread+color), opacity, and text content. Selector fallback: tries [data-testid], [aria-label], #id, .class per node."
34
+ }
35
+ ]
36
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "argusqa-os",
3
- "version": "9.5.1",
3
+ "version": "9.5.5",
4
4
  "mcpName": "io.github.ironclawdevs27/argus",
5
5
  "description": "Argus — AI-powered automated dev-testing platform using Chrome DevTools MCP and Claude Code",
6
6
  "keywords": [
@@ -25,7 +25,7 @@ export class CdpBrowserAdapter {
25
25
 
26
26
  // ── Evaluation & snapshots ──────────────────────────────────────────────────
27
27
  evaluate(fn) { return this._mcp.evaluate_script({ function: fn }); }
28
- snapshot() { return this._mcp.take_snapshot(); }
28
+ snapshot(opts = {}) { return this._mcp.take_snapshot(opts); }
29
29
  screenshot(opts = {}) { return this._mcp.take_screenshot(opts); }
30
30
  heapSnapshot(opts = {}) { return this._mcp.take_heapsnapshot(opts); }
31
31
 
@@ -45,9 +45,10 @@ export class CdpBrowserAdapter {
45
45
  waitFor(opts) { return this._mcp.wait_for(opts); }
46
46
 
47
47
  // ── Viewport ────────────────────────────────────────────────────────────────
48
- emulate(viewport) { return this._mcp.emulate({ viewport }); }
49
- emulateCpu(rate) { return this._mcp.emulate({ cpuThrottlingRate: rate }); }
50
- resize(w, h) { return this._mcp.resize_page({ width: w, height: h }); }
48
+ emulate(viewport) { return this._mcp.emulate({ viewport }); }
49
+ emulateCpu(rate) { return this._mcp.emulate({ cpuThrottlingRate: rate }); }
50
+ emulateColorScheme(scheme) { return this._mcp.emulate({ colorScheme: scheme }); }
51
+ resize(w, h) { return this._mcp.resize_page({ width: w, height: h }); }
51
52
 
52
53
  // ── Network & performance ───────────────────────────────────────────────────
53
54
  getNetworkRequest(reqId) { return this._mcp.get_network_request({ requestId: reqId }); }
@@ -0,0 +1,336 @@
1
+ /**
2
+ * Figma REST adapter — extracts the full design spec from a Figma frame.
3
+ *
4
+ * For each node in the frame tree, extracts:
5
+ * bounds — x, y, width, height relative to the frame origin
6
+ * fill — primary solid fill color (r,g,b,a — 0-255)
7
+ * stroke — border color + weight
8
+ * typography — fontSize, fontWeight, lineHeightPx, letterSpacing (TEXT nodes)
9
+ * spacing — Auto Layout padding + gap
10
+ * cornerRadius
11
+ * shadow — primary DROP_SHADOW effect
12
+ * opacity
13
+ *
14
+ * Returns null gracefully when FIGMA_API_TOKEN is absent, the URL is invalid,
15
+ * or the API call fails — callers skip analysis without crashing.
16
+ *
17
+ * Supported Figma URL formats:
18
+ * https://www.figma.com/file/<fileKey>/Name?node-id=42%3A0
19
+ * https://www.figma.com/design/<fileKey>/Name?node-id=42-0
20
+ *
21
+ * Requires env: FIGMA_API_TOKEN (Personal Access Token from figma.com/settings)
22
+ */
23
+
24
+ import { childLogger } from '../utils/logger.js';
25
+
26
+ const logger = childLogger('figma-adapter');
27
+
28
+ const FIGMA_API = 'https://api.figma.com/v1';
29
+
30
+ // Node types that carry no useful layout/style data — skip during tree walk
31
+ const SKIP_TYPES = new Set(['VECTOR', 'STAR', 'LINE', 'BOOLEAN_OPERATION', 'REGULAR_POLYGON']);
32
+
33
+ // ── URL parsing ───────────────────────────────────────────────────────────────
34
+
35
+ /**
36
+ * Parse fileKey and nodeId from a Figma frame URL.
37
+ * Returns null if the URL is not a recognisable Figma frame URL.
38
+ */
39
+ export function parseFigmaUrl(url) {
40
+ if (!url || typeof url !== 'string') return null;
41
+ const fileMatch = url.match(/figma\.com\/(?:file|design)\/([A-Za-z0-9]+)/);
42
+ const nodeMatch = url.match(/node-id=([^&]+)/);
43
+ if (!fileMatch) return null;
44
+ const fileKey = fileMatch[1];
45
+ const rawNode = nodeMatch?.[1];
46
+ if (!rawNode) return null;
47
+ // node-id can be URL-encoded "42%3A0" or dash-separated "42-0" — normalise to "42:0"
48
+ const nodeId = decodeURIComponent(rawNode).replace('-', ':');
49
+ return { fileKey, nodeId };
50
+ }
51
+
52
+ // ── Selector inference ────────────────────────────────────────────────────────
53
+
54
+ /**
55
+ * Infer a prioritised list of CSS selector candidates from a Figma layer name.
56
+ * The analyzer tries each in order and uses the first that matches a DOM element.
57
+ *
58
+ * Priority:
59
+ * 1. Explicit selector — if the name starts with #, ., or [ use it verbatim
60
+ * 2. data-testid attribute (slug form)
61
+ * 3. aria-label attribute (raw name)
62
+ * 4. ID selector (slug form)
63
+ * 5. Class selector (BEM slug: spaces→-, /→--)
64
+ *
65
+ * Examples:
66
+ * "Button / Primary" → [data-testid="button--primary"], [aria-label="Button / Primary"], #button--primary, .button--primary
67
+ * "#hero" → ["#hero"] (explicit — used verbatim)
68
+ * ".card" → [".card"] (explicit)
69
+ */
70
+ function inferSelectors(node) {
71
+ const name = node.name;
72
+
73
+ // Designer typed an explicit selector — honour it and skip inference
74
+ if (name.startsWith('#') || name.startsWith('.') || name.startsWith('[')) {
75
+ return [name];
76
+ }
77
+
78
+ const slug = name
79
+ .toLowerCase()
80
+ .replace(/\s*\/\s*/g, '--')
81
+ .replace(/[^a-z0-9-]+/g, '-')
82
+ .replace(/^-+|-+$/g, '');
83
+
84
+ if (!slug) return [];
85
+
86
+ return [
87
+ `[data-testid="${slug}"]`,
88
+ `[aria-label="${name}"]`,
89
+ `#${slug}`,
90
+ `.${slug}`,
91
+ ];
92
+ }
93
+
94
+ // ── Node extraction ───────────────────────────────────────────────────────────
95
+
96
+ /**
97
+ * Extract all design properties from a single Figma node into a flat object.
98
+ * All color channels are 0-255. Bounds are relative to the frame origin.
99
+ */
100
+ function extractNode(node, frameX, frameY) {
101
+ if (!node) return null;
102
+
103
+ const selectors = inferSelectors(node);
104
+ const result = {
105
+ id: node.id,
106
+ name: node.name,
107
+ type: node.type,
108
+ selectors: selectors, // ordered candidates — analyzer tries each until one matches
109
+ selector: selectors[0] ?? `.${node.name.toLowerCase().replace(/[^a-z0-9]+/g, '-')}`,
110
+ opacity: node.opacity ?? 1,
111
+
112
+ // Populated below
113
+ bounds: null,
114
+ fill: null,
115
+ stroke: null,
116
+ typography: null,
117
+ spacing: null,
118
+ cornerRadius: null,
119
+ shadow: null,
120
+ };
121
+
122
+ // Bounds — relative to frame origin so they map to viewport coords when the
123
+ // browser viewport matches the frame dimensions.
124
+ if (node.absoluteBoundingBox) {
125
+ result.bounds = {
126
+ x: node.absoluteBoundingBox.x - frameX,
127
+ y: node.absoluteBoundingBox.y - frameY,
128
+ width: node.absoluteBoundingBox.width,
129
+ height: node.absoluteBoundingBox.height,
130
+ };
131
+ }
132
+
133
+ // Primary solid fill (first visible solid fill)
134
+ for (const fill of (node.fills ?? [])) {
135
+ if (fill.type === 'SOLID' && fill.color && fill.visible !== false) {
136
+ result.fill = {
137
+ r: Math.round(fill.color.r * 255),
138
+ g: Math.round(fill.color.g * 255),
139
+ b: Math.round(fill.color.b * 255),
140
+ a: Math.round((fill.opacity ?? 1) * 255),
141
+ };
142
+ break;
143
+ }
144
+ }
145
+
146
+ // Primary stroke
147
+ for (const stroke of (node.strokes ?? [])) {
148
+ if (stroke.type === 'SOLID' && stroke.color && stroke.visible !== false) {
149
+ result.stroke = {
150
+ r: Math.round(stroke.color.r * 255),
151
+ g: Math.round(stroke.color.g * 255),
152
+ b: Math.round(stroke.color.b * 255),
153
+ a: Math.round((stroke.opacity ?? 1) * 255),
154
+ weight: node.strokeWeight ?? 1,
155
+ };
156
+ break;
157
+ }
158
+ }
159
+
160
+ // Typography (TEXT nodes only)
161
+ if (node.type === 'TEXT' && node.style) {
162
+ const s = node.style;
163
+ result.typography = {
164
+ fontFamily: s.fontFamily ?? null,
165
+ fontSize: s.fontSize ?? null,
166
+ fontWeight: s.fontWeight ?? null,
167
+ lineHeightPx: s.lineHeightPx ?? null,
168
+ letterSpacing: s.letterSpacing ?? 0,
169
+ };
170
+ }
171
+
172
+ // Text content — actual Figma copy; compared against DOM textContent
173
+ if (node.type === 'TEXT' && typeof node.characters === 'string') {
174
+ result.characters = node.characters.trim() || null;
175
+ }
176
+
177
+ // Auto Layout spacing — maps to CSS padding + gap.
178
+ // layoutMode ('HORIZONTAL'|'VERTICAL') lets the analyzer pick columnGap vs rowGap.
179
+ if (node.layoutMode && node.layoutMode !== 'NONE') {
180
+ result.spacing = {
181
+ paddingTop: node.paddingTop ?? 0,
182
+ paddingRight: node.paddingRight ?? 0,
183
+ paddingBottom: node.paddingBottom ?? 0,
184
+ paddingLeft: node.paddingLeft ?? 0,
185
+ gap: node.itemSpacing ?? 0,
186
+ layoutMode: node.layoutMode,
187
+ };
188
+ }
189
+
190
+ // Corner radius — uniform number or per-corner object.
191
+ // Figma rectangleCornerRadii = [topLeft, topRight, bottomRight, bottomLeft].
192
+ if (node.cornerRadius != null) {
193
+ result.cornerRadius = node.cornerRadius; // uniform
194
+ } else if (node.rectangleCornerRadii) {
195
+ const [tl, tr, br, bl] = node.rectangleCornerRadii;
196
+ // Collapse to a single number if all corners are equal (avoids noise in findings)
197
+ result.cornerRadius = (tl === tr && tr === br && br === bl)
198
+ ? tl
199
+ : { topLeft: tl, topRight: tr, bottomRight: br, bottomLeft: bl };
200
+ }
201
+
202
+ // Primary DROP_SHADOW effect
203
+ for (const eff of (node.effects ?? [])) {
204
+ if (eff.type === 'DROP_SHADOW' && eff.visible !== false) {
205
+ result.shadow = {
206
+ offsetX: eff.offset?.x ?? 0,
207
+ offsetY: eff.offset?.y ?? 0,
208
+ blur: eff.radius ?? 0,
209
+ spread: eff.spread ?? 0,
210
+ r: Math.round((eff.color?.r ?? 0) * 255),
211
+ g: Math.round((eff.color?.g ?? 0) * 255),
212
+ b: Math.round((eff.color?.b ?? 0) * 255),
213
+ a: Math.round((eff.color?.a ?? 0.25) * 255),
214
+ };
215
+ break;
216
+ }
217
+ }
218
+
219
+ return result;
220
+ }
221
+
222
+ // ── Tree walker ───────────────────────────────────────────────────────────────
223
+
224
+ function parseFigmaNodes(data, nodeId) {
225
+ const nodeKey = nodeId.replace(':', '-');
226
+ const nodeData = data?.nodes?.[nodeKey] ?? data?.nodes?.[nodeId];
227
+ if (!nodeData?.document) return null;
228
+
229
+ const doc = nodeData.document;
230
+ const frameX = doc.absoluteBoundingBox?.x ?? 0;
231
+ const frameY = doc.absoluteBoundingBox?.y ?? 0;
232
+
233
+ const nodes = [];
234
+ const tokens = {}; // legacy CSS-var format
235
+ const components = []; // legacy component presence format
236
+
237
+ function walk(node) {
238
+ if (!node || SKIP_TYPES.has(node.type)) return;
239
+
240
+ const extracted = extractNode(node, frameX, frameY);
241
+ if (extracted) {
242
+ nodes.push(extracted);
243
+
244
+ // Build legacy tokens map for backward compat
245
+ if (extracted.fill) {
246
+ const hex = '#' +
247
+ extracted.fill.r.toString(16).padStart(2, '0') +
248
+ extracted.fill.g.toString(16).padStart(2, '0') +
249
+ extracted.fill.b.toString(16).padStart(2, '0');
250
+ tokens[`--figma-${extracted.selector.slice(1)}-fill`] = hex;
251
+ }
252
+
253
+ // Legacy component presence list
254
+ if (node.type === 'COMPONENT' || node.type === 'INSTANCE') {
255
+ components.push({ name: node.name, selector: extracted.selector });
256
+ }
257
+ }
258
+
259
+ for (const child of (node.children ?? [])) walk(child);
260
+ }
261
+
262
+ walk(doc);
263
+
264
+ return {
265
+ nodes,
266
+ frame: {
267
+ name: doc.name ?? '',
268
+ x: frameX,
269
+ y: frameY,
270
+ width: doc.absoluteBoundingBox?.width ?? 0,
271
+ height: doc.absoluteBoundingBox?.height ?? 0,
272
+ },
273
+ // Legacy fields — still consumed by backward-compat token comparison path
274
+ tokens,
275
+ components,
276
+ };
277
+ }
278
+
279
+ // ── Public API ────────────────────────────────────────────────────────────────
280
+
281
+ /**
282
+ * Fetch the full design spec for a Figma frame URL.
283
+ *
284
+ * Returns null when FIGMA_API_TOKEN is unset, the URL is unparseable,
285
+ * or the Figma API returns an error. All errors are logged at warn level.
286
+ *
287
+ * @param {string} figmaFrameUrl
288
+ * @returns {Promise<{nodes, frame, tokens, components}|null>}
289
+ */
290
+ export async function getFigmaFrame(figmaFrameUrl) {
291
+ const token = process.env.FIGMA_API_TOKEN;
292
+ if (!token) {
293
+ logger.debug('[ARGUS] figma-adapter: FIGMA_API_TOKEN not set — skipping design fidelity fetch');
294
+ return null;
295
+ }
296
+
297
+ const parsed = parseFigmaUrl(figmaFrameUrl);
298
+ if (!parsed) {
299
+ logger.warn(`[ARGUS] figma-adapter: cannot parse Figma URL: ${figmaFrameUrl}`);
300
+ return null;
301
+ }
302
+
303
+ const { fileKey, nodeId } = parsed;
304
+ const encodedId = nodeId.replace(':', '-');
305
+ const apiUrl = `${FIGMA_API}/files/${fileKey}/nodes?ids=${encodedId}&geometry=paths`;
306
+
307
+ try {
308
+ const res = await fetch(apiUrl, {
309
+ headers: { 'X-Figma-Token': token },
310
+ signal: AbortSignal.timeout(15000),
311
+ });
312
+
313
+ if (!res.ok) {
314
+ logger.warn(`[ARGUS] figma-adapter: Figma API ${res.status} for ${figmaFrameUrl}`);
315
+ return null;
316
+ }
317
+
318
+ const data = await res.json();
319
+ const result = parseFigmaNodes(data, nodeId);
320
+
321
+ if (!result) {
322
+ logger.warn(`[ARGUS] figma-adapter: no node data for nodeId "${nodeId}" in ${figmaFrameUrl}`);
323
+ return null;
324
+ }
325
+
326
+ logger.info(
327
+ `[ARGUS] figma-adapter: extracted ${result.nodes.length} node(s) from "${result.frame.name}" ` +
328
+ `(${result.frame.width}×${result.frame.height})`
329
+ );
330
+ return result;
331
+
332
+ } catch (err) {
333
+ logger.warn(`[ARGUS] figma-adapter: fetch failed for ${figmaFrameUrl}: ${err.message}`);
334
+ return null;
335
+ }
336
+ }
@@ -64,6 +64,10 @@ export const thresholds = {
64
64
  seo: { critical: 50, warning: 90 },
65
65
  'best-practices': { critical: 50, warning: 90 },
66
66
  },
67
+ visual: {
68
+ warnPercent: parseFloat(process.env.VISUAL_WARN_PERCENT ?? '0.1'), // % pixels changed → warning
69
+ critPercent: parseFloat(process.env.VISUAL_CRIT_PERCENT ?? '5.0'), // % pixels changed → critical
70
+ },
67
71
  };
68
72
 
69
73
  /**
@@ -14,7 +14,22 @@ const VALID_SEVERITIES = new Set(['critical', 'warning', 'info']);
14
14
  /**
15
15
  * Create an immutable finding object.
16
16
  *
17
- * @param {{ type: string, severity: string, message: string, url?: string }} opts
17
+ * Required fields: type, severity ('critical'|'warning'|'info'), message.
18
+ * Common optional fields passed via ...rest (use these names for consistency):
19
+ * url — affected URL (defaults to '')
20
+ * selector — CSS selector of the offending element
21
+ * requestUrl — URL of the offending network request
22
+ * status — HTTP status code (number)
23
+ * method — HTTP method string
24
+ * element — human-readable element description (e.g. 'button#submit')
25
+ * property — CSS property name
26
+ * metric — performance metric name (e.g. 'LCP')
27
+ * value — measured value
28
+ * budget — expected threshold value
29
+ * count — numeric count of occurrences
30
+ * source — source file or stylesheet path
31
+ *
32
+ * @param {{ type: string, severity: string, message: string, url?: string, [key: string]: any }} opts
18
33
  * @returns {Readonly<object>}
19
34
  */
20
35
  export function createFinding({ type, severity, message, url = '', ...rest }) {
package/src/mcp-server.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * Argus MCP Server (v9.5.1)
3
+ * Argus MCP Server (v9.5.5)
4
4
  *
5
5
  * Exposes Argus as an MCP server so Claude (or any MCP client) can call
6
6
  * argus_audit, argus_audit_full, argus_compare, argus_last_report, and
@@ -30,6 +30,8 @@ import { crawlRouteCheap, runCrawl } from './orchestration/crawl-and-re
30
30
  import { runComparison } from './orchestration/env-comparison.js';
31
31
  import { WatchSession } from './orchestration/watch-mode.js';
32
32
  import { CdpBrowserAdapter } from './adapters/browser.js';
33
+ import { getFigmaFrame } from './adapters/figma.js';
34
+ import { analyzeDesignFidelity } from './utils/design-fidelity-analyzer.js';
33
35
 
34
36
  const REPORTS_DIR = path.resolve(process.cwd(), 'reports');
35
37
 
@@ -117,6 +119,18 @@ const TOOLS = [
117
119
  },
118
120
  },
119
121
  },
122
+ {
123
+ name: 'argus_design_audit',
124
+ description: 'Full design-to-implementation fidelity audit against a Figma frame. 13 mismatch finding types: CSS token values, component presence, fill/text color (RGB delta), typography (fontSize/fontWeight/lineHeight/fontFamily/letterSpacing), Auto Layout padding and gap, border-radius (per-corner), bounding-box overflow, absolute position drift (scroll-corrected x/y, 20px threshold), border stroke (color+weight), box-shadow (offset+blur+spread+color), opacity, and text content. Selector fallback: tries [data-testid], [aria-label], #id, .class per node. Requires FIGMA_API_TOKEN env var and Chrome on --remote-debugging-port=9222. Returns { findings, summary } where summary includes 13 mismatch-type counts.',
125
+ inputSchema: {
126
+ type: 'object',
127
+ properties: {
128
+ url: { type: 'string', description: 'Full URL of the page to audit (e.g. http://localhost:3000/dashboard). Must be reachable by the running Chrome instance.' },
129
+ figmaFrameUrl: { type: 'string', description: 'Figma frame URL to fetch design tokens from (e.g. https://www.figma.com/file/ABC123/Name?node-id=42%3A0). Must include the node-id query parameter pointing to the specific frame.' },
130
+ },
131
+ required: ['url', 'figmaFrameUrl'],
132
+ },
133
+ },
120
134
  ];
121
135
 
122
136
  // ── Helpers ───────────────────────────────────────────────────────────────────
@@ -129,7 +143,7 @@ async function withMcp(fn) {
129
143
  logger.error('[ARGUS] MCP tool handler error:', err.message);
130
144
  throw err;
131
145
  } finally {
132
- try { mcp.close(); } catch { /* ignore process already gone */ }
146
+ try { mcp.close(); } catch (e) { logger.debug({ err: e }, 'mcp close (ignored)'); }
133
147
  }
134
148
  }
135
149
 
@@ -268,6 +282,42 @@ async function handleGetContext({ url, snapshot_id: prevId, tabId } = {}) {
268
282
  });
269
283
  }
270
284
 
285
+ async function handleDesignAudit({ url, figmaFrameUrl }) {
286
+ if (!url) throw new Error('argus_design_audit: url is required');
287
+ if (!figmaFrameUrl) throw new Error('argus_design_audit: figmaFrameUrl is required');
288
+
289
+ const figmaData = await getFigmaFrame(figmaFrameUrl);
290
+ if (!figmaData) {
291
+ return { content: [{ type: 'text', text: JSON.stringify({
292
+ error: 'Could not fetch Figma data. Ensure FIGMA_API_TOKEN is set and the figmaFrameUrl is valid.',
293
+ findings: [],
294
+ summary: { tokenMismatches: 0, missingComponents: 0, colorMismatches: 0, typographyMismatches: 0, spacingMismatches: 0, radiusMismatches: 0, boundsOverflows: 0, positionDrifts: 0, strokeMismatches: 0, shadowMismatches: 0, opacityMismatches: 0, gapMismatches: 0, textMismatches: 0 },
295
+ }) }] };
296
+ }
297
+
298
+ return withMcp(async (mcp) => {
299
+ const browser = new CdpBrowserAdapter(mcp);
300
+ const findings = await analyzeDesignFidelity(browser, url, figmaData);
301
+ const count = (type) => findings.filter(f => f.type === type).length;
302
+ const summary = {
303
+ tokenMismatches: count('design_token_mismatch'),
304
+ missingComponents: count('design_component_missing'),
305
+ colorMismatches: count('design_color_mismatch'),
306
+ typographyMismatches: count('design_typography_mismatch'),
307
+ spacingMismatches: count('design_spacing_mismatch'),
308
+ radiusMismatches: count('design_radius_mismatch'),
309
+ boundsOverflows: count('design_bounds_overflow'),
310
+ positionDrifts: count('design_position_drift'),
311
+ strokeMismatches: count('design_stroke_mismatch'),
312
+ shadowMismatches: count('design_shadow_mismatch'),
313
+ opacityMismatches: count('design_opacity_mismatch'),
314
+ gapMismatches: count('design_gap_mismatch'),
315
+ textMismatches: count('design_text_mismatch'),
316
+ };
317
+ return { content: [{ type: 'text', text: JSON.stringify({ findings, summary }, null, 2) }] };
318
+ });
319
+ }
320
+
271
321
  async function handleLastReport() {
272
322
  if (!fs.existsSync(REPORTS_DIR)) {
273
323
  return { content: [{ type: 'text', text: '{"error":"No reports found in reports/"}' }] };
@@ -286,7 +336,7 @@ async function handleLastReport() {
286
336
  // ── Server bootstrap ──────────────────────────────────────────────────────────
287
337
 
288
338
  const server = new Server(
289
- { name: 'argus', version: '9.5.1' },
339
+ { name: 'argus', version: '9.5.5' },
290
340
  { capabilities: { tools: {} } },
291
341
  );
292
342
 
@@ -301,6 +351,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
301
351
  case 'argus_last_report': return await handleLastReport();
302
352
  case 'argus_watch_snapshot': return await handleWatchSnapshot(req.params.arguments ?? {});
303
353
  case 'argus_get_context': return await handleGetContext(req.params.arguments ?? {});
354
+ case 'argus_design_audit': return await handleDesignAudit(req.params.arguments ?? {});
304
355
  default: throw new Error(`Unknown tool: ${req.params.name}`);
305
356
  }
306
357
  } catch (err) {
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Argus Report Dispatcher (v9.3.0)
2
+ * Argus Report Dispatcher
3
3
  *
4
4
  * Dispatches a completed report to Slack, GitHub, and/or HTML.
5
5
  * Extracted from crawl-and-report.js god object.