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.
- package/.mcp.json +8 -0
- package/LICENSE +21 -0
- package/README.md +879 -0
- package/package.json +69 -0
- package/src/adapters/browser.js +82 -0
- package/src/argus.js +8 -0
- package/src/batch-runner.js +8 -0
- package/src/cli/init.js +314 -0
- package/src/config/schema.js +108 -0
- package/src/config/targets.js +309 -0
- package/src/domain/finding.js +25 -0
- package/src/mcp-server.js +156 -0
- package/src/orchestration/crawl-and-report.js +16 -0
- package/src/orchestration/dispatcher.js +263 -0
- package/src/orchestration/env-comparison.js +498 -0
- package/src/orchestration/orchestrator.js +1128 -0
- package/src/orchestration/report-processor.js +134 -0
- package/src/orchestration/slack-notifier.js +337 -0
- package/src/orchestration/watch-mode.js +316 -0
- package/src/registry.js +18 -0
- package/src/server/index.js +94 -0
- package/src/server/interaction-handler.js +126 -0
- package/src/server/slash-command-handler.js +185 -0
- package/src/utils/api-frequency.js +128 -0
- package/src/utils/baseline-manager.js +255 -0
- package/src/utils/codebase-analyzer.js +299 -0
- package/src/utils/content-analyzer.js +155 -0
- package/src/utils/contract-validator.js +178 -0
- package/src/utils/css-analyzer.js +407 -0
- package/src/utils/diff.js +189 -0
- package/src/utils/flakiness-detector.js +82 -0
- package/src/utils/flow-runner.js +572 -0
- package/src/utils/github-reporter.js +310 -0
- package/src/utils/hover-analyzer.js +214 -0
- package/src/utils/html-reporter.js +301 -0
- package/src/utils/issues-analyzer.js +171 -0
- package/src/utils/keyboard-analyzer.js +141 -0
- package/src/utils/lighthouse-checker.js +120 -0
- package/src/utils/logger.js +39 -0
- package/src/utils/login-orchestrator.js +99 -0
- package/src/utils/mcp-client.js +264 -0
- package/src/utils/mcp-parsers.js +57 -0
- package/src/utils/memory-analyzer.js +270 -0
- package/src/utils/network-timing-analyzer.js +76 -0
- package/src/utils/parallel-crawler.js +28 -0
- package/src/utils/responsive-analyzer.js +253 -0
- package/src/utils/retry.js +36 -0
- package/src/utils/route-discoverer.js +306 -0
- package/src/utils/security-analyzer.js +302 -0
- package/src/utils/seo-analyzer.js +164 -0
- package/src/utils/session-manager.js +12 -0
- package/src/utils/session-persistence.js +214 -0
- package/src/utils/severity-overrides.js +91 -0
- package/src/utils/slack-guard.js +18 -0
- package/src/utils/slug.js +8 -0
- package/src/utils/snapshot-analyzer.js +330 -0
- package/src/utils/telemetry.js +190 -0
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ARGUS Target Configuration
|
|
3
|
+
*
|
|
4
|
+
* Define which URLs to test, what flows to check, and per-route settings.
|
|
5
|
+
* Claude Code reads this file when building test runs.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { childLogger } from '../utils/logger.js';
|
|
9
|
+
const logger = childLogger('targets');
|
|
10
|
+
|
|
11
|
+
export const config = {
|
|
12
|
+
/** Milliseconds to wait after navigation before capturing state */
|
|
13
|
+
pageSettleMs: 2000,
|
|
14
|
+
|
|
15
|
+
/** Screenshot quality (1–100) */
|
|
16
|
+
screenshotQuality: 90,
|
|
17
|
+
|
|
18
|
+
/** Pixel diff % above which a visual change is flagged */
|
|
19
|
+
screenshotDiffThreshold: parseFloat(process.env.SCREENSHOT_DIFF_THRESHOLD ?? '0.5'),
|
|
20
|
+
|
|
21
|
+
/** Directory to write reports and screenshots */
|
|
22
|
+
outputDir: process.env.REPORT_OUTPUT_DIR ?? './reports',
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Centralized detection thresholds (v9.1.5).
|
|
27
|
+
* All magic-number limits across analyzers live here — change once, apply everywhere.
|
|
28
|
+
* Override individual values in your fork; never edit analyzer source files for tuning.
|
|
29
|
+
*/
|
|
30
|
+
export const thresholds = {
|
|
31
|
+
perf: {
|
|
32
|
+
LCP: 2500, // ms — Largest Contentful Paint
|
|
33
|
+
CLS: 0.1, // Cumulative Layout Shift
|
|
34
|
+
FID: 100, // ms — First Input Delay (approx via TBT)
|
|
35
|
+
TTFB: 800, // ms — Time to First Byte
|
|
36
|
+
},
|
|
37
|
+
network: {
|
|
38
|
+
slowWarning: 1000, // ms — API response time warning
|
|
39
|
+
slowCritical: 3000, // ms — API response time critical
|
|
40
|
+
sizeWarning: 500 * 1024, // bytes — payload size warning (500 KB)
|
|
41
|
+
sizeCritical: 2 * 1024 * 1024, // bytes — payload size critical (2 MB)
|
|
42
|
+
},
|
|
43
|
+
memory: {
|
|
44
|
+
detachedWarning: 10, // detached DOM nodes → warning
|
|
45
|
+
detachedCritical: 100, // detached DOM nodes → critical
|
|
46
|
+
heapGrowthWarning: 2 * 1024 * 1024, // bytes heap growth → warning (2 MB)
|
|
47
|
+
heapGrowthCritical: 10 * 1024 * 1024, // bytes heap growth → critical (10 MB)
|
|
48
|
+
},
|
|
49
|
+
hover: {
|
|
50
|
+
waitMs: 350, // ms to wait after hover before checking DOM state
|
|
51
|
+
maxDropdowns: 8, // max [aria-haspopup] elements to test per page
|
|
52
|
+
maxTooltips: 5, // max [data-tooltip] elements to test per page
|
|
53
|
+
},
|
|
54
|
+
security: {
|
|
55
|
+
headTimeoutMs: 3000, // ms — HEAD fetch timeout for CSP/XFrame header check
|
|
56
|
+
},
|
|
57
|
+
apiFrequency: {
|
|
58
|
+
warningCount: 3, // API called ≥ this many times → warning
|
|
59
|
+
criticalCount: 5, // API called ≥ this many times → critical
|
|
60
|
+
},
|
|
61
|
+
lighthouse: {
|
|
62
|
+
accessibility: { critical: 50, warning: 90 },
|
|
63
|
+
performance: { critical: 50, warning: 90 },
|
|
64
|
+
seo: { critical: 50, warning: 90 },
|
|
65
|
+
'best-practices': { critical: 50, warning: 90 },
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Routes to test in crawl-and-report.js (error detection).
|
|
71
|
+
* Add every key page your application serves.
|
|
72
|
+
*
|
|
73
|
+
* Fields:
|
|
74
|
+
* path — URL path appended to the base URL
|
|
75
|
+
* name — human-readable label for reports
|
|
76
|
+
* critical — if true, any error on this route is escalated to 'critical'
|
|
77
|
+
* waitFor — optional CSS selector to wait for before capturing (signals page ready)
|
|
78
|
+
*/
|
|
79
|
+
export const routes = [
|
|
80
|
+
{ path: '/', name: 'Home', critical: true, waitFor: 'main' },
|
|
81
|
+
{ path: '/login', name: 'Login', critical: true, waitFor: 'form' },
|
|
82
|
+
{ path: '/dashboard', name: 'Dashboard', critical: true, waitFor: '[data-testid="dashboard"]' },
|
|
83
|
+
{ path: '/settings', name: 'Settings', critical: false, waitFor: null },
|
|
84
|
+
// Add more routes here as your app grows
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Comparison route pairs for env-comparison.js.
|
|
89
|
+
* Each entry maps a dev path to the equivalent staging path (usually the same).
|
|
90
|
+
*/
|
|
91
|
+
export const comparisonRoutes = [
|
|
92
|
+
{ path: '/', name: 'Home' },
|
|
93
|
+
{ path: '/login', name: 'Login' },
|
|
94
|
+
{ path: '/dashboard', name: 'Dashboard' },
|
|
95
|
+
];
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* API endpoints to validate response shapes against (D7.4).
|
|
99
|
+
* Each entry is checked against captured network requests on every crawled route.
|
|
100
|
+
*
|
|
101
|
+
* Fields:
|
|
102
|
+
* url — path (e.g. '/api/user') or full URL for exact match
|
|
103
|
+
* method — HTTP method to match (optional — omit to match any method)
|
|
104
|
+
* schema — inline JSON Schema object (preferred)
|
|
105
|
+
* schemaFile — path to a JSON file containing the schema (alternative to schema)
|
|
106
|
+
*
|
|
107
|
+
* Supported schema keywords: type, required, properties, items.
|
|
108
|
+
*
|
|
109
|
+
* Violations are emitted as api_contract_violation warnings in the report.
|
|
110
|
+
*
|
|
111
|
+
* Examples:
|
|
112
|
+
* { url: '/api/user', method: 'GET', schema: { type: 'object', required: ['id', 'name'], properties: { id: { type: 'number' }, name: { type: 'string' } } } }
|
|
113
|
+
* { url: '/api/products', method: 'GET', schemaFile: './schemas/products.json' }
|
|
114
|
+
*/
|
|
115
|
+
export const apiContracts = [
|
|
116
|
+
// Uncomment and configure to validate API response shapes:
|
|
117
|
+
// { url: '/api/user', method: 'GET', schema: { type: 'object', required: ['id', 'name'], properties: { id: { type: 'number' }, name: { type: 'string' } } } },
|
|
118
|
+
// { url: '/api/products', method: 'GET', schemaFile: './schemas/products.json' },
|
|
119
|
+
];
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Severity policy overrides (D7.5).
|
|
123
|
+
* Post-processes all findings before Slack routing, letting teams adjust or
|
|
124
|
+
* silence specific detection types without editing analyzer source code.
|
|
125
|
+
*
|
|
126
|
+
* Keys are finding type strings (e.g. 'seo_missing_description').
|
|
127
|
+
* Values are one of: 'critical' | 'warning' | 'info' | 'suppress'
|
|
128
|
+
* 'suppress' removes the finding entirely from the report and Slack alerts.
|
|
129
|
+
*
|
|
130
|
+
* Examples:
|
|
131
|
+
* seo_missing_description: 'info' — demote noisy SEO finding to info
|
|
132
|
+
* cache_headers_missing: 'suppress' — silence entirely on this project
|
|
133
|
+
* redirect_chain: 'warning' — keep at warning (already is; no-op)
|
|
134
|
+
*/
|
|
135
|
+
export const severityOverrides = {
|
|
136
|
+
// seo_missing_description: 'info',
|
|
137
|
+
// cache_headers_missing: 'suppress',
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Auth session persistence (v3 Phase B2 / D7.6).
|
|
142
|
+
*
|
|
143
|
+
* When set, runCrawl() runs the login flow once before crawling, saves the
|
|
144
|
+
* session state (cookies + localStorage), and restores it before each route.
|
|
145
|
+
* This unlocks crawling of authenticated routes without re-logging in per page.
|
|
146
|
+
*
|
|
147
|
+
* D7.6 mid-run refresh: before each route, if the saved session has less than
|
|
148
|
+
* `sessionRefreshWindowMs` of validity remaining, the login flow is re-run
|
|
149
|
+
* automatically so long crawls never fail due to an expired auth cookie.
|
|
150
|
+
*
|
|
151
|
+
* Credentials MUST come from environment variables — never hardcode them here.
|
|
152
|
+
* Add ARGUS_AUTH_EMAIL and ARGUS_AUTH_PASSWORD to your .env file.
|
|
153
|
+
*
|
|
154
|
+
* Fields:
|
|
155
|
+
* sessionFile — path for the saved session JSON (default: .argus-session.json)
|
|
156
|
+
* sessionMaxAgeMs — max session lifetime before forcing re-login (default: 1 h)
|
|
157
|
+
* sessionRefreshWindowMs — refresh when this many ms remain before expiry (default: 5 min)
|
|
158
|
+
* steps — login flow steps (navigate, fill, click, waitFor, sleep)
|
|
159
|
+
*
|
|
160
|
+
* Set to null to disable auth (public crawl only).
|
|
161
|
+
*/
|
|
162
|
+
export const auth = null;
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* User flow definitions (v3 Phase B5).
|
|
166
|
+
*
|
|
167
|
+
* Each flow is a named sequence of steps executed end-to-end by flow-runner.js.
|
|
168
|
+
* Supported actions:
|
|
169
|
+
* navigate — { action: 'navigate', path: '/route' } or url: 'https://...' for absolute
|
|
170
|
+
* fill — { action: 'fill', selector: 'input#email', value: 'x@y.com' }
|
|
171
|
+
* Add typing: true to dispatch real keyboard events (needed for input-validation handlers)
|
|
172
|
+
* click — { action: 'click', selector: 'button[type=submit]' }
|
|
173
|
+
* press_key — { action: 'press_key', key: 'Tab' | 'Enter' | 'Escape' | 'ArrowDown' | ... }
|
|
174
|
+
* drag — { action: 'drag', sourceSelector: '.card', targetSelector: '.dropzone' }
|
|
175
|
+
* upload_file — { action: 'upload_file', selector: 'input[type=file]', filePath: '/abs/path' }
|
|
176
|
+
* select_option — { action: 'select_option', selector: 'select#country', value: 'US' }
|
|
177
|
+
* waitFor — { action: 'waitFor', selector: '#results', timeout: 10000 }
|
|
178
|
+
* sleep — { action: 'sleep', ms: 500 } (avoid; use waitFor instead)
|
|
179
|
+
* handle_dialog — { action: 'handle_dialog', accept: true } or accept: false
|
|
180
|
+
* assert — { action: 'assert', type: '...' } — see types below
|
|
181
|
+
*
|
|
182
|
+
* Assert types: no_console_errors, no_network_errors, element_visible, element_not_visible,
|
|
183
|
+
* url_contains, no_js_errors
|
|
184
|
+
*
|
|
185
|
+
* Set to [] to disable (default).
|
|
186
|
+
*/
|
|
187
|
+
export const flows = [];
|
|
188
|
+
|
|
189
|
+
// Uncomment and configure to test user journeys:
|
|
190
|
+
// export const flows = [
|
|
191
|
+
// {
|
|
192
|
+
// name: 'Login flow',
|
|
193
|
+
// steps: [
|
|
194
|
+
// { action: 'navigate', path: '/login' },
|
|
195
|
+
// { action: 'fill', selector: '#email', value: process.env.ARGUS_AUTH_EMAIL ?? '' },
|
|
196
|
+
// { action: 'fill', selector: '#password', value: process.env.ARGUS_AUTH_PASSWORD ?? '' },
|
|
197
|
+
// { action: 'click', selector: 'button[type="submit"]' },
|
|
198
|
+
// { action: 'waitFor', selector: '[data-testid="dashboard"]', timeout: 15000 },
|
|
199
|
+
// { action: 'assert', type: 'no_console_errors' },
|
|
200
|
+
// { action: 'assert', type: 'url_contains', value: '/dashboard' },
|
|
201
|
+
// ],
|
|
202
|
+
// },
|
|
203
|
+
// {
|
|
204
|
+
// name: 'Checkout flow',
|
|
205
|
+
// steps: [
|
|
206
|
+
// { action: 'navigate', path: '/cart' },
|
|
207
|
+
// { action: 'click', selector: '[data-testid="checkout-btn"]' },
|
|
208
|
+
// { action: 'waitFor', selector: '[data-testid="payment-form"]' },
|
|
209
|
+
// { action: 'assert', type: 'no_network_errors' },
|
|
210
|
+
// { action: 'assert', type: 'element_visible', selector: '[data-testid="order-summary"]' },
|
|
211
|
+
// ],
|
|
212
|
+
// },
|
|
213
|
+
// {
|
|
214
|
+
// name: 'Advanced interactions',
|
|
215
|
+
// steps: [
|
|
216
|
+
// { action: 'navigate', path: '/upload' },
|
|
217
|
+
// // Upload a file — resolves the file input by selector
|
|
218
|
+
// { action: 'upload_file', selector: 'input[type="file"]', filePath: '/tmp/test-file.pdf' },
|
|
219
|
+
// // Select a dropdown option
|
|
220
|
+
// { action: 'navigate', path: '/settings' },
|
|
221
|
+
// { action: 'select_option', selector: 'select#country', value: 'US' },
|
|
222
|
+
// // Drag and drop
|
|
223
|
+
// { action: 'navigate', path: '/kanban' },
|
|
224
|
+
// { action: 'drag', sourceSelector: '[data-testid="card-1"]', targetSelector: '[data-testid="done-column"]' },
|
|
225
|
+
// // Keyboard navigation test
|
|
226
|
+
// { action: 'navigate', path: '/menu' },
|
|
227
|
+
// { action: 'press_key', key: 'Tab' },
|
|
228
|
+
// { action: 'press_key', key: 'Enter' },
|
|
229
|
+
// { action: 'assert', type: 'element_visible', selector: '[data-testid="submenu"]' },
|
|
230
|
+
// // Type with real keyboard events (for inputs with keydown validators)
|
|
231
|
+
// { action: 'fill', selector: 'input[name="search"]', value: 'hello', typing: true },
|
|
232
|
+
// ],
|
|
233
|
+
// },
|
|
234
|
+
// ];
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Codebase cross-reference (Phase C1).
|
|
238
|
+
*
|
|
239
|
+
* Point sourceDir at your app's source code to enable:
|
|
240
|
+
* - env_var_missing — process.env.X used in code but absent from .env files
|
|
241
|
+
* - feature_flag_leakage — conditional env var that is falsy/unset in .env
|
|
242
|
+
* - error_source_linked — console error stack trace parsed to file:line (info)
|
|
243
|
+
* - dead_route — internal nav link that returns 404
|
|
244
|
+
*
|
|
245
|
+
* Set to null to disable (default).
|
|
246
|
+
*/
|
|
247
|
+
export const codebase = {
|
|
248
|
+
sourceDir: process.env.ARGUS_SOURCE_DIR ?? null,
|
|
249
|
+
envFile: process.env.ARGUS_ENV_FILE ?? null,
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
// Warn at startup when codebase analysis is unconfigured so operators know
|
|
253
|
+
// env_var_missing / feature_flag_leakage detections will be silently skipped.
|
|
254
|
+
if (!codebase.sourceDir) {
|
|
255
|
+
logger.warn('[ARGUS] codebase.sourceDir not configured — codebase analysis will be skipped (set ARGUS_SOURCE_DIR to enable)');
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Auto route discovery (Phase C3).
|
|
260
|
+
*
|
|
261
|
+
* Augments the manual routes[] above with paths discovered from:
|
|
262
|
+
* sitemap — fetches /sitemap.xml from the base URL; follows one level of sitemap index
|
|
263
|
+
* nextjs — scans pages/ (Next 12) and app/ (Next 13+) under codebase.sourceDir
|
|
264
|
+
* reactRouter — greps JS/TS source for <Route path="..."> and { path: "..." } patterns
|
|
265
|
+
*
|
|
266
|
+
* Discovered routes get: critical: false, waitFor: null, discovered: true.
|
|
267
|
+
* Manual route config (critical, waitFor, name) is always preserved.
|
|
268
|
+
* Set to null to disable auto-discovery entirely.
|
|
269
|
+
*/
|
|
270
|
+
export const autoDiscover = {
|
|
271
|
+
sitemap: true, // fetch /sitemap.xml from BASE_URL
|
|
272
|
+
nextjs: true, // scan pages/ + app/ under codebase.sourceDir (if set)
|
|
273
|
+
reactRouter: false, // grep source for React Router path declarations (experimental)
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* GitHub PR integration (Phase C2).
|
|
278
|
+
*
|
|
279
|
+
* All configuration is via environment variables — no settings object needed here.
|
|
280
|
+
*
|
|
281
|
+
* Required env vars (set in .env or GitHub Actions secrets):
|
|
282
|
+
* GITHUB_TOKEN — personal access token or Actions GITHUB_TOKEN
|
|
283
|
+
* GITHUB_REPOSITORY — "owner/repo" (set automatically in GitHub Actions)
|
|
284
|
+
*
|
|
285
|
+
* Optional env vars:
|
|
286
|
+
* GITHUB_SHA — commit SHA for status checks (auto in GitHub Actions)
|
|
287
|
+
* GITHUB_PR_NUMBER — PR number; add to your workflow:
|
|
288
|
+
* env:
|
|
289
|
+
* GITHUB_PR_NUMBER: ${{ github.event.pull_request.number }}
|
|
290
|
+
* ARGUS_REPORT_URL — URL to the full HTML report; linked from the commit status check
|
|
291
|
+
*
|
|
292
|
+
* When GITHUB_TOKEN + GITHUB_REPOSITORY are present, Argus will:
|
|
293
|
+
* - Post (or update) a findings summary comment on the open PR
|
|
294
|
+
* - Set a commit status check: 'failure' if new criticals exist, 'success' otherwise
|
|
295
|
+
*/
|
|
296
|
+
|
|
297
|
+
// Uncomment and configure for authenticated crawls:
|
|
298
|
+
// export const auth = {
|
|
299
|
+
// sessionFile: '.argus-session.json',
|
|
300
|
+
// sessionMaxAgeMs: 60 * 60 * 1000, // 1 hour — re-login after this
|
|
301
|
+
// sessionRefreshWindowMs: 5 * 60 * 1000, // refresh when < 5 min remain (D7.6)
|
|
302
|
+
// steps: [
|
|
303
|
+
// { action: 'navigate', path: '/login' },
|
|
304
|
+
// { action: 'fill', selector: '#email', value: process.env.ARGUS_AUTH_EMAIL ?? '' },
|
|
305
|
+
// { action: 'fill', selector: '#password', value: process.env.ARGUS_AUTH_PASSWORD ?? '' },
|
|
306
|
+
// { action: 'click', selector: 'button[type="submit"]' },
|
|
307
|
+
// { action: 'waitFor', selector: '[data-testid="dashboard"]', timeout: 15000 },
|
|
308
|
+
// ],
|
|
309
|
+
// };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Finding factory — enforces required fields and valid severity at creation time.
|
|
3
|
+
*
|
|
4
|
+
* All analyzer modules should build findings with createFinding() instead of
|
|
5
|
+
* raw object literals so missing fields throw at dev time rather than silently
|
|
6
|
+
* producing malformed reports.
|
|
7
|
+
*
|
|
8
|
+
* Object.freeze() prevents accidental mutation as findings pass through the
|
|
9
|
+
* dedup, severity-override, and baseline-diff pipeline.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const VALID_SEVERITIES = new Set(['critical', 'warning', 'info']);
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Create an immutable finding object.
|
|
16
|
+
*
|
|
17
|
+
* @param {{ type: string, severity: string, message: string, url?: string }} opts
|
|
18
|
+
* @returns {Readonly<object>}
|
|
19
|
+
*/
|
|
20
|
+
export function createFinding({ type, severity, message, url = '', ...rest }) {
|
|
21
|
+
if (!type) throw new Error(`Finding missing: type`);
|
|
22
|
+
if (!VALID_SEVERITIES.has(severity)) throw new Error(`Invalid severity "${severity}" for type "${type}"`);
|
|
23
|
+
if (!message) throw new Error(`Finding missing: message`);
|
|
24
|
+
return Object.freeze({ type, severity, message, url, ...rest });
|
|
25
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Argus MCP Server (v9.2.0)
|
|
4
|
+
*
|
|
5
|
+
* Exposes Argus as an MCP server so Claude (or any MCP client) can call
|
|
6
|
+
* argus_audit, argus_audit_full, argus_compare, and argus_last_report
|
|
7
|
+
* directly from a conversation without using the CLI.
|
|
8
|
+
*
|
|
9
|
+
* Architecture: MCP-inside-MCP
|
|
10
|
+
* Claude (MCP client)
|
|
11
|
+
* → Argus MCP Server (this file)
|
|
12
|
+
* → chrome-devtools-mcp client (mcp-client.js)
|
|
13
|
+
* → Chrome (CDP)
|
|
14
|
+
*
|
|
15
|
+
* Registration: add to .mcp.json as "argus" server.
|
|
16
|
+
* Run standalone: node src/mcp-server.js
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
20
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
21
|
+
import {
|
|
22
|
+
ListToolsRequestSchema,
|
|
23
|
+
CallToolRequestSchema,
|
|
24
|
+
} from '@modelcontextprotocol/sdk/types.js';
|
|
25
|
+
import fs from 'fs';
|
|
26
|
+
import path from 'path';
|
|
27
|
+
|
|
28
|
+
import { createMcpClient } from './utils/mcp-client.js';
|
|
29
|
+
import { crawlRouteCheap, runCrawl } from './orchestration/crawl-and-report.js';
|
|
30
|
+
import { runComparison } from './orchestration/env-comparison.js';
|
|
31
|
+
|
|
32
|
+
const REPORTS_DIR = path.resolve(process.cwd(), 'reports');
|
|
33
|
+
|
|
34
|
+
// ── Tool definitions ─────────────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
const TOOLS = [
|
|
37
|
+
{
|
|
38
|
+
name: 'argus_audit',
|
|
39
|
+
description: 'Run a quick (cheap) QA pass on a URL. Returns findings as JSON.',
|
|
40
|
+
inputSchema: {
|
|
41
|
+
type: 'object',
|
|
42
|
+
properties: {
|
|
43
|
+
url: { type: 'string', description: 'Full URL to audit (e.g. http://localhost:3000/checkout)' },
|
|
44
|
+
critical: { type: 'boolean', description: 'Treat this route as critical — console errors become critical severity', default: false },
|
|
45
|
+
},
|
|
46
|
+
required: ['url'],
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: 'argus_audit_full',
|
|
51
|
+
description: 'Run a deep QA pass on a URL using all analyzers — Lighthouse performance/accessibility scoring, responsive layout checks across mobile/tablet/desktop viewports, memory leak detection via heap snapshot, hover-state bug detection, and accessibility tree snapshot. Returns a full JSON report with findings grouped by severity.',
|
|
52
|
+
inputSchema: {
|
|
53
|
+
type: 'object',
|
|
54
|
+
properties: {
|
|
55
|
+
url: { type: 'string', description: 'Full URL to audit (e.g. https://example.com/dashboard)' },
|
|
56
|
+
critical: { type: 'boolean', description: 'Mark this route as critical — console errors are escalated to critical severity', default: false },
|
|
57
|
+
},
|
|
58
|
+
required: ['url'],
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
name: 'argus_compare',
|
|
63
|
+
description: 'Snapshot and diff two environments (dev vs staging) side-by-side. Navigates both URLs, captures screenshots, runs the full analyzer suite on each, then diffs the findings to surface regressions — things that appear in staging but not dev, or changed severity. Configure the two target URLs via TARGET_DEV_URL and TARGET_STAGING_URL environment variables before starting the server.',
|
|
64
|
+
inputSchema: { type: 'object', properties: {} },
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
name: 'argus_last_report',
|
|
68
|
+
description: 'Return the most recent Argus JSON report from the reports/ directory.',
|
|
69
|
+
inputSchema: { type: 'object', properties: {} },
|
|
70
|
+
},
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
async function withMcp(fn) {
|
|
76
|
+
const mcp = await createMcpClient();
|
|
77
|
+
try {
|
|
78
|
+
return await fn(mcp);
|
|
79
|
+
} finally {
|
|
80
|
+
try { mcp.close(); } catch { /* ignore — process already gone */ }
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ── Tool handlers ─────────────────────────────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
async function handleAudit({ url, critical = false }) {
|
|
87
|
+
return withMcp(async (mcp) => {
|
|
88
|
+
const parsed = new URL(url);
|
|
89
|
+
const route = { path: parsed.pathname + parsed.search + parsed.hash, name: 'audit', critical };
|
|
90
|
+
const findings = await crawlRouteCheap(route, parsed.origin, mcp);
|
|
91
|
+
return { content: [{ type: 'text', text: JSON.stringify(findings, null, 2) }] };
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function handleAuditFull({ url, critical = false }) {
|
|
96
|
+
return withMcp(async (mcp) => {
|
|
97
|
+
const parsed = new URL(url);
|
|
98
|
+
const report = await runCrawl(
|
|
99
|
+
mcp,
|
|
100
|
+
[{ path: parsed.pathname + parsed.search + parsed.hash, name: 'audit', critical }],
|
|
101
|
+
parsed.origin,
|
|
102
|
+
);
|
|
103
|
+
return { content: [{ type: 'text', text: JSON.stringify(report, null, 2) }] };
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function handleCompare() {
|
|
108
|
+
return withMcp(async (mcp) => {
|
|
109
|
+
const report = await runComparison(mcp);
|
|
110
|
+
return { content: [{ type: 'text', text: JSON.stringify(report, null, 2) }] };
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function handleLastReport() {
|
|
115
|
+
if (!fs.existsSync(REPORTS_DIR)) {
|
|
116
|
+
return { content: [{ type: 'text', text: '{"error":"No reports found in reports/"}' }] };
|
|
117
|
+
}
|
|
118
|
+
const files = fs.readdirSync(REPORTS_DIR).filter(f => f.endsWith('.json'));
|
|
119
|
+
if (files.length === 0) {
|
|
120
|
+
return { content: [{ type: 'text', text: '{"error":"No reports found in reports/"}' }] };
|
|
121
|
+
}
|
|
122
|
+
const latest = files
|
|
123
|
+
.map(f => ({ f, mt: fs.statSync(path.join(REPORTS_DIR, f)).mtimeMs }))
|
|
124
|
+
.sort((a, b) => b.mt - a.mt)[0].f;
|
|
125
|
+
const json = fs.readFileSync(path.join(REPORTS_DIR, latest), 'utf8');
|
|
126
|
+
return { content: [{ type: 'text', text: json }] };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ── Server bootstrap ──────────────────────────────────────────────────────────
|
|
130
|
+
|
|
131
|
+
const server = new Server(
|
|
132
|
+
{ name: 'argus', version: '9.2.0' },
|
|
133
|
+
{ capabilities: { tools: {} } },
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
|
|
137
|
+
|
|
138
|
+
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
139
|
+
try {
|
|
140
|
+
switch (req.params.name) {
|
|
141
|
+
case 'argus_audit': return await handleAudit(req.params.arguments ?? {});
|
|
142
|
+
case 'argus_audit_full': return await handleAuditFull(req.params.arguments ?? {});
|
|
143
|
+
case 'argus_compare': return await handleCompare();
|
|
144
|
+
case 'argus_last_report': return await handleLastReport();
|
|
145
|
+
default: throw new Error(`Unknown tool: ${req.params.name}`);
|
|
146
|
+
}
|
|
147
|
+
} catch (err) {
|
|
148
|
+
return {
|
|
149
|
+
content: [{ type: 'text', text: JSON.stringify({ error: err.message }) }],
|
|
150
|
+
isError: true,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const transport = new StdioServerTransport();
|
|
156
|
+
await server.connect(transport);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Argus Crawl Pipeline — backward-compat re-export shell (v9.2.0)
|
|
3
|
+
*
|
|
4
|
+
* The implementation has been split across three focused modules:
|
|
5
|
+
*
|
|
6
|
+
* orchestrator.js — crawl loop, route/flow crawl functions, runCrawl()
|
|
7
|
+
* report-processor.js — dedup, severity overrides, baseline, JSON write
|
|
8
|
+
* dispatcher.js — Slack / GitHub / HTML dispatch
|
|
9
|
+
*
|
|
10
|
+
* All callers (argus.js, batch-runner.js, server handlers, test-harness)
|
|
11
|
+
* continue to import from this file unchanged.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export { runCrawl, crawlRouteCheap, crawlRouteExpensive } from './orchestrator.js';
|
|
15
|
+
export { processReport, deduplicateFindings, rebuildSummary } from './report-processor.js';
|
|
16
|
+
export { dispatchAll } from './dispatcher.js';
|