aegis_auto 1.0.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 ADDED
@@ -0,0 +1,77 @@
1
+ # aegis_auto
2
+
3
+ Autonomous synthetic monitoring — record user journeys, replay with observability, diagnose failures with AI, and dispatch reports to your endpoint.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install aegis_auto
9
+ ```
10
+
11
+ > **Requires Node.js ≥ 18** (uses native `fetch`).
12
+ > Playwright browsers are installed automatically on first run.
13
+
14
+ ## Quick Start — Shadow Mode (Zero Manual Steps)
15
+
16
+ ```bash
17
+ npx aegis-shadow https://your-app.com
18
+ ```
19
+
20
+ This single command runs the full lifecycle:
21
+
22
+ | Phase | What Happens |
23
+ |-------|---|
24
+ | 1. **Record** (30s) | Opens headed Chromium, captures user interactions with a live progress bar |
25
+ | 2. **Simulate** | Replays the golden recording with HAR capture, console/network sentinels, latency comparison |
26
+ | 3. **Diagnose** | AI-powered root cause analysis via Groq LLM (skipped if PASS) |
27
+ | 4. **Dispatch** | POSTs `run_summary` + `incident_report` to your endpoint |
28
+
29
+ ### Configuration (.env)
30
+
31
+ ```env
32
+ GROQ_API_KEY=gsk_your_key_here # Required for AI diagnostics
33
+ WEBHOOK_URL=https://api.example.com # Optional — auto-POST results here
34
+ RECORD_DURATION=30 # Optional — recording time in seconds
35
+ ```
36
+
37
+ ## Programmatic Usage
38
+
39
+ ```js
40
+ import { SessionRecorder } from 'aegis_auto/recorder';
41
+
42
+ const recorder = new SessionRecorder({
43
+ outputDir: './my-sessions',
44
+ });
45
+
46
+ await recorder.start('https://your-app.com');
47
+ // ... user interacts ...
48
+ const sessionPath = await recorder.stop();
49
+ console.log('Session saved to:', sessionPath);
50
+ ```
51
+
52
+ ## CLI Commands
53
+
54
+ ```bash
55
+ # Full autonomous pipeline
56
+ npx aegis-shadow https://your-app.com
57
+
58
+ # Individual tools
59
+ npx aegis-monitor ./sessions/golden.json # Replay + observe
60
+ npx aegis-diagnose --logs-dir ./logs # AI analysis
61
+ npx aegis-diagnose --codebase ./src # AI analysis with file-level mapping
62
+ ```
63
+
64
+ ## Output Files
65
+
66
+ | File | Contents |
67
+ |---|---|
68
+ | `sessions/session-*.json` | Golden recording (actions + deduplicated errors) |
69
+ | `logs/run_summary.json` | Step-by-step pass/fail with latency data |
70
+ | `logs/anomalies.json` | HTTP ≥ 400 responses during replay |
71
+ | `logs/crash_report.json` | Console errors + failing selectors |
72
+ | `logs/network.har` | Full HAR network trace |
73
+ | `logs/incident_report.md` | AI-generated incident analysis |
74
+
75
+ ## License
76
+
77
+ MIT
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "aegis_auto",
3
+ "version": "1.0.0",
4
+ "description": "Autonomous synthetic monitoring — record user journeys, replay with observability, diagnose failures with AI, and dispatch reports to your endpoint.",
5
+ "type": "module",
6
+ "main": "src/index.js",
7
+ "exports": {
8
+ ".": "./src/index.js",
9
+ "./recorder": "./src/recorder.js"
10
+ },
11
+ "bin": {
12
+ "aegis-shadow": "src/aegis-shadow.js",
13
+ "aegis-monitor": "src/monitor.js",
14
+ "aegis-diagnose": "src/diagnostics.js"
15
+ },
16
+ "scripts": {
17
+ "start": "node src/index.js",
18
+ "shadow": "node src/aegis-shadow.js",
19
+ "monitor": "node src/monitor.js",
20
+ "diagnose": "node src/diagnostics.js"
21
+ },
22
+ "keywords": [
23
+ "synthetic-monitoring",
24
+ "playwright",
25
+ "session-recorder",
26
+ "error-tracking",
27
+ "AI-diagnostics",
28
+ "observability",
29
+ "SRE"
30
+ ],
31
+ "author": "Arun Kumar",
32
+ "license": "MIT",
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "https://github.com/arun-kumar-24/auto-aegis.git"
36
+ },
37
+ "engines": {
38
+ "node": ">=18.0.0"
39
+ },
40
+ "files": [
41
+ "src/",
42
+ "README.md",
43
+ "LICENSE"
44
+ ],
45
+ "dependencies": {
46
+ "chalk": "^5.6.2",
47
+ "ora": "^9.3.0",
48
+ "playwright": "^1.58.2",
49
+ "uuid": "^11.0.0"
50
+ }
51
+ }
@@ -0,0 +1,378 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Aegis Shadow — Autonomous Lifecycle Orchestrator
4
+ *
5
+ * A single file that manages the full lifecycle:
6
+ * Start → Track (30s) → Simulate → Report → Dispatch
7
+ *
8
+ * Usage:
9
+ * node src/aegis-shadow.js https://example.com
10
+ * npm run shadow -- https://example.com
11
+ *
12
+ * Environment:
13
+ * WEBHOOK_URL — POST endpoint for dispatching results
14
+ * GROQ_API_KEY — For AI diagnostics (loaded from .env)
15
+ * RECORD_DURATION — Override recording duration in seconds (default: 30)
16
+ */
17
+
18
+ import chalk from 'chalk';
19
+ import ora from 'ora';
20
+ import { SessionRecorder } from './recorder.js';
21
+ import { spawn } from 'node:child_process';
22
+ import { readFileSync, existsSync } from 'node:fs';
23
+ import { resolve, join, dirname } from 'node:path';
24
+ import { fileURLToPath } from 'node:url';
25
+
26
+ // ── Paths ──────────────────────────────────────────────────────────────
27
+
28
+ const __filename = fileURLToPath(import.meta.url);
29
+ const __dirname = dirname(__filename);
30
+ const DOM_DIR = resolve(__dirname, '..');
31
+
32
+ // ── Config ─────────────────────────────────────────────────────────────
33
+
34
+ loadEnvFile();
35
+
36
+ const RECORD_DURATION_S = parseInt(process.env.RECORD_DURATION || '30', 10);
37
+ const WEBHOOK_URL = process.env.WEBHOOK_URL || '';
38
+ const START_URL = process.argv[2] || null;
39
+ const CODEBASE_DIR = parseFlag('--codebase') || DOM_DIR;
40
+
41
+ // ── Main ───────────────────────────────────────────────────────────────
42
+
43
+ async function main() {
44
+ console.clear();
45
+ printBanner();
46
+
47
+ if (!START_URL) {
48
+ console.log(chalk.red.bold(' ✖ No URL provided.\n'));
49
+ console.log(chalk.dim(' Usage: npm run shadow -- https://example.com\n'));
50
+ process.exit(1);
51
+ }
52
+
53
+ let goldenPath = null;
54
+ let summaryPath = null;
55
+ let reportPath = null;
56
+
57
+ try {
58
+ // ━━━ Phase 1: Shadow Recording ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
59
+ goldenPath = await phaseRecord(START_URL);
60
+
61
+ // Check if we have any actions to simulate
62
+ const golden = JSON.parse(readFileSync(goldenPath, 'utf-8'));
63
+ const actionCount = (golden.actions || []).length;
64
+
65
+ if (actionCount === 0) {
66
+ console.log(chalk.yellow.bold('\n ⚠ No interactions captured during recording.'));
67
+ console.log(chalk.dim(' User did not interact with the page.\n'));
68
+ } else {
69
+ // ━━━ Phase 2: Stress-Test Simulation ━━━━━━━━━━━━━━━━━━━━━━━━━━
70
+ summaryPath = await phaseSimulate(goldenPath);
71
+
72
+ // ━━━ Phase 3: AI Diagnostics ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
73
+ reportPath = await phaseDiagnose();
74
+ }
75
+ } finally {
76
+ // ━━━ Phase 4: Dispatcher ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
77
+ await phaseDispatch(summaryPath, reportPath);
78
+ printFooter(goldenPath, summaryPath, reportPath);
79
+ }
80
+ }
81
+
82
+ main().catch((err) => {
83
+ console.error(chalk.red.bold(`\n ✖ Fatal Error: ${err.message}\n`));
84
+ process.exit(1);
85
+ });
86
+
87
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
88
+ // Phase 1: Shadow Recording
89
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
90
+
91
+ async function phaseRecord(url) {
92
+ printPhaseHeader(1, 'Shadow Recording', 'cyan');
93
+
94
+ const recorder = new SessionRecorder({
95
+ outputDir: resolve(DOM_DIR, 'sessions'),
96
+ });
97
+
98
+ await recorder.start(url);
99
+
100
+ // ── 30s countdown with progress bar ─────────────────────────────────
101
+
102
+ const spinner = ora({
103
+ text: progressText(0, RECORD_DURATION_S),
104
+ color: 'cyan',
105
+ prefixText: ' ',
106
+ }).start();
107
+
108
+ let elapsed = 0;
109
+ let browserClosed = false;
110
+
111
+ // Detect if the user closes the browser early
112
+ recorder.context.on('close', () => {
113
+ browserClosed = true;
114
+ });
115
+
116
+ await new Promise((done) => {
117
+ const tick = setInterval(() => {
118
+ elapsed++;
119
+ spinner.text = progressText(elapsed, RECORD_DURATION_S);
120
+
121
+ if (elapsed >= RECORD_DURATION_S || browserClosed) {
122
+ clearInterval(tick);
123
+ done();
124
+ }
125
+ }, 1000);
126
+ });
127
+
128
+ if (browserClosed) {
129
+ spinner.info(chalk.dim('Browser closed by user — saving session.'));
130
+ } else {
131
+ spinner.succeed(chalk.green('Recording window complete.'));
132
+ }
133
+
134
+ // Stop the recorder — saves golden JSON + closes browser
135
+ const outputPath = await recorder.stop();
136
+
137
+ console.log(chalk.green.bold(' ✔ User Journey Captured'));
138
+ console.log(chalk.dim(` → ${outputPath}\n`));
139
+
140
+ return outputPath;
141
+ }
142
+
143
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
144
+ // Phase 2: Stress-Test Simulation
145
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
146
+
147
+ async function phaseSimulate(goldenPath) {
148
+ printPhaseHeader(2, 'Stress-Test Simulation', 'yellow');
149
+
150
+ console.log(chalk.yellow(' 🚀 Recording Complete. Starting Stress-Test Simulation...\n'));
151
+
152
+ const spinner = ora({
153
+ text: chalk.yellow('Performing simulation in background...'),
154
+ color: 'yellow',
155
+ prefixText: ' ',
156
+ }).start();
157
+
158
+ try {
159
+ await runChildScript('monitor.js', [goldenPath]);
160
+ spinner.succeed(chalk.green('Simulation complete.'));
161
+ } catch (err) {
162
+ spinner.warn(chalk.yellow('Simulation completed with issues.'));
163
+ console.log(chalk.dim(` ${err.message.split('\n')[0]}`));
164
+ }
165
+
166
+ // Show run summary stats
167
+ const summaryPath = resolve(DOM_DIR, 'logs', 'run_summary.json');
168
+ if (existsSync(summaryPath)) {
169
+ const summary = JSON.parse(readFileSync(summaryPath, 'utf-8'));
170
+ const statusIcon = summary.status === 'PASS' ? chalk.green('✔ PASS') : chalk.red('✖ FAIL');
171
+ console.log(chalk.dim(` Status : ${statusIcon}`));
172
+ console.log(chalk.dim(` Steps : ${summary.passedSteps}/${summary.totalSteps} passed`));
173
+ console.log(chalk.dim(` Latency : ${summary.performanceSummary?.totalLatency}ms`));
174
+ console.log();
175
+ }
176
+
177
+ return summaryPath;
178
+ }
179
+
180
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
181
+ // Phase 3: AI Diagnostics
182
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
183
+
184
+ async function phaseDiagnose() {
185
+ printPhaseHeader(3, 'AI Diagnostics', 'magenta');
186
+
187
+ const summaryPath = resolve(DOM_DIR, 'logs', 'run_summary.json');
188
+ const reportPath = resolve(DOM_DIR, 'logs', 'incident_report.md');
189
+
190
+ if (!existsSync(summaryPath)) {
191
+ console.log(chalk.dim(' No run summary found. Skipping diagnostics.\n'));
192
+ return null;
193
+ }
194
+
195
+ // If the run passed, skip expensive AI call
196
+ const summary = JSON.parse(readFileSync(summaryPath, 'utf-8'));
197
+ if (summary.status === 'PASS') {
198
+ console.log(chalk.green.bold(' ✔ All steps passed — no incidents to diagnose.\n'));
199
+ return null;
200
+ }
201
+
202
+ const spinner = ora({
203
+ text: chalk.magenta('Generating AI incident report via Groq...'),
204
+ color: 'magenta',
205
+ prefixText: ' ',
206
+ }).start();
207
+
208
+ try {
209
+ await runChildScript('diagnostics.js', ['--codebase', CODEBASE_DIR]);
210
+ spinner.succeed(chalk.green('Incident report generated.'));
211
+ console.log(chalk.dim(` → ${reportPath}\n`));
212
+ return reportPath;
213
+ } catch (err) {
214
+ spinner.fail(chalk.red('Diagnostics failed.'));
215
+ console.log(chalk.dim(` ${err.message.split('\n')[0]}\n`));
216
+ return null;
217
+ }
218
+ }
219
+
220
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
221
+ // Phase 4: Dispatcher
222
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
223
+
224
+ async function phaseDispatch(summaryPath, reportPath) {
225
+ if (!WEBHOOK_URL) {
226
+ console.log(chalk.dim('\n No WEBHOOK_URL configured — skipping dispatch.'));
227
+ return;
228
+ }
229
+
230
+ printPhaseHeader(4, 'Dispatching Results', 'blue');
231
+
232
+ const spinner = ora({
233
+ text: chalk.blue(`Sending logs to endpoint...`),
234
+ color: 'blue',
235
+ prefixText: ' ',
236
+ }).start();
237
+
238
+ try {
239
+ const payload = { timestamp: new Date().toISOString() };
240
+
241
+ if (summaryPath && existsSync(summaryPath)) {
242
+ payload.runSummary = JSON.parse(readFileSync(summaryPath, 'utf-8'));
243
+ }
244
+ if (reportPath && existsSync(reportPath)) {
245
+ payload.incidentReport = readFileSync(reportPath, 'utf-8');
246
+ }
247
+
248
+ const response = await fetch(WEBHOOK_URL, {
249
+ method: 'POST',
250
+ headers: { 'Content-Type': 'application/json' },
251
+ body: JSON.stringify(payload),
252
+ });
253
+
254
+ if (response.ok) {
255
+ spinner.succeed(chalk.green(`Logs dispatched to endpoint (${response.status}).`));
256
+ } else {
257
+ spinner.warn(chalk.yellow(`Endpoint responded with ${response.status}.`));
258
+ }
259
+ } catch (err) {
260
+ spinner.fail(chalk.red(`Dispatch failed: ${err.message}`));
261
+ }
262
+ }
263
+
264
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
265
+ // Terminal UX
266
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
267
+
268
+ function printBanner() {
269
+ console.log(chalk.bold.cyan(`
270
+ ╔═══════════════════════════════════════════════════╗
271
+ ║ ║
272
+ ║ ◈ A E G I S S H A D O W ◈ ║
273
+ ║ Autonomous Lifecycle Orchestrator ║
274
+ ║ ║
275
+ ╚═══════════════════════════════════════════════════╝
276
+ `));
277
+ console.log(chalk.dim(` Target : ${START_URL || 'none'}`));
278
+ console.log(chalk.dim(` Record : ${RECORD_DURATION_S}s`));
279
+ console.log(chalk.dim(` Codebase: ${CODEBASE_DIR}`));
280
+ if (WEBHOOK_URL) {
281
+ console.log(chalk.dim(` Webhook: ${WEBHOOK_URL}`));
282
+ }
283
+ console.log();
284
+ }
285
+
286
+ function printPhaseHeader(num, title, color) {
287
+ const c = chalk[color] || chalk.white;
288
+ console.log(c.bold(` ━━━ Phase ${num}: ${title} ${'━'.repeat(Math.max(0, 38 - title.length))}`));
289
+ console.log();
290
+ }
291
+
292
+ function printFooter(goldenPath, summaryPath, reportPath) {
293
+ console.log(chalk.bold.cyan(`
294
+ ╔═══════════════════════════════════════════════════╗
295
+ ║ Run Complete ║
296
+ ╚═══════════════════════════════════════════════════╝`));
297
+
298
+ if (goldenPath) console.log(chalk.dim(` 📄 Golden : ${goldenPath}`));
299
+ if (summaryPath && existsSync(summaryPath)) console.log(chalk.dim(` 📄 Summary : ${summaryPath}`));
300
+ if (reportPath && existsSync(reportPath)) console.log(chalk.dim(` 📄 Report : ${reportPath}`));
301
+ if (WEBHOOK_URL) console.log(chalk.dim(` 🔗 Webhook : ${WEBHOOK_URL}`));
302
+ console.log();
303
+ }
304
+
305
+ function progressText(current, total) {
306
+ const width = 20;
307
+ const filled = Math.round((current / total) * width);
308
+ const empty = width - filled;
309
+ const bar = chalk.cyan('█'.repeat(filled)) + chalk.dim('░'.repeat(empty));
310
+ return `[${bar}] ${chalk.bold(current)}/${total}s — ${chalk.dim('Recording User Interaction...')}`;
311
+ }
312
+
313
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
314
+ // Helpers
315
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
316
+
317
+ /**
318
+ * Run a sibling script (monitor.js / diagnostics.js) as a child process.
319
+ * Pipes output to /dev/null; captures stderr for error reporting.
320
+ */
321
+ function runChildScript(scriptName, args = []) {
322
+ return new Promise((resolve, reject) => {
323
+ const scriptPath = join(__dirname, scriptName);
324
+ const proc = spawn(process.execPath, [scriptPath, ...args], {
325
+ cwd: DOM_DIR,
326
+ stdio: ['ignore', 'pipe', 'pipe'],
327
+ env: { ...process.env },
328
+ });
329
+
330
+ let stderr = '';
331
+ proc.stderr.on('data', (chunk) => { stderr += chunk; });
332
+
333
+ proc.on('close', (code) => {
334
+ if (code === 0) resolve();
335
+ else reject(new Error(stderr.trim() || `${scriptName} exited with code ${code}`));
336
+ });
337
+
338
+ proc.on('error', (err) => {
339
+ reject(new Error(`Failed to spawn ${scriptName}: ${err.message}`));
340
+ });
341
+ });
342
+ }
343
+
344
+ /**
345
+ * Load .env file from the dom directory (zero-dependency).
346
+ */
347
+ function loadEnvFile() {
348
+ const envPath = resolve(DOM_DIR, '.env');
349
+ if (!existsSync(envPath)) return;
350
+ try {
351
+ const content = readFileSync(envPath, 'utf-8');
352
+ for (const line of content.split('\n')) {
353
+ const trimmed = line.trim();
354
+ if (!trimmed || trimmed.startsWith('#')) continue;
355
+ const eqIdx = trimmed.indexOf('=');
356
+ if (eqIdx === -1) continue;
357
+ const key = trimmed.slice(0, eqIdx).trim();
358
+ const val = trimmed.slice(eqIdx + 1).trim();
359
+ if (!process.env[key]) {
360
+ process.env[key] = val;
361
+ }
362
+ }
363
+ } catch {
364
+ // .env is optional
365
+ }
366
+ }
367
+
368
+ /**
369
+ * Parse a --flag value from argv.
370
+ */
371
+ function parseFlag(flag) {
372
+ const args = process.argv.slice(2);
373
+ const idx = args.indexOf(flag);
374
+ if (idx !== -1 && args[idx + 1]) {
375
+ return resolve(args[idx + 1]);
376
+ }
377
+ return null;
378
+ }