argusqa-os 9.3.0 β 9.4.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/README.md +5 -5
- package/glama.json +29 -1
- package/package.json +1 -1
- package/src/adapters/browser.js +4 -0
- package/src/mcp-server.js +90 -15
- package/src/orchestration/watch-mode.js +161 -0
package/README.md
CHANGED
|
@@ -79,7 +79,7 @@ The `landing/` directory contains the product landing page (React + Vite + Tailw
|
|
|
79
79
|
|
|
80
80
|
| π΄ Critical / π‘ Warning / π΅ Info | βοΈ | π§ͺ | π |
|
|
81
81
|
| :---: | :---: | :---: | :---: |
|
|
82
|
-
| **114 distinct issue types detected** | **24 analysis engines** | **
|
|
82
|
+
| **114 distinct issue types detected** | **24 analysis engines** | **367 test assertions** | **84 test blocks** |
|
|
83
83
|
|
|
84
84
|
</div>
|
|
85
85
|
|
|
@@ -341,7 +341,7 @@ Argus watches your running application and automatically surfaces issues that te
|
|
|
341
341
|
| **GitHub PR Integration** | Posts a structured Markdown findings table as a PR comment (updates in-place β one comment per PR, no spam); sets an `argus-qa` commit status check (`failure` when new criticals exist, `success` otherwise) β blocks merge via branch protection when regressions are introduced. Requires `GITHUB_TOKEN` + `GITHUB_REPOSITORY` env vars |
|
|
342
342
|
| **Auto Route Discovery** | Augments manual `routes[]` with paths from three sources: fetches `/sitemap.xml` (follows one sitemap-index level, 10s timeout), scans Next.js `pages/` (Next 12) and `app/` (Next 13+) directories stripping route groups `(auth)`, and greps JS/TS source for React Router `<Route path>` declarations. Dynamic `[param]` segments are skipped β no concrete URL to crawl. Manual route config (`critical`, `waitFor`) always takes precedence. |
|
|
343
343
|
| **`argus init` Setup Wizard** | `npm run init` (or `npx argus init`) guides first-time setup: collects target URLs, detects the app framework (Next.js / React Router / unknown) from the source directory's `package.json`, runs C3 route discovery against the dev URL, prompts for optional Slack tokens and GitHub credentials, then writes a populated `.env` and a pre-filled `src/config/targets.js` β zero manual config editing required. |
|
|
344
|
-
| **Watch Mode** | `npm run watch` attaches to whatever Chrome tab is open and polls `list_console_messages` + `list_network_requests` every 1 s (configurable via `ARGUS_WATCH_INTERVAL_MS`). Reports new console errors, network failures (4xx/5xx), CORS blocks, and auth failures in real time β without navigating. On `Ctrl+C`, generates a final `reports/report.html`. No route config needed. |
|
|
344
|
+
| **Watch Mode** | `npm run watch` attaches to whatever Chrome tab is open and polls `list_console_messages` + `list_network_requests` every 1 s (configurable via `ARGUS_WATCH_INTERVAL_MS`). Reports new console errors, network failures (4xx/5xx), CORS blocks, and auth failures in real time β without navigating. Starts a live web dashboard at `http://localhost:3002` (configurable via `ARGUS_WATCH_UI_PORT`). On `Ctrl+C`, generates a final `reports/report.html`. No route config needed. |
|
|
345
345
|
| **Full Lighthouse Suite** | All 4 Lighthouse categories (performance, SEO, best-practices, accessibility) with per-audit items |
|
|
346
346
|
| **Performance Budgets** | Enforces LCP < 2500ms, CLS < 0.1, FID < 100ms, TTFB < 800ms per route |
|
|
347
347
|
| **Slack Notifications** | Rich Block Kit reports with inline screenshots routed to `#bugs-critical`, `#bugs-warnings`, `#bugs-digest` |
|
|
@@ -943,11 +943,11 @@ argus/
|
|
|
943
943
|
β βββ README.md # Setup guide, Supabase SQL schema, env vars, deployment
|
|
944
944
|
βββ scripts/
|
|
945
945
|
β βββ dispatch-report.js # Standalone Slack re-dispatch script (re-posts last report.json to Slack)
|
|
946
|
-
βββ test-harness/ # Fixture server + test runner (
|
|
946
|
+
βββ test-harness/ # Fixture server + test runner (84 blocks, 367 hard assertions, 54 fixture pages)
|
|
947
947
|
β βββ README.md
|
|
948
948
|
β βββ server.js # Express fixture server (ports 3100 dev / 3101 staging)
|
|
949
949
|
β βββ harness-config.js # Route definitions + expected findings
|
|
950
|
-
β βββ validate.js # Test runner β
|
|
950
|
+
β βββ validate.js # Test runner β 83 numbered blocks ([80] MCP server, [81] createFinding, [82] withRetry, [83] watch dashboard)
|
|
951
951
|
β βββ pages/ # 54 fixture pages (one per detection category)
|
|
952
952
|
β βββ nextjs-fixture/ # Next.js app structure for C3 discovery tests (10 files)
|
|
953
953
|
β βββ source-fixture/ # Minimal app.js for C1 codebase-analyzer tests (env var audit)
|
|
@@ -988,7 +988,7 @@ argus/
|
|
|
988
988
|
|
|
989
989
|
## Known MCP Tool Limitations
|
|
990
990
|
|
|
991
|
-
The Chrome DevTools MCP behavioral constraints below cause **3 permanent test failures** in the harness (`
|
|
991
|
+
The Chrome DevTools MCP behavioral constraints below cause **3 permanent test failures** in the harness (`364/367` pass). These are MCP-layer restrictions β they cannot be fixed in Argus code. `validate.js` now exits with code 0 when only these 3 failures remain, making the CI harness gate reliable.
|
|
992
992
|
|
|
993
993
|
> **`type_text` clarification**: `type_text` does fire DOM `input` events when the element is properly focused first with `mcp.click({ uid })`. Always use uid-based focus β passing `{ selector }` to `mcp.click` silently does nothing.
|
|
994
994
|
|
package/glama.json
CHANGED
|
@@ -1,4 +1,32 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://glama.ai/mcp/schemas/server.json",
|
|
3
|
-
"
|
|
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). 84 test blocks, 367 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
|
+
]
|
|
4
32
|
}
|
package/package.json
CHANGED
package/src/adapters/browser.js
CHANGED
|
@@ -56,6 +56,10 @@ export class CdpBrowserAdapter {
|
|
|
56
56
|
stopTrace() { return this._mcp.performance_stop_trace({}); }
|
|
57
57
|
analyzeInsight(opts) { return this._mcp.performance_analyze_insight(opts); }
|
|
58
58
|
|
|
59
|
+
// ββ Tab management βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
60
|
+
listPages() { return this._mcp.list_pages({}); }
|
|
61
|
+
selectPage(tabId) { return this._mcp.select_page({ pageId: tabId }); }
|
|
62
|
+
|
|
59
63
|
// ββ Lifecycle βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
60
64
|
close() { return this._mcp.close(); }
|
|
61
65
|
|
package/src/mcp-server.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* Argus MCP Server (v9.
|
|
3
|
+
* Argus MCP Server (v9.4.0)
|
|
4
4
|
*
|
|
5
5
|
* Exposes Argus as an MCP server so Claude (or any MCP client) can call
|
|
6
|
-
* argus_audit, argus_audit_full, argus_compare, and
|
|
7
|
-
* directly from a conversation without using the CLI.
|
|
6
|
+
* argus_audit, argus_audit_full, argus_compare, argus_last_report, and
|
|
7
|
+
* argus_get_context (fix loop) directly from a conversation without using the CLI.
|
|
8
8
|
*
|
|
9
9
|
* Architecture: MCP-inside-MCP
|
|
10
10
|
* Claude (MCP client)
|
|
@@ -33,17 +33,41 @@ import { CdpBrowserAdapter } from './adapters/browser.js';
|
|
|
33
33
|
|
|
34
34
|
const REPORTS_DIR = path.resolve(process.cwd(), 'reports');
|
|
35
35
|
|
|
36
|
+
// Fix loop: stores up to 20 snapshots keyed by snapshot_id so argus_get_context
|
|
37
|
+
// can diff before/after a fix. Keys are evicted oldest-first when the limit is hit.
|
|
38
|
+
const snapshotStore = new Map();
|
|
39
|
+
const MAX_SNAPSHOTS = 20;
|
|
40
|
+
|
|
41
|
+
// Audit cache: stores argus_audit results keyed by URL so cache:true skips re-crawl.
|
|
42
|
+
const auditCache = new Map();
|
|
43
|
+
const MAX_AUDIT_CACHE = 20;
|
|
44
|
+
|
|
45
|
+
function storeSnapshot(id, findings) {
|
|
46
|
+
snapshotStore.set(id, findings);
|
|
47
|
+
if (snapshotStore.size > MAX_SNAPSHOTS) {
|
|
48
|
+
snapshotStore.delete(snapshotStore.keys().next().value);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function cacheAudit(url, result) {
|
|
53
|
+
auditCache.set(url, { result, ts: Date.now() });
|
|
54
|
+
if (auditCache.size > MAX_AUDIT_CACHE) {
|
|
55
|
+
auditCache.delete(auditCache.keys().next().value);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
36
59
|
// ββ Tool definitions βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
37
60
|
|
|
38
61
|
const TOOLS = [
|
|
39
62
|
{
|
|
40
63
|
name: 'argus_audit',
|
|
41
|
-
description: 'Fast QA audit on a URL via Chrome DevTools Protocol. Runs 8 analyzers in one pass: JS errors, unhandled rejections, network failures (4xx/5xx), API frequency loops, CSS cascade issues, SEO violations, security header checks, and accessibility. Returns { findings: [{severity, type, message, url}], summary: {critical, warning, info} }. Use for CI smoke tests and pre-deploy gates. For Lighthouse scoring and memory leak detection, use argus_audit_full. Requires Chrome running with --remote-debugging-port=9222.',
|
|
64
|
+
description: 'Fast QA audit on a URL via Chrome DevTools Protocol. Runs 8 analyzers in one pass: JS errors, unhandled rejections, network failures (4xx/5xx), API frequency loops, CSS cascade issues, SEO violations, security header checks, and accessibility. Returns { findings: [{severity, type, message, url}], summary: {critical, warning, info} }. Use for CI smoke tests and pre-deploy gates. Pass cache: true to skip re-crawl on repeat calls to the same URL within a session β useful in tight fix loops. For Lighthouse scoring and memory leak detection, use argus_audit_full. Requires Chrome running with --remote-debugging-port=9222.',
|
|
42
65
|
inputSchema: {
|
|
43
66
|
type: 'object',
|
|
44
67
|
properties: {
|
|
45
68
|
url: { type: 'string', description: 'Full URL to audit, including protocol and path (e.g. http://localhost:3000/checkout). Must be reachable by the running Chrome instance.' },
|
|
46
69
|
critical: { type: 'boolean', description: 'When true, console.error calls are escalated to critical severity. Set true for business-critical routes (login, checkout, dashboard) where any error is a blocker.', default: false },
|
|
70
|
+
cache: { type: 'boolean', description: 'When true, returns the cached result for this URL if one exists (from a previous argus_audit call in this session) without re-crawling. Use in fix loops to cheaply re-read the last audit while iterating on a fix. Cache is per-session, max 20 entries, LRU eviction.', default: false },
|
|
47
71
|
},
|
|
48
72
|
required: ['url'],
|
|
49
73
|
},
|
|
@@ -72,21 +96,24 @@ const TOOLS = [
|
|
|
72
96
|
},
|
|
73
97
|
{
|
|
74
98
|
name: 'argus_watch_snapshot',
|
|
75
|
-
description: 'Snapshots the currently open Chrome tab without navigating β captures console errors, network failures (4xx/5xx), CORS blocks, and auth failures in one poll. Returns { findings: [{severity, type, message, url}], newConsole, newNetwork }. Use during active development to inspect what is happening on the current page without running a full audit. Requires Chrome on --remote-debugging-port=9222 with a page already open.',
|
|
99
|
+
description: 'Snapshots the currently open Chrome tab without navigating β captures console errors, network failures (4xx/5xx), CORS blocks, and auth failures in one poll. Returns { findings: [{severity, type, message, url}], newConsole, newNetwork }. Use during active development to inspect what is happening on the current page without running a full audit. Pass tabId to inspect a specific tab (get IDs from argus_get_context or list_pages). Without tabId, reads the active tab. Requires Chrome on --remote-debugging-port=9222 with a page already open.',
|
|
76
100
|
inputSchema: {
|
|
77
101
|
type: 'object',
|
|
78
102
|
properties: {
|
|
79
|
-
url:
|
|
103
|
+
url: { type: 'string', description: 'Optional base URL to attribute findings to (default: TARGET_DEV_URL env var). Does not navigate β reads the currently open Chrome tab.' },
|
|
104
|
+
tabId: { type: 'string', description: 'Optional Chrome page/tab ID (e.g. from a prior argus_get_context response). When provided, switches focus to that tab before snapshotting β useful for SPAs that spawn new windows or multi-tab flows.' },
|
|
80
105
|
},
|
|
81
106
|
},
|
|
82
107
|
},
|
|
83
108
|
{
|
|
84
109
|
name: 'argus_get_context',
|
|
85
|
-
description: 'Captures everything currently broken on the open Chrome tab and formats it as a diagnostic context for Claude to read and suggest fixes. Does NOT navigate β reads the live tab state after user interactions, in authenticated sessions, or mid-flow. Returns { summary, url, timestamp, critical_issues, warnings, js_errors, network_failures, console_errors, recent_requests }
|
|
110
|
+
description: 'Captures everything currently broken on the open Chrome tab and formats it as a diagnostic context for Claude to read and suggest fixes. Does NOT navigate β reads the live tab state after user interactions, in authenticated sessions, or mid-flow. Returns { snapshot_id, summary, url, timestamp, critical_issues, warnings, js_errors, network_failures, console_errors, recent_requests, open_tabs }. Fix loop: pass the snapshot_id from a previous call as snapshot_id to get a diff β the response will include resolved (cleared since last snapshot), new_issues (appeared since last snapshot), and persisting (unchanged). Multi-tab: pass tabId to inspect a specific tab, or omit to read the active tab. The open_tabs array always lists all currently open Chrome tabs. Workflow: call argus_get_context β Claude suggests fix β apply fix β call argus_get_context with snapshot_id β verify resolved array is non-empty. Requires Chrome on --remote-debugging-port=9222.',
|
|
86
111
|
inputSchema: {
|
|
87
112
|
type: 'object',
|
|
88
113
|
properties: {
|
|
89
|
-
url:
|
|
114
|
+
url: { type: 'string', description: 'Optional base URL to attribute findings to (default: TARGET_DEV_URL env var). Does not navigate β inspects the currently open Chrome tab.' },
|
|
115
|
+
snapshot_id: { type: 'string', description: 'Optional snapshot_id from a previous argus_get_context call. When provided, the response includes resolved/new_issues/persisting arrays showing what changed since that snapshot.' },
|
|
116
|
+
tabId: { type: 'string', description: 'Optional Chrome page/tab ID. When provided, switches focus to that specific tab before capturing context β useful for SPAs that spawn new windows (e.g. OAuth popups, checkout flows). Get tab IDs from the open_tabs array in a prior argus_get_context response.' },
|
|
90
117
|
},
|
|
91
118
|
},
|
|
92
119
|
},
|
|
@@ -105,11 +132,16 @@ async function withMcp(fn) {
|
|
|
105
132
|
|
|
106
133
|
// ββ Tool handlers βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
107
134
|
|
|
108
|
-
async function handleAudit({ url, critical = false }) {
|
|
135
|
+
async function handleAudit({ url, critical = false, cache = false }) {
|
|
136
|
+
if (cache && auditCache.has(url)) {
|
|
137
|
+
const { result, ts } = auditCache.get(url);
|
|
138
|
+
return { content: [{ type: 'text', text: JSON.stringify({ ...result, _cached: true, _cachedAt: new Date(ts).toISOString() }, null, 2) }] };
|
|
139
|
+
}
|
|
109
140
|
return withMcp(async (mcp) => {
|
|
110
141
|
const parsed = new URL(url);
|
|
111
142
|
const route = { path: parsed.pathname + parsed.search + parsed.hash, name: 'audit', critical };
|
|
112
143
|
const findings = await crawlRouteCheap(route, parsed.origin, mcp);
|
|
144
|
+
if (cache) cacheAudit(url, findings);
|
|
113
145
|
return { content: [{ type: 'text', text: JSON.stringify(findings, null, 2) }] };
|
|
114
146
|
});
|
|
115
147
|
}
|
|
@@ -133,9 +165,10 @@ async function handleCompare() {
|
|
|
133
165
|
});
|
|
134
166
|
}
|
|
135
167
|
|
|
136
|
-
async function handleWatchSnapshot({ url } = {}) {
|
|
168
|
+
async function handleWatchSnapshot({ url, tabId } = {}) {
|
|
137
169
|
return withMcp(async (mcp) => {
|
|
138
170
|
const browser = new CdpBrowserAdapter(mcp);
|
|
171
|
+
if (tabId) await browser.selectPage(tabId);
|
|
139
172
|
const baseUrl = url ?? process.env.TARGET_DEV_URL ?? 'http://localhost:3000';
|
|
140
173
|
const session = new WatchSession(browser, baseUrl);
|
|
141
174
|
const result = await session.poll();
|
|
@@ -143,26 +176,66 @@ async function handleWatchSnapshot({ url } = {}) {
|
|
|
143
176
|
});
|
|
144
177
|
}
|
|
145
178
|
|
|
146
|
-
async function handleGetContext({ url } = {}) {
|
|
179
|
+
async function handleGetContext({ url, snapshot_id: prevId, tabId } = {}) {
|
|
147
180
|
return withMcp(async (mcp) => {
|
|
148
181
|
const browser = new CdpBrowserAdapter(mcp);
|
|
182
|
+
if (tabId) await browser.selectPage(tabId);
|
|
149
183
|
const baseUrl = url ?? process.env.TARGET_DEV_URL ?? 'http://localhost:3000';
|
|
150
184
|
const session = new WatchSession(browser, baseUrl);
|
|
151
185
|
const { findings, newConsole, newNetwork } = await session.poll();
|
|
152
186
|
|
|
187
|
+
// List all open tabs so the caller can target a specific tab on the next call.
|
|
188
|
+
let open_tabs = [];
|
|
189
|
+
try {
|
|
190
|
+
const pages = await browser.listPages();
|
|
191
|
+
if (Array.isArray(pages)) {
|
|
192
|
+
open_tabs = pages.map(p => ({ id: p.id ?? p.pageId, url: p.url, title: p.title }));
|
|
193
|
+
}
|
|
194
|
+
} catch { /* list_pages not available in all Chrome configs β degrade gracefully */ }
|
|
195
|
+
|
|
196
|
+
const newId = Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
|
|
197
|
+
storeSnapshot(newId, findings);
|
|
198
|
+
|
|
153
199
|
const critical = findings.filter(f => f.severity === 'critical');
|
|
154
200
|
const warnings = findings.filter(f => f.severity === 'warning');
|
|
155
201
|
|
|
202
|
+
const findingKey = (f) => `${f.type}::${(f.message ?? '').slice(0, 120)}`;
|
|
203
|
+
|
|
204
|
+
let resolved = [];
|
|
205
|
+
let persisting = [];
|
|
206
|
+
let new_issues = findings;
|
|
207
|
+
const isDiff = prevId && snapshotStore.has(prevId);
|
|
208
|
+
|
|
209
|
+
if (isDiff) {
|
|
210
|
+
const prev = snapshotStore.get(prevId);
|
|
211
|
+
const prevKeys = new Set(prev.map(findingKey));
|
|
212
|
+
const curKeys = new Set(findings.map(findingKey));
|
|
213
|
+
resolved = prev.filter(f => !curKeys.has(findingKey(f)));
|
|
214
|
+
persisting = findings.filter(f => prevKeys.has(findingKey(f)));
|
|
215
|
+
new_issues = findings.filter(f => !prevKeys.has(findingKey(f)));
|
|
216
|
+
}
|
|
217
|
+
|
|
156
218
|
let summary;
|
|
157
|
-
if (
|
|
219
|
+
if (isDiff) {
|
|
220
|
+
if (resolved.length > 0 && critical.length === 0 && warnings.length === 0) {
|
|
221
|
+
summary = `All issues resolved on ${baseUrl}. ${resolved.length} finding${resolved.length > 1 ? 's' : ''} cleared since last snapshot.`;
|
|
222
|
+
} else if (resolved.length > 0) {
|
|
223
|
+
summary = `${resolved.length} issue${resolved.length > 1 ? 's' : ''} resolved on ${baseUrl}. ${new_issues.length} new + ${persisting.length} persisting (${critical.length} critical). Pass the new snapshot_id to continue the fix loop.`;
|
|
224
|
+
} else if (critical.length === 0 && warnings.length === 0) {
|
|
225
|
+
summary = `No issues on ${baseUrl} β console and network are clean.`;
|
|
226
|
+
} else {
|
|
227
|
+
summary = `No change on ${baseUrl}: ${critical.length} critical + ${warnings.length} warning${warnings.length !== 1 ? 's' : ''} still present. Check the persisting array for what hasn't been fixed.`;
|
|
228
|
+
}
|
|
229
|
+
} else if (critical.length === 0 && warnings.length === 0) {
|
|
158
230
|
summary = `No issues detected on ${baseUrl} β console and network are clean.`;
|
|
159
231
|
} else if (critical.length > 0) {
|
|
160
|
-
summary = `${critical.length} critical issue${critical.length > 1 ? 's' : ''} + ${warnings.length} warning${warnings.length !== 1 ? 's' : ''} detected on ${baseUrl}. Focus on critical issues first.`;
|
|
232
|
+
summary = `${critical.length} critical issue${critical.length > 1 ? 's' : ''} + ${warnings.length} warning${warnings.length !== 1 ? 's' : ''} detected on ${baseUrl}. Focus on critical issues first. Pass snapshot_id to the next argus_get_context call after applying a fix to see what changed.`;
|
|
161
233
|
} else {
|
|
162
|
-
summary = `${warnings.length} warning${warnings.length !== 1 ? 's' : ''} detected on ${baseUrl}. No critical errors.`;
|
|
234
|
+
summary = `${warnings.length} warning${warnings.length !== 1 ? 's' : ''} detected on ${baseUrl}. No critical errors. Pass snapshot_id to the next call to verify fixes.`;
|
|
163
235
|
}
|
|
164
236
|
|
|
165
237
|
const context = {
|
|
238
|
+
snapshot_id: newId,
|
|
166
239
|
summary,
|
|
167
240
|
url: baseUrl,
|
|
168
241
|
timestamp: new Date().toISOString(),
|
|
@@ -172,6 +245,8 @@ async function handleGetContext({ url } = {}) {
|
|
|
172
245
|
network_failures: findings.filter(f => f.type === 'network-error' || f.type === 'cors-error' || f.type === 'auth-error'),
|
|
173
246
|
console_errors: newConsole.filter(m => m.level === 'error' || m.level === 'warning'),
|
|
174
247
|
recent_requests: newNetwork.slice(-20),
|
|
248
|
+
open_tabs,
|
|
249
|
+
...(isDiff ? { resolved, new_issues, persisting } : {}),
|
|
175
250
|
};
|
|
176
251
|
|
|
177
252
|
return { content: [{ type: 'text', text: JSON.stringify(context, null, 2) }] };
|
|
@@ -196,7 +271,7 @@ async function handleLastReport() {
|
|
|
196
271
|
// ββ Server bootstrap ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
197
272
|
|
|
198
273
|
const server = new Server(
|
|
199
|
-
{ name: 'argus', version: '9.
|
|
274
|
+
{ name: 'argus', version: '9.4.0' },
|
|
200
275
|
{ capabilities: { tools: {} } },
|
|
201
276
|
);
|
|
202
277
|
|
|
@@ -15,10 +15,12 @@
|
|
|
15
15
|
* Environment variables:
|
|
16
16
|
* ARGUS_WATCH_INTERVAL_MS β poll interval in ms (default: 1000)
|
|
17
17
|
* TARGET_DEV_URL β base URL to monitor (default: http://localhost:3000)
|
|
18
|
+
* ARGUS_WATCH_UI_PORT β port for the live web dashboard (default: 3002)
|
|
18
19
|
*/
|
|
19
20
|
|
|
20
21
|
import fs from 'fs';
|
|
21
22
|
import path from 'path';
|
|
23
|
+
import http from 'http';
|
|
22
24
|
import { fileURLToPath } from 'url';
|
|
23
25
|
import 'dotenv/config';
|
|
24
26
|
import { createMcpClient } from '../utils/mcp-client.js';
|
|
@@ -41,6 +43,161 @@ import {
|
|
|
41
43
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
42
44
|
const REPORTS_DIR = path.resolve(__dirname, '../../reports');
|
|
43
45
|
|
|
46
|
+
// ββ Live dashboard βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
47
|
+
|
|
48
|
+
const DASHBOARD_HTML = `<!DOCTYPE html>
|
|
49
|
+
<html lang="en">
|
|
50
|
+
<head>
|
|
51
|
+
<meta charset="UTF-8">
|
|
52
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
53
|
+
<title>Argus Watch Dashboard</title>
|
|
54
|
+
<style>
|
|
55
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
56
|
+
body { background: #0d0d0d; color: #e0e0e0; font-family: 'Segoe UI', system-ui, sans-serif; min-height: 100vh; }
|
|
57
|
+
header { background: #111; border-bottom: 1px solid #1e1e1e; padding: 14px 24px; display: flex; align-items: center; gap: 12px; }
|
|
58
|
+
.pulse-dot { width: 10px; height: 10px; border-radius: 50%; background: #5E0ED7; flex-shrink: 0;
|
|
59
|
+
animation: pulse 1.8s ease-in-out infinite; }
|
|
60
|
+
@keyframes pulse { 0%,100%{opacity:1;transform:scale(1)} 50%{opacity:.4;transform:scale(1.3)} }
|
|
61
|
+
.header-text { flex: 1; }
|
|
62
|
+
.header-text h1 { font-size: 15px; font-weight: 600; color: #fff; letter-spacing: .02em; }
|
|
63
|
+
.header-text small { font-size: 11px; color: #666; }
|
|
64
|
+
.pills { display: flex; gap: 8px; flex-wrap: wrap; padding: 16px 24px; }
|
|
65
|
+
.pill { font-size: 12px; font-weight: 600; padding: 4px 12px; border-radius: 999px; letter-spacing: .04em; }
|
|
66
|
+
.pill-critical { background: #3a0e0e; color: #f87171; border: 1px solid #7f1d1d; }
|
|
67
|
+
.pill-warning { background: #2e2200; color: #fbbf24; border: 1px solid #78350f; }
|
|
68
|
+
.pill-info { background: #1a0b33; color: #a78bfa; border: 1px solid #4c1d95; }
|
|
69
|
+
.pill-clear { background: #0b2718; color: #34d399; border: 1px solid #064e3b; }
|
|
70
|
+
.status-bar { font-size: 11px; padding: 4px 24px 12px; color: #555; }
|
|
71
|
+
.status-bar.error { color: #f87171; }
|
|
72
|
+
table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
|
73
|
+
thead th { background: #161616; color: #888; font-weight: 600; text-align: left;
|
|
74
|
+
padding: 10px 16px; border-bottom: 1px solid #222; font-size: 11px; text-transform: uppercase; letter-spacing: .06em; }
|
|
75
|
+
tbody tr { border-bottom: 1px solid #181818; transition: background .15s; }
|
|
76
|
+
tbody tr:hover { background: #151515; }
|
|
77
|
+
td { padding: 10px 16px; vertical-align: top; }
|
|
78
|
+
.sev { font-size: 11px; font-weight: 700; padding: 2px 8px; border-radius: 4px; white-space: nowrap; }
|
|
79
|
+
.sev-critical { background: #3a0e0e; color: #f87171; }
|
|
80
|
+
.sev-warning { background: #2e2200; color: #fbbf24; }
|
|
81
|
+
.sev-info { background: #1a0b33; color: #a78bfa; }
|
|
82
|
+
.type-cell { color: #9ca3af; font-size: 12px; font-family: monospace; white-space: nowrap; }
|
|
83
|
+
.msg-cell { color: #d1d5db; word-break: break-word; max-width: 480px; }
|
|
84
|
+
.empty-row td { text-align: center; color: #444; padding: 40px; font-size: 13px; }
|
|
85
|
+
.table-wrap { padding: 0 24px 24px; overflow-x: auto; }
|
|
86
|
+
</style>
|
|
87
|
+
</head>
|
|
88
|
+
<body>
|
|
89
|
+
<header>
|
|
90
|
+
<div class="pulse-dot" id="dot"></div>
|
|
91
|
+
<div class="header-text">
|
|
92
|
+
<h1 id="target">Argus Watch</h1>
|
|
93
|
+
<small id="lastPoll">Connectingβ¦</small>
|
|
94
|
+
</div>
|
|
95
|
+
</header>
|
|
96
|
+
<div class="pills" id="pills"></div>
|
|
97
|
+
<div class="status-bar" id="status"></div>
|
|
98
|
+
<div class="table-wrap">
|
|
99
|
+
<table>
|
|
100
|
+
<thead><tr><th>Severity</th><th>Type</th><th>Message</th></tr></thead>
|
|
101
|
+
<tbody id="tbody"></tbody>
|
|
102
|
+
</table>
|
|
103
|
+
</div>
|
|
104
|
+
<script>
|
|
105
|
+
const SEV_ORDER = { critical: 0, warning: 1, info: 2 };
|
|
106
|
+
|
|
107
|
+
function renderPills(findings) {
|
|
108
|
+
const counts = { critical: 0, warning: 0, info: 0 };
|
|
109
|
+
for (const f of findings) counts[f.severity] = (counts[f.severity] || 0) + 1;
|
|
110
|
+
const pills = document.getElementById('pills');
|
|
111
|
+
if (findings.length === 0) {
|
|
112
|
+
pills.innerHTML = '<span class="pill pill-clear">All clear</span>';
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
let html = '';
|
|
116
|
+
if (counts.critical) html += \`<span class="pill pill-critical">\${counts.critical} Critical</span>\`;
|
|
117
|
+
if (counts.warning) html += \`<span class="pill pill-warning">\${counts.warning} Warning</span>\`;
|
|
118
|
+
if (counts.info) html += \`<span class="pill pill-info">\${counts.info} Info</span>\`;
|
|
119
|
+
pills.innerHTML = html;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function renderTable(findings) {
|
|
123
|
+
const sorted = [...findings].sort((a, b) =>
|
|
124
|
+
(SEV_ORDER[a.severity] ?? 3) - (SEV_ORDER[b.severity] ?? 3));
|
|
125
|
+
const tbody = document.getElementById('tbody');
|
|
126
|
+
if (sorted.length === 0) {
|
|
127
|
+
tbody.innerHTML = '<tr class="empty-row"><td colspan="3">No findings yet</td></tr>';
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
tbody.innerHTML = sorted.map(f => {
|
|
131
|
+
const sc = 'sev-' + (f.severity || 'info');
|
|
132
|
+
const msg = (f.message || '').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
|
133
|
+
const typ = (f.type || '').replace(/&/g,'&');
|
|
134
|
+
return \`<tr>
|
|
135
|
+
<td><span class="sev \${sc}">\${f.severity ?? 'info'}</span></td>
|
|
136
|
+
<td class="type-cell">\${typ}</td>
|
|
137
|
+
<td class="msg-cell">\${msg}</td>
|
|
138
|
+
</tr>\`;
|
|
139
|
+
}).join('');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function poll() {
|
|
143
|
+
try {
|
|
144
|
+
const res = await fetch('/data');
|
|
145
|
+
if (!res.ok) throw new Error('HTTP ' + res.status);
|
|
146
|
+
const data = await res.json();
|
|
147
|
+
document.getElementById('target').textContent = 'Argus Watch β ' + (data.target || '');
|
|
148
|
+
document.getElementById('lastPoll').textContent = 'Last poll: ' + new Date(data.lastPoll).toLocaleTimeString();
|
|
149
|
+
document.getElementById('dot').style.background = '#5E0ED7';
|
|
150
|
+
document.getElementById('status').textContent = '';
|
|
151
|
+
document.getElementById('status').className = 'status-bar';
|
|
152
|
+
renderPills(data.findings || []);
|
|
153
|
+
renderTable(data.findings || []);
|
|
154
|
+
} catch (e) {
|
|
155
|
+
document.getElementById('status').textContent = 'Connection lost β ' + e.message;
|
|
156
|
+
document.getElementById('status').className = 'status-bar error';
|
|
157
|
+
document.getElementById('dot').style.background = '#555';
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
poll();
|
|
162
|
+
setInterval(poll, 2000);
|
|
163
|
+
</script>
|
|
164
|
+
</body>
|
|
165
|
+
</html>`;
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Start the live web dashboard HTTP server.
|
|
169
|
+
*
|
|
170
|
+
* @param {() => object[]} getFindings β callback that returns current findings array
|
|
171
|
+
* @param {string} target β the URL being monitored (shown in header)
|
|
172
|
+
* @param {number} port β TCP port to listen on (default 3002)
|
|
173
|
+
* @returns {http.Server}
|
|
174
|
+
*/
|
|
175
|
+
function startDashboard(getFindings, target, port) {
|
|
176
|
+
const server = http.createServer((req, res) => {
|
|
177
|
+
if (req.url === '/data' || req.url?.startsWith('/data?')) {
|
|
178
|
+
const payload = JSON.stringify({
|
|
179
|
+
target,
|
|
180
|
+
lastPoll: new Date().toISOString(),
|
|
181
|
+
findings: getFindings(),
|
|
182
|
+
});
|
|
183
|
+
res.writeHead(200, {
|
|
184
|
+
'Content-Type': 'application/json',
|
|
185
|
+
'Access-Control-Allow-Origin': '*',
|
|
186
|
+
});
|
|
187
|
+
res.end(payload);
|
|
188
|
+
} else {
|
|
189
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
190
|
+
res.end(DASHBOARD_HTML);
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
server.listen(port, '127.0.0.1', () => {
|
|
195
|
+
logger.info(`[ARGUS WATCH] Dashboard β http://localhost:${port}`);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
return server;
|
|
199
|
+
}
|
|
200
|
+
|
|
44
201
|
// ββ Deduplication key generators βββββββββββββββββββββββββββββββββββββββββββββββ
|
|
45
202
|
// Two messages/requests are considered "the same" if their keys match. This
|
|
46
203
|
// prevents re-reporting errors that were already captured in a previous poll.
|
|
@@ -228,6 +385,9 @@ export async function runWatchMode(baseUrl) {
|
|
|
228
385
|
logger.info(`[ARGUS WATCH] Polling every ${pollIntervalMs}ms. Press Ctrl+C to stop.`);
|
|
229
386
|
logger.info('[ARGUS WATCH] βββββββββββββββββββββββββββββββββββββββββββββββββ\n');
|
|
230
387
|
|
|
388
|
+
const uiPort = parseInt(process.env.ARGUS_WATCH_UI_PORT ?? '3002', 10);
|
|
389
|
+
const dashServer = startDashboard(() => session.getAllFindings(), target, uiPort);
|
|
390
|
+
|
|
231
391
|
const badge = (severity) =>
|
|
232
392
|
severity === 'critical' ? 'β CRIT' :
|
|
233
393
|
severity === 'warning' ? '! WARN' : 'i INFO';
|
|
@@ -270,6 +430,7 @@ export async function runWatchMode(baseUrl) {
|
|
|
270
430
|
|
|
271
431
|
process.on('SIGINT', async () => {
|
|
272
432
|
clearInterval(interval);
|
|
433
|
+
try { dashServer.close(); } catch { /* ignore */ }
|
|
273
434
|
const all = session.getAllFindings();
|
|
274
435
|
|
|
275
436
|
logger.info(`\n[ARGUS WATCH] Stopped. Total findings: ${all.length}`);
|