@scanwarp/core 0.1.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.
@@ -0,0 +1,24 @@
1
+ import type { Event, ProviderStatus } from './types.js';
2
+ export interface CorrelationResult {
3
+ shouldCorrelate: boolean;
4
+ correlationGroup?: string;
5
+ existingIncidentId?: string;
6
+ reason?: string;
7
+ }
8
+ export declare class Correlator {
9
+ /**
10
+ * Analyzes if a new event should be correlated with existing events/incidents
11
+ */
12
+ correlate(newEvent: Event, recentEvents: Event[], openIncidents: Array<{
13
+ id: string;
14
+ events: string[];
15
+ correlation_group?: string;
16
+ }>, providerStatuses: ProviderStatus[]): Promise<CorrelationResult>;
17
+ private checkProviderOutage;
18
+ private findSameEndpoint;
19
+ private extractEndpoint;
20
+ private isCheckoutEndpoint;
21
+ private correlatePaymentAndCheckout;
22
+ private checkMultipleMonitorFailures;
23
+ private findIncidentWithEvent;
24
+ }
@@ -0,0 +1,173 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Correlator = void 0;
4
+ class Correlator {
5
+ /**
6
+ * Analyzes if a new event should be correlated with existing events/incidents
7
+ */
8
+ async correlate(newEvent, recentEvents, openIncidents, providerStatuses) {
9
+ // Rule 1: Check if this is part of a provider outage
10
+ const providerOutage = this.checkProviderOutage(newEvent, providerStatuses);
11
+ if (providerOutage) {
12
+ return {
13
+ shouldCorrelate: true,
14
+ correlationGroup: `provider-${providerOutage.provider}`,
15
+ reason: `${providerOutage.provider} is experiencing ${providerOutage.status}`,
16
+ };
17
+ }
18
+ // Rule 2: Same URL/endpoint within 5 minutes
19
+ const sameEndpoint = this.findSameEndpoint(newEvent, recentEvents);
20
+ if (sameEndpoint) {
21
+ const incident = this.findIncidentWithEvent(sameEndpoint.id, openIncidents);
22
+ if (incident) {
23
+ return {
24
+ shouldCorrelate: true,
25
+ correlationGroup: incident.correlation_group || `endpoint-${this.extractEndpoint(newEvent)}`,
26
+ existingIncidentId: incident.id,
27
+ reason: 'Same endpoint affected within 5 minutes',
28
+ };
29
+ }
30
+ }
31
+ // Rule 3: Stripe payment failure + server 500 on checkout endpoint within 2 minutes
32
+ if (newEvent.source === 'stripe' || this.isCheckoutEndpoint(newEvent)) {
33
+ const correlated = this.correlatePaymentAndCheckout(newEvent, recentEvents);
34
+ if (correlated) {
35
+ const incident = this.findIncidentWithEvent(correlated.id, openIncidents);
36
+ if (incident) {
37
+ return {
38
+ shouldCorrelate: true,
39
+ correlationGroup: incident.correlation_group || 'payment-checkout-failure',
40
+ existingIncidentId: incident.id,
41
+ reason: 'Payment failure correlated with checkout endpoint error',
42
+ };
43
+ }
44
+ return {
45
+ shouldCorrelate: true,
46
+ correlationGroup: 'payment-checkout-failure',
47
+ reason: 'Payment failure correlated with checkout endpoint error',
48
+ };
49
+ }
50
+ }
51
+ // Rule 4: Multiple monitors failing at once (3+ within 2 minutes)
52
+ const multipleFailures = this.checkMultipleMonitorFailures(newEvent, recentEvents);
53
+ if (multipleFailures.length >= 2) {
54
+ // Check if there's already an incident for this burst
55
+ const burstIncident = openIncidents.find((inc) => inc.correlation_group?.startsWith('multi-failure-'));
56
+ if (burstIncident) {
57
+ return {
58
+ shouldCorrelate: true,
59
+ correlationGroup: burstIncident.correlation_group,
60
+ existingIncidentId: burstIncident.id,
61
+ reason: 'Part of multi-monitor failure burst',
62
+ };
63
+ }
64
+ return {
65
+ shouldCorrelate: true,
66
+ correlationGroup: `multi-failure-${Date.now()}`,
67
+ reason: `${multipleFailures.length + 1} monitors failing simultaneously`,
68
+ };
69
+ }
70
+ // No correlation found
71
+ return { shouldCorrelate: false };
72
+ }
73
+ checkProviderOutage(event, providerStatuses) {
74
+ // Map event sources to provider names
75
+ const providerMap = {
76
+ vercel: 'vercel',
77
+ supabase: 'supabase',
78
+ stripe: 'stripe',
79
+ github: 'github',
80
+ };
81
+ const provider = providerMap[event.source];
82
+ if (!provider)
83
+ return null;
84
+ const status = providerStatuses.find((p) => p.provider === provider);
85
+ if (status && (status.status === 'degraded' || status.status === 'outage')) {
86
+ return status;
87
+ }
88
+ return null;
89
+ }
90
+ findSameEndpoint(newEvent, recentEvents) {
91
+ const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000);
92
+ const newEndpoint = this.extractEndpoint(newEvent);
93
+ if (!newEndpoint)
94
+ return null;
95
+ return (recentEvents.find((event) => {
96
+ if (event.created_at < fiveMinutesAgo)
97
+ return false;
98
+ if (event.id === newEvent.id)
99
+ return false;
100
+ const endpoint = this.extractEndpoint(event);
101
+ return endpoint === newEndpoint;
102
+ }) || null);
103
+ }
104
+ extractEndpoint(event) {
105
+ // Try to extract URL from monitor or raw_data
106
+ if (event.monitor_id && event.raw_data?.url) {
107
+ return String(event.raw_data.url);
108
+ }
109
+ // Try to extract from message (basic pattern matching)
110
+ const urlMatch = event.message.match(/https?:\/\/[^\s]+/);
111
+ if (urlMatch) {
112
+ return urlMatch[0];
113
+ }
114
+ // Extract path from message like "POST /api/checkout failed"
115
+ const pathMatch = event.message.match(/\/(api|checkout|webhook|auth)\/[^\s]*/);
116
+ if (pathMatch) {
117
+ return pathMatch[0];
118
+ }
119
+ return null;
120
+ }
121
+ isCheckoutEndpoint(event) {
122
+ const endpoint = this.extractEndpoint(event);
123
+ if (!endpoint)
124
+ return false;
125
+ return (endpoint.includes('/checkout') ||
126
+ endpoint.includes('/payment') ||
127
+ endpoint.includes('/stripe'));
128
+ }
129
+ correlatePaymentAndCheckout(newEvent, recentEvents) {
130
+ const twoMinutesAgo = new Date(Date.now() - 2 * 60 * 1000);
131
+ // If this is a stripe event, look for recent checkout errors
132
+ if (newEvent.source === 'stripe') {
133
+ return (recentEvents.find((event) => {
134
+ if (event.created_at < twoMinutesAgo)
135
+ return false;
136
+ if (event.source === 'stripe')
137
+ return false;
138
+ return this.isCheckoutEndpoint(event) && event.type === 'error';
139
+ }) || null);
140
+ }
141
+ // If this is a checkout error, look for recent stripe failures
142
+ if (this.isCheckoutEndpoint(newEvent) && newEvent.type === 'error') {
143
+ return (recentEvents.find((event) => {
144
+ if (event.created_at < twoMinutesAgo)
145
+ return false;
146
+ return event.source === 'stripe' && event.type === 'error';
147
+ }) || null);
148
+ }
149
+ return null;
150
+ }
151
+ checkMultipleMonitorFailures(newEvent, recentEvents) {
152
+ if (newEvent.type !== 'down' && newEvent.type !== 'error')
153
+ return [];
154
+ const twoMinutesAgo = new Date(Date.now() - 2 * 60 * 1000);
155
+ return recentEvents.filter((event) => {
156
+ if (event.created_at < twoMinutesAgo)
157
+ return false;
158
+ if (event.id === newEvent.id)
159
+ return false;
160
+ if (event.type !== 'down' && event.type !== 'error')
161
+ return false;
162
+ // Must be from different monitors
163
+ if (event.monitor_id && newEvent.monitor_id && event.monitor_id === newEvent.monitor_id) {
164
+ return false;
165
+ }
166
+ return true;
167
+ });
168
+ }
169
+ findIncidentWithEvent(eventId, incidents) {
170
+ return incidents.find((inc) => inc.events.includes(eventId)) || null;
171
+ }
172
+ }
173
+ exports.Correlator = Correlator;
@@ -0,0 +1,26 @@
1
+ import type { Event, Monitor, DiagnosisResult } from './types.js';
2
+ interface DiagnoserConfig {
3
+ apiKey: string;
4
+ model?: string;
5
+ }
6
+ interface DiagnosisContext {
7
+ events: Event[];
8
+ monitor?: Monitor;
9
+ recentHistory?: Array<{
10
+ timestamp: Date;
11
+ status: string;
12
+ message: string;
13
+ }>;
14
+ }
15
+ export declare class Diagnoser {
16
+ private client;
17
+ private model;
18
+ constructor(config: DiagnoserConfig);
19
+ diagnose(context: DiagnosisContext): Promise<DiagnosisResult>;
20
+ private getSystemPrompt;
21
+ private buildPrompt;
22
+ private sanitizeRawData;
23
+ private parseResponse;
24
+ private normalizeSeverity;
25
+ }
26
+ export {};
@@ -0,0 +1,159 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.Diagnoser = void 0;
7
+ const sdk_1 = __importDefault(require("@anthropic-ai/sdk"));
8
+ class Diagnoser {
9
+ client;
10
+ model;
11
+ constructor(config) {
12
+ this.client = new sdk_1.default({
13
+ apiKey: config.apiKey,
14
+ });
15
+ this.model = config.model || 'claude-sonnet-4-20250514';
16
+ }
17
+ async diagnose(context) {
18
+ const prompt = this.buildPrompt(context);
19
+ const response = await this.client.messages.create({
20
+ model: this.model,
21
+ max_tokens: 2000,
22
+ temperature: 0.3,
23
+ system: this.getSystemPrompt(),
24
+ messages: [
25
+ {
26
+ role: 'user',
27
+ content: prompt,
28
+ },
29
+ ],
30
+ });
31
+ const content = response.content[0];
32
+ if (content.type !== 'text') {
33
+ throw new Error('Unexpected response type from Claude');
34
+ }
35
+ return this.parseResponse(content.text);
36
+ }
37
+ getSystemPrompt() {
38
+ return `You are a senior engineering mentor helping developers who built their application using AI coding tools like Cursor or Claude Code. These developers may not have deep infrastructure knowledge or be familiar with reading stack traces.
39
+
40
+ Your job is to:
41
+ 1. Explain what went wrong in plain, conversational English (no jargon)
42
+ 2. Explain WHY it happened in a way a non-expert can understand
43
+ 3. Provide a clear, actionable fix in plain language
44
+ 4. Write a ready-to-paste prompt they can give to their AI coding assistant to fix the issue
45
+
46
+ Think of yourself as a patient mentor who's explaining a production issue to someone smart but new to production systems.
47
+
48
+ IMPORTANT RULES:
49
+ - NO technical jargon without explanation
50
+ - NO raw stack traces in your response
51
+ - Use analogies when helpful
52
+ - Be encouraging, not condescending
53
+ - Focus on "what to do" not "what you did wrong"
54
+
55
+ Respond in this exact JSON format:
56
+ {
57
+ "root_cause": "1-2 sentence plain English explanation of what broke",
58
+ "severity": "critical|warning|info",
59
+ "suggested_fix": "Plain English explanation of how to fix it (2-4 sentences)",
60
+ "fix_prompt": "A complete, copy-pasteable prompt for Cursor/Claude Code that will fix this issue"
61
+ }
62
+
63
+ The fix_prompt should be detailed and include:
64
+ - What file(s) to modify
65
+ - What specific changes to make
66
+ - Any environment variables or config needed
67
+ - How to test the fix
68
+
69
+ Make the fix_prompt actionable enough that an AI coding assistant can implement it without asking follow-up questions.`;
70
+ }
71
+ buildPrompt(context) {
72
+ const { events, monitor, recentHistory } = context;
73
+ let prompt = '## Production Issue Detected\n\n';
74
+ // Add monitor context if available
75
+ if (monitor) {
76
+ prompt += `**Service:** ${monitor.url}\n`;
77
+ prompt += `**Current Status:** ${monitor.status}\n\n`;
78
+ }
79
+ // Add event information
80
+ prompt += `**Recent Events:**\n`;
81
+ for (const event of events) {
82
+ prompt += `- [${event.type.toUpperCase()}] ${event.message}\n`;
83
+ prompt += ` Severity: ${event.severity} | Time: ${event.created_at.toISOString()}\n`;
84
+ if (event.raw_data) {
85
+ const sanitizedData = this.sanitizeRawData(event.raw_data);
86
+ if (Object.keys(sanitizedData).length > 0) {
87
+ prompt += ` Details: ${JSON.stringify(sanitizedData, null, 2)}\n`;
88
+ }
89
+ }
90
+ prompt += '\n';
91
+ }
92
+ // Add recent history if available
93
+ if (recentHistory && recentHistory.length > 0) {
94
+ prompt += `\n**Recent History (last 24 hours):**\n`;
95
+ for (const item of recentHistory.slice(0, 10)) {
96
+ prompt += `- ${item.timestamp.toISOString()}: ${item.status} - ${item.message}\n`;
97
+ }
98
+ prompt += '\n';
99
+ }
100
+ prompt += '\nPlease diagnose this issue and provide a fix.';
101
+ return prompt;
102
+ }
103
+ sanitizeRawData(data) {
104
+ const sanitized = {};
105
+ // Include relevant fields, exclude sensitive or verbose ones
106
+ const relevantFields = [
107
+ 'statusCode',
108
+ 'responseTime',
109
+ 'error',
110
+ 'url',
111
+ 'method',
112
+ 'level',
113
+ 'message',
114
+ 'type',
115
+ 'source',
116
+ ];
117
+ for (const field of relevantFields) {
118
+ if (field in data) {
119
+ sanitized[field] = data[field];
120
+ }
121
+ }
122
+ return sanitized;
123
+ }
124
+ parseResponse(text) {
125
+ try {
126
+ // Try to extract JSON from the response
127
+ const jsonMatch = text.match(/\{[\s\S]*\}/);
128
+ if (!jsonMatch) {
129
+ throw new Error('No JSON found in response');
130
+ }
131
+ const parsed = JSON.parse(jsonMatch[0]);
132
+ return {
133
+ root_cause: parsed.root_cause || 'Unable to determine root cause',
134
+ severity: this.normalizeSeverity(parsed.severity),
135
+ suggested_fix: parsed.suggested_fix || 'No fix suggested',
136
+ fix_prompt: parsed.fix_prompt || 'No fix prompt provided',
137
+ };
138
+ }
139
+ catch (error) {
140
+ // Fallback if parsing fails
141
+ console.error('Failed to parse diagnosis response:', error);
142
+ return {
143
+ root_cause: 'Failed to parse diagnosis from AI response',
144
+ severity: 'warning',
145
+ suggested_fix: text.substring(0, 500),
146
+ fix_prompt: 'Unable to generate fix prompt. Please review the raw diagnosis and consult your AI coding assistant.',
147
+ };
148
+ }
149
+ }
150
+ normalizeSeverity(severity) {
151
+ const normalized = severity.toLowerCase();
152
+ if (normalized === 'critical')
153
+ return 'critical';
154
+ if (normalized === 'warning')
155
+ return 'warning';
156
+ return 'info';
157
+ }
158
+ }
159
+ exports.Diagnoser = Diagnoser;
@@ -0,0 +1,3 @@
1
+ export * from './types.js';
2
+ export * from './diagnoser.js';
3
+ export * from './correlator.js';
package/dist/index.js ADDED
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./types.js"), exports);
18
+ __exportStar(require("./diagnoser.js"), exports);
19
+ __exportStar(require("./correlator.js"), exports);
@@ -0,0 +1,115 @@
1
+ export interface Monitor {
2
+ id: string;
3
+ project_id: string;
4
+ url: string;
5
+ check_interval_seconds: number;
6
+ last_checked_at?: Date;
7
+ status: 'up' | 'down' | 'unknown';
8
+ created_at: Date;
9
+ }
10
+ export type EventSource = 'monitor' | 'vercel' | 'stripe' | 'supabase' | 'github' | 'provider-status';
11
+ export interface Event {
12
+ id: string;
13
+ project_id: string;
14
+ monitor_id?: string;
15
+ type: 'error' | 'slow' | 'down' | 'up';
16
+ source: EventSource;
17
+ message: string;
18
+ raw_data?: Record<string, unknown>;
19
+ severity: 'low' | 'medium' | 'high' | 'critical';
20
+ created_at: Date;
21
+ }
22
+ export interface EventStats {
23
+ monitor_id: string;
24
+ avg_response_time?: number;
25
+ total_checks: number;
26
+ error_count: number;
27
+ last_error_at?: Date;
28
+ updated_at: Date;
29
+ }
30
+ export interface VercelLogDrainPayload {
31
+ source: string;
32
+ deploymentId: string;
33
+ message: string;
34
+ timestamp: number;
35
+ type: 'stdout' | 'stderr' | 'request' | 'response';
36
+ level?: 'info' | 'warn' | 'error' | 'debug';
37
+ [key: string]: unknown;
38
+ }
39
+ export interface WebhookPayload {
40
+ event: string;
41
+ service: string;
42
+ data: Record<string, unknown>;
43
+ timestamp: string;
44
+ }
45
+ export interface Incident {
46
+ id: string;
47
+ project_id: string;
48
+ events: string[];
49
+ correlation_group?: string;
50
+ status: 'open' | 'investigating' | 'resolved';
51
+ diagnosis_text?: string;
52
+ diagnosis_fix?: string;
53
+ severity: 'critical' | 'warning' | 'info';
54
+ fix_prompt?: string;
55
+ created_at: Date;
56
+ resolved_at?: Date;
57
+ }
58
+ export interface DiagnosisResult {
59
+ root_cause: string;
60
+ severity: 'critical' | 'warning' | 'info';
61
+ suggested_fix: string;
62
+ fix_prompt: string;
63
+ }
64
+ export interface ProviderStatus {
65
+ provider: string;
66
+ status: 'operational' | 'degraded' | 'outage';
67
+ last_checked_at: Date;
68
+ details?: string;
69
+ }
70
+ export interface StripeWebhookEvent {
71
+ id: string;
72
+ type: string;
73
+ data: {
74
+ object: Record<string, unknown>;
75
+ };
76
+ [key: string]: unknown;
77
+ }
78
+ export interface GitHubWebhookEvent {
79
+ action?: string;
80
+ workflow_run?: {
81
+ conclusion: string;
82
+ name: string;
83
+ html_url: string;
84
+ };
85
+ alert?: {
86
+ number: number;
87
+ state: string;
88
+ html_url: string;
89
+ };
90
+ [key: string]: unknown;
91
+ }
92
+ export interface ProviderEvent {
93
+ source: EventSource;
94
+ type: Event['type'];
95
+ message: string;
96
+ severity: Event['severity'];
97
+ raw_data: Record<string, unknown>;
98
+ }
99
+ export interface MonitoringConfig {
100
+ id: string;
101
+ serviceName: string;
102
+ endpoint: string;
103
+ interval: number;
104
+ createdAt: Date;
105
+ updatedAt: Date;
106
+ }
107
+ export interface MonitoringEvent {
108
+ id: string;
109
+ configId: string;
110
+ status: 'success' | 'failure';
111
+ responseTime?: number;
112
+ statusCode?: number;
113
+ errorMessage?: string;
114
+ timestamp: Date;
115
+ }
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@scanwarp/core",
3
+ "version": "0.1.0",
4
+ "description": "Shared types and logic for ScanWarp monitoring",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "files": [
8
+ "dist"
9
+ ],
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "dev": "tsc --watch",
13
+ "typecheck": "tsc --noEmit",
14
+ "clean": "rm -rf dist",
15
+ "prepublishOnly": "npm run build"
16
+ },
17
+ "keywords": [
18
+ "monitoring",
19
+ "observability",
20
+ "ai",
21
+ "claude",
22
+ "diagnosis",
23
+ "production",
24
+ "incidents"
25
+ ],
26
+ "author": "ScanWarp",
27
+ "license": "MIT",
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "https://github.com/scanwarp/scanwarp.git",
31
+ "directory": "packages/core"
32
+ },
33
+ "bugs": {
34
+ "url": "https://github.com/scanwarp/scanwarp/issues"
35
+ },
36
+ "homepage": "https://scanwarp.com",
37
+ "devDependencies": {
38
+ "@types/node": "^20.11.0",
39
+ "typescript": "^5.3.3"
40
+ },
41
+ "dependencies": {
42
+ "@anthropic-ai/sdk": "^0.74.0"
43
+ }
44
+ }