frogo 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.
Files changed (41) hide show
  1. package/README.md +79 -0
  2. package/dist/agent/launch.js +384 -0
  3. package/dist/cli/commands/configure.js +243 -0
  4. package/dist/cli/commands/debug.js +9 -0
  5. package/dist/cli/commands/investigate.js +50 -0
  6. package/dist/cli/commands/mcp.js +53 -0
  7. package/dist/cli/commands/scan.js +5 -0
  8. package/dist/cli/index.js +22 -0
  9. package/dist/config/load.js +36 -0
  10. package/dist/config/save.js +19 -0
  11. package/dist/connectors/datadog.js +113 -0
  12. package/dist/connectors/demo-events.js +82 -0
  13. package/dist/connectors/local.js +25 -0
  14. package/dist/connectors/trigger.js +13 -0
  15. package/dist/connectors/vercel.js +13 -0
  16. package/dist/core/correlator.js +17 -0
  17. package/dist/core/investigator.js +49 -0
  18. package/dist/core/pattern-engine.js +108 -0
  19. package/dist/core/timeline.js +24 -0
  20. package/dist/core/types.js +1 -0
  21. package/dist/llm/explain.js +14 -0
  22. package/package.json +37 -0
  23. package/src/agent/launch.ts +449 -0
  24. package/src/cli/commands/configure.ts +265 -0
  25. package/src/cli/commands/debug.ts +10 -0
  26. package/src/cli/commands/mcp.ts +66 -0
  27. package/src/cli/commands/scan.ts +6 -0
  28. package/src/cli/index.ts +27 -0
  29. package/src/config/load.ts +42 -0
  30. package/src/config/save.ts +27 -0
  31. package/src/connectors/datadog.ts +152 -0
  32. package/src/connectors/local.ts +27 -0
  33. package/src/connectors/trigger.ts +16 -0
  34. package/src/connectors/vercel.ts +16 -0
  35. package/src/core/correlator.ts +27 -0
  36. package/src/core/investigator.ts +64 -0
  37. package/src/core/pattern-engine.ts +139 -0
  38. package/src/core/timeline.ts +32 -0
  39. package/src/core/types.ts +92 -0
  40. package/src/llm/explain.ts +20 -0
  41. package/tsconfig.json +15 -0
@@ -0,0 +1,9 @@
1
+ import { Command } from "commander";
2
+ import { runInvestigation } from "../../core/investigator.js";
3
+ export const debugCommand = new Command("debug")
4
+ .description("run deterministic investigation against a focused query")
5
+ .argument("query", "natural language prompt")
6
+ .action(async (query) => {
7
+ console.log(`🐸 Debugging query: ${query}`);
8
+ await runInvestigation({ windowMinutes: 15, query });
9
+ });
@@ -0,0 +1,50 @@
1
+ import { loadConfig } from "../../config/load.js";
2
+ import { VercelConnector } from "../../connectors/vercel.js";
3
+ import { TriggerConnector } from "../../connectors/trigger.js";
4
+ import { LocalConnector } from "../../connectors/local.js";
5
+ import { DatadogConnector } from "../../connectors/datadog.js";
6
+ import { correlateEvents } from "../../core/correlator.js";
7
+ import { explainIncident } from "../../llm/explain.js";
8
+ const DEFAULT_WINDOW_MINUTES = 15;
9
+ function formatEvent(event) {
10
+ return `${event.timestamp.toISOString()} ${event.source} ${event.type}`;
11
+ }
12
+ async function runInvestigation(options = {}) {
13
+ const config = await loadConfig();
14
+ const windowMinutes = options.windowMinutes ?? DEFAULT_WINDOW_MINUTES;
15
+ const windowStart = new Date(Date.now() - windowMinutes * 60 * 1000);
16
+ const connectors = [
17
+ new VercelConnector(config),
18
+ new TriggerConnector(config),
19
+ new LocalConnector(config),
20
+ new DatadogConnector(config)
21
+ ];
22
+ console.log(`🐸 Scanning last ${windowMinutes} minutes...`);
23
+ const events = await Promise.all(connectors.map((connector) => connector.fetchEvents(windowStart)));
24
+ const flattened = events.flat();
25
+ const report = correlateEvents(flattened);
26
+ if (!report) {
27
+ console.log("🐸 No significant failure patterns detected.");
28
+ return;
29
+ }
30
+ const explanation = await explainIncident({
31
+ pattern: report.match.patternId,
32
+ timeline: report.timeline,
33
+ evidence: report.match.evidence,
34
+ confidence: report.match.confidence,
35
+ rootCauseEvent: report.match.rootCauseEvent,
36
+ query: options.query
37
+ });
38
+ console.log("Incident detected.");
39
+ console.log("Trigger:");
40
+ console.log(`- ${report.match.rootCauseEvent.type}`);
41
+ console.log("Cascade:");
42
+ report.match.evidence
43
+ .slice(1)
44
+ .forEach((event) => console.log(`→ ${formatEvent(event)}`));
45
+ console.log("Root Cause:");
46
+ console.log(`- ${explanation.explanation}`);
47
+ console.log(`Confidence: ${report.match.confidence.toFixed(2)}`);
48
+ console.log(`Suggested Fix: ${explanation.suggestion}`);
49
+ }
50
+ export { runInvestigation };
@@ -0,0 +1,53 @@
1
+ import { Command } from "commander";
2
+ import prompts from "prompts";
3
+ import { loadConfig } from "../../config/load.js";
4
+ import { saveConfig } from "../../config/save.js";
5
+ const LANGSMITH_SERVER = "langsmith";
6
+ const langsmithQuestions = [
7
+ {
8
+ type: "text",
9
+ name: "apiKey",
10
+ message: "LangSmith API key (ls_api_key_... or lsv2_pt_...)",
11
+ validate: (value) => (value.trim() ? true : "API key is required")
12
+ },
13
+ {
14
+ type: "text",
15
+ name: "workspaceKey",
16
+ message: "LangSmith workspace ID (optional)",
17
+ initial: ""
18
+ },
19
+ {
20
+ type: "text",
21
+ name: "mcpUrl",
22
+ message: "LangSmith MCP URL",
23
+ initial: "https://langsmith-mcp-server.onrender.com/mcp"
24
+ }
25
+ ];
26
+ export const mcpCommand = new Command("mcp").description("manage MCP server integrations");
27
+ mcpCommand
28
+ .command("login")
29
+ .description("store credentials for an MCP server, e.g. langsmith")
30
+ .argument("server", "name of the MCP server")
31
+ .action(async (server) => {
32
+ if (server.toLowerCase() !== LANGSMITH_SERVER) {
33
+ console.log(`Unsupported MCP server: ${server}. Try 'frogo mcp login langsmith'.`);
34
+ return;
35
+ }
36
+ const answers = (await prompts(langsmithQuestions));
37
+ if (!answers.apiKey) {
38
+ console.log("LangSmith login canceled.");
39
+ return;
40
+ }
41
+ const config = await loadConfig();
42
+ const updated = {
43
+ ...config,
44
+ langsmith: {
45
+ apiKey: answers.apiKey.trim(),
46
+ workspaceKey: answers.workspaceKey?.trim() || undefined,
47
+ mcpUrl: answers.mcpUrl?.trim() || undefined
48
+ }
49
+ };
50
+ await saveConfig(updated);
51
+ console.log("🐸 LangSmith MCP credentials saved.");
52
+ console.log("If you plan to use ai-sdk's mcp login CLI, run `npx ai-sdk mcp login langsmith` now.");
53
+ });
@@ -0,0 +1,5 @@
1
+ import { Command } from "commander";
2
+ import { runInvestigation } from "../../core/investigator.js";
3
+ export const scanCommand = new Command("scan")
4
+ .description("run deterministic incident scan")
5
+ .action(() => runInvestigation());
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from "commander";
3
+ import { configureCommand } from "./commands/configure.js";
4
+ import { debugCommand } from "./commands/debug.js";
5
+ import { scanCommand } from "./commands/scan.js";
6
+ import { runAgentChat } from "../agent/launch.js";
7
+ import { mcpCommand } from "./commands/mcp.js";
8
+ const program = new Command();
9
+ program.name("frogo");
10
+ program.description("Frogo v0 — incident investigator CLI");
11
+ program.version("0.1.0");
12
+ program.addCommand(configureCommand);
13
+ program.addCommand(scanCommand);
14
+ program.addCommand(debugCommand);
15
+ program.addCommand(mcpCommand);
16
+ program.action(() => {
17
+ runAgentChat().catch((error) => {
18
+ console.error("Agent process failed:", error);
19
+ process.exit(1);
20
+ });
21
+ });
22
+ program.parse();
@@ -0,0 +1,36 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import os from "node:os";
4
+ const PROJECT_CONFIG = ".frogo.json";
5
+ const LEGACY_PROJECT_CONFIG = ".frog.json";
6
+ const GLOBAL_DIR = path.join(os.homedir(), ".frogo");
7
+ const LEGACY_GLOBAL_DIR = path.join(os.homedir(), ".frog");
8
+ const GLOBAL_CONFIG = path.join(GLOBAL_DIR, "config.json");
9
+ const LEGACY_GLOBAL_CONFIG = path.join(LEGACY_GLOBAL_DIR, "config.json");
10
+ async function readJson(filePath) {
11
+ try {
12
+ const raw = await fs.readFile(filePath, "utf-8");
13
+ const parsed = JSON.parse(raw);
14
+ return parsed;
15
+ }
16
+ catch (error) {
17
+ return null;
18
+ }
19
+ }
20
+ export async function loadConfig() {
21
+ const localPaths = [path.join(process.cwd(), PROJECT_CONFIG), path.join(process.cwd(), LEGACY_PROJECT_CONFIG)];
22
+ for (const localPath of localPaths) {
23
+ const local = await readJson(localPath);
24
+ if (local) {
25
+ return local;
26
+ }
27
+ }
28
+ const globalPaths = [GLOBAL_CONFIG, LEGACY_GLOBAL_CONFIG];
29
+ for (const globalPath of globalPaths) {
30
+ const global = await readJson(globalPath);
31
+ if (global) {
32
+ return global;
33
+ }
34
+ }
35
+ return {};
36
+ }
@@ -0,0 +1,19 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import os from "node:os";
4
+ const PROJECT_CONFIG = ".frogo.json";
5
+ const GLOBAL_DIR = path.join(os.homedir(), ".frogo");
6
+ const GLOBAL_CONFIG = path.join(GLOBAL_DIR, "config.json");
7
+ export async function saveConfig(config, options) {
8
+ const local = options?.local ?? true;
9
+ const global = options?.global ?? true;
10
+ const serialized = JSON.stringify(config, null, 2);
11
+ if (local) {
12
+ const localPath = path.join(process.cwd(), PROJECT_CONFIG);
13
+ await fs.writeFile(localPath, serialized, "utf-8");
14
+ }
15
+ if (global) {
16
+ await fs.mkdir(GLOBAL_DIR, { recursive: true });
17
+ await fs.writeFile(GLOBAL_CONFIG, serialized, "utf-8");
18
+ }
19
+ }
@@ -0,0 +1,113 @@
1
+ import { Client } from "@modelcontextprotocol/sdk/client";
2
+ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
3
+ const DEFAULT_COMMAND = "datadog-mcp-server";
4
+ const DEFAULT_QUERY = "status:error OR status:warn";
5
+ const DEFAULT_LIMIT = 80;
6
+ export class DatadogConnector {
7
+ constructor(config) {
8
+ this.config = config;
9
+ this.name = "datadog";
10
+ }
11
+ async fetchEvents(since) {
12
+ const datadog = this.config.datadog;
13
+ if (!datadog?.apiKey || !datadog?.appKey) {
14
+ return [];
15
+ }
16
+ const command = datadog.command ?? DEFAULT_COMMAND;
17
+ const args = datadog.args ?? [];
18
+ const env = { ...process.env };
19
+ env.DD_API_KEY = datadog.apiKey;
20
+ env.DD_APP_KEY = datadog.appKey;
21
+ env.DD_SITE = datadog.site ?? env.DD_SITE ?? "datadoghq.com";
22
+ if (datadog.logsSite) {
23
+ env.DD_LOGS_SITE = datadog.logsSite;
24
+ }
25
+ if (datadog.metricsSite) {
26
+ env.DD_METRICS_SITE = datadog.metricsSite;
27
+ }
28
+ const transport = new StdioClientTransport({
29
+ command,
30
+ args,
31
+ env,
32
+ stderr: "inherit"
33
+ });
34
+ const client = new Client({ name: "frog-datadog-connector", version: "0.1.0" });
35
+ try {
36
+ await client.connect(transport);
37
+ const response = await client.callTool({
38
+ name: "search-logs",
39
+ arguments: {
40
+ filter: {
41
+ query: datadog.query ?? DEFAULT_QUERY,
42
+ from: since.toISOString(),
43
+ to: new Date().toISOString(),
44
+ indexes: datadog.indexes
45
+ },
46
+ limit: datadog.limit ?? DEFAULT_LIMIT
47
+ }
48
+ });
49
+ const toolResponse = response;
50
+ const raw = toolResponse.content?.find((item) => item.type === "text")?.text;
51
+ if (!raw) {
52
+ return [];
53
+ }
54
+ const parsed = this.safeParse(raw);
55
+ if (!parsed) {
56
+ return [];
57
+ }
58
+ return (parsed.data ?? [])
59
+ .map((entry) => this.normalizeEntry(entry))
60
+ .filter((event) => event.timestamp.getTime() >= since.getTime());
61
+ }
62
+ catch (error) {
63
+ console.error("Datadog MCP fetch failed:", error);
64
+ return [];
65
+ }
66
+ finally {
67
+ await transport.close();
68
+ }
69
+ }
70
+ safeParse(raw) {
71
+ try {
72
+ return JSON.parse(raw);
73
+ }
74
+ catch (error) {
75
+ console.error("Failed to parse Datadog search response", error);
76
+ return null;
77
+ }
78
+ }
79
+ normalizeEntry(entry) {
80
+ const attributes = entry.attributes ?? {};
81
+ const timestamp = attributes.timestamp ? new Date(attributes.timestamp) : new Date();
82
+ return {
83
+ source: "datadog",
84
+ type: attributes.service ? `${attributes.service}.log` : "datadog.log",
85
+ severity: mapSeverity(attributes.status),
86
+ timestamp,
87
+ metadata: {
88
+ id: entry.id,
89
+ message: attributes.message,
90
+ status: attributes.status,
91
+ service: attributes.service,
92
+ host: attributes.host,
93
+ logger: attributes.logger,
94
+ traceId: entry.relationships?.trace?.data?.id,
95
+ tags: attributes.tags,
96
+ raw: entry
97
+ }
98
+ };
99
+ }
100
+ }
101
+ function mapSeverity(status) {
102
+ if (!status) {
103
+ return "info";
104
+ }
105
+ const normalized = status.toLowerCase();
106
+ if (normalized === "error" || normalized === "critical" || normalized === "fatal") {
107
+ return "error";
108
+ }
109
+ if (normalized === "warn" || normalized === "warning") {
110
+ return "warn";
111
+ }
112
+ return "info";
113
+ }
@@ -0,0 +1,82 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getDemoEvents = getDemoEvents;
4
+ const now = Date.now();
5
+ const minutesAgo = (minutes) => new Date(now - minutes * 60 * 1000);
6
+ const DEMO_EVENTS = {
7
+ vercel: [
8
+ {
9
+ source: "vercel",
10
+ type: "function.timeout",
11
+ severity: "error",
12
+ timestamp: minutesAgo(12),
13
+ metadata: {
14
+ project: "runclaw",
15
+ functionName: "provision-worker",
16
+ timeoutMs: 10000,
17
+ durationMs: 12000
18
+ }
19
+ }
20
+ ],
21
+ trigger: [
22
+ {
23
+ source: "trigger",
24
+ type: "job.retry",
25
+ severity: "warn",
26
+ timestamp: minutesAgo(11),
27
+ metadata: {
28
+ jobName: "provision-worker",
29
+ attempt: 1,
30
+ reason: "function timeout"
31
+ }
32
+ },
33
+ {
34
+ source: "trigger",
35
+ type: "job.retry",
36
+ severity: "warn",
37
+ timestamp: minutesAgo(10),
38
+ metadata: {
39
+ jobName: "provision-worker",
40
+ attempt: 2,
41
+ reason: "function timeout"
42
+ }
43
+ },
44
+ {
45
+ source: "trigger",
46
+ type: "job.retry",
47
+ severity: "warn",
48
+ timestamp: minutesAgo(9),
49
+ metadata: {
50
+ jobName: "provision-worker",
51
+ attempt: 3,
52
+ reason: "function timeout"
53
+ }
54
+ },
55
+ {
56
+ source: "trigger",
57
+ type: "worker.restart",
58
+ severity: "error",
59
+ timestamp: minutesAgo(8),
60
+ metadata: {
61
+ worker: "provision-worker",
62
+ reason: "retry storm"
63
+ }
64
+ }
65
+ ],
66
+ datadog: [],
67
+ local: [
68
+ {
69
+ source: "local",
70
+ type: "external.api.rate_limit",
71
+ severity: "warn",
72
+ timestamp: minutesAgo(7),
73
+ metadata: {
74
+ target: "https://api.example.com/allocations",
75
+ status: 429
76
+ }
77
+ }
78
+ ]
79
+ };
80
+ function getDemoEvents(source, since) {
81
+ return DEMO_EVENTS[source].filter((event) => event.timestamp.getTime() >= since.getTime());
82
+ }
@@ -0,0 +1,25 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ export class LocalConnector {
4
+ constructor(config) {
5
+ this.config = config;
6
+ this.name = "local";
7
+ }
8
+ async fetchEvents(since) {
9
+ const manualPath = process.env.FROGO_LOCAL_EVENTS;
10
+ if (!manualPath) {
11
+ return [];
12
+ }
13
+ try {
14
+ const resolved = path.resolve(process.cwd(), manualPath);
15
+ const raw = await fs.readFile(resolved, "utf-8");
16
+ const parsed = JSON.parse(raw);
17
+ return parsed
18
+ .map((event) => ({ ...event, timestamp: new Date(event.timestamp) }))
19
+ .filter((event) => event.timestamp.getTime() >= since.getTime());
20
+ }
21
+ catch (error) {
22
+ return [];
23
+ }
24
+ }
25
+ }
@@ -0,0 +1,13 @@
1
+ export class TriggerConnector {
2
+ constructor(config) {
3
+ this.config = config;
4
+ this.name = "trigger";
5
+ }
6
+ async fetchEvents(since) {
7
+ if (!this.config.triggerToken) {
8
+ return [];
9
+ }
10
+ // TODO: implement Trigger.dev API fetch once credentials are available.
11
+ return [];
12
+ }
13
+ }
@@ -0,0 +1,13 @@
1
+ export class VercelConnector {
2
+ constructor(config) {
3
+ this.config = config;
4
+ this.name = "vercel";
5
+ }
6
+ async fetchEvents(since) {
7
+ if (!this.config.vercelToken) {
8
+ return [];
9
+ }
10
+ // TODO: hook into the Vercel API once the token and project are configured.
11
+ return [];
12
+ }
13
+ }
@@ -0,0 +1,17 @@
1
+ import { buildIncidentClusters } from "./timeline.js";
2
+ import { matchPattern } from "./pattern-engine.js";
3
+ export function correlateEvents(events) {
4
+ const sortedTimeline = [...events].sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
5
+ const clusters = buildIncidentClusters(events);
6
+ for (const cluster of clusters) {
7
+ const match = matchPattern(cluster);
8
+ if (match) {
9
+ return {
10
+ cluster,
11
+ match,
12
+ timeline: sortedTimeline
13
+ };
14
+ }
15
+ }
16
+ return null;
17
+ }
@@ -0,0 +1,49 @@
1
+ import { loadConfig } from "../config/load.js";
2
+ import { VercelConnector } from "../connectors/vercel.js";
3
+ import { TriggerConnector } from "../connectors/trigger.js";
4
+ import { LocalConnector } from "../connectors/local.js";
5
+ import { DatadogConnector } from "../connectors/datadog.js";
6
+ import { correlateEvents } from "../core/correlator.js";
7
+ import { explainIncident } from "../llm/explain.js";
8
+ const DEFAULT_WINDOW_MINUTES = 15;
9
+ function formatEvent(event) {
10
+ return `${event.timestamp.toISOString()} ${event.source} ${event.type}`;
11
+ }
12
+ export async function runInvestigation(options = {}) {
13
+ const config = await loadConfig();
14
+ const windowMinutes = options.windowMinutes ?? DEFAULT_WINDOW_MINUTES;
15
+ const windowStart = new Date(Date.now() - windowMinutes * 60 * 1000);
16
+ const connectors = [
17
+ new VercelConnector(config),
18
+ new TriggerConnector(config),
19
+ new LocalConnector(config),
20
+ new DatadogConnector(config)
21
+ ];
22
+ console.log(`🐸 Scanning last ${windowMinutes} minutes...`);
23
+ const events = await Promise.all(connectors.map((connector) => connector.fetchEvents(windowStart)));
24
+ const flattened = events.flat();
25
+ const report = correlateEvents(flattened);
26
+ if (!report) {
27
+ console.log("🐸 No significant failure patterns detected.");
28
+ return;
29
+ }
30
+ const explanation = await explainIncident({
31
+ pattern: report.match.patternId,
32
+ timeline: report.timeline,
33
+ evidence: report.match.evidence,
34
+ confidence: report.match.confidence,
35
+ rootCauseEvent: report.match.rootCauseEvent,
36
+ query: options.query
37
+ });
38
+ console.log("Incident detected.");
39
+ console.log("Trigger:");
40
+ console.log(`- ${report.match.rootCauseEvent.type}`);
41
+ console.log("Cascade:");
42
+ report.match.evidence
43
+ .slice(1)
44
+ .forEach((event) => console.log(`→ ${formatEvent(event)}`));
45
+ console.log("Root Cause:");
46
+ console.log(`- ${explanation.explanation}`);
47
+ console.log(`Confidence: ${report.match.confidence.toFixed(2)}`);
48
+ console.log(`Suggested Fix: ${explanation.suggestion}`);
49
+ }
@@ -0,0 +1,108 @@
1
+ const patternMatchers = [
2
+ matchServerlessTimeoutCascade,
3
+ matchRetryStorm,
4
+ matchDeployFailure,
5
+ matchCrashLoop
6
+ ];
7
+ export function matchPattern(cluster) {
8
+ for (const matcher of patternMatchers) {
9
+ const match = matcher(cluster);
10
+ if (match) {
11
+ return match;
12
+ }
13
+ }
14
+ return null;
15
+ }
16
+ function matchServerlessTimeoutCascade(cluster) {
17
+ const { triggerEvent, cascade } = cluster;
18
+ if (triggerEvent.source !== "vercel") {
19
+ return null;
20
+ }
21
+ if (!triggerEvent.type.toLowerCase().includes("timeout")) {
22
+ return null;
23
+ }
24
+ const evidence = [
25
+ triggerEvent,
26
+ ...cascade.filter((event) => isRetry(event) || isRestart(event) || isExternal429(event))
27
+ ];
28
+ const hasRetry = cascade.some(isRetry);
29
+ const hasRestart = cascade.some(isRestart);
30
+ if (!hasRetry) {
31
+ return null;
32
+ }
33
+ const confidence = hasRestart ? 0.87 : 0.82;
34
+ return buildMatch("SERVERLESS_TIMEOUT_CASCADE", triggerEvent, evidence, confidence, {
35
+ retryCount: cascade.filter(isRetry).length,
36
+ restartDetected: hasRestart
37
+ });
38
+ }
39
+ function matchRetryStorm(cluster) {
40
+ const { cascade } = cluster;
41
+ const retries = cascade.filter((event) => isRetry(event));
42
+ if (retries.length < 3) {
43
+ return null;
44
+ }
45
+ const grouped = groupBy(retries, (event) => String(event.metadata.jobName ?? "unknown"));
46
+ const hasBusyJob = Object.values(grouped).some((jobs) => jobs.length >= 3);
47
+ if (!hasBusyJob) {
48
+ return null;
49
+ }
50
+ const root = retries[0];
51
+ const evidence = retries.slice(0, 5);
52
+ return buildMatch("RETRY_STORM", root, evidence, 0.74, {
53
+ retries: retries.length,
54
+ jobs: Object.keys(grouped)
55
+ });
56
+ }
57
+ function matchDeployFailure(cluster) {
58
+ const timeline = [cluster.triggerEvent, ...cluster.cascade];
59
+ const deployEvent = timeline.find((event) => event.type.toLowerCase().includes("deploy"));
60
+ if (!deployEvent) {
61
+ return null;
62
+ }
63
+ const errorsAfterDeployment = timeline.filter((event) => event.timestamp.getTime() >= deployEvent.timestamp.getTime() && event.severity === "error");
64
+ if (errorsAfterDeployment.length === 0) {
65
+ return null;
66
+ }
67
+ return buildMatch("DEPLOY_FAILURE", deployEvent, errorsAfterDeployment, 0.72, {
68
+ deployEventType: deployEvent.type,
69
+ errorCount: errorsAfterDeployment.length
70
+ });
71
+ }
72
+ function matchCrashLoop(cluster) {
73
+ const restarts = cluster.cascade.filter((event) => isRestart(event));
74
+ if (restarts.length < 2) {
75
+ return null;
76
+ }
77
+ const root = restarts[0];
78
+ const evidence = restarts.slice(0, 3);
79
+ return buildMatch("CRASH_LOOP", root, evidence, 0.66, {
80
+ restartCount: restarts.length
81
+ });
82
+ }
83
+ function isRetry(event) {
84
+ return event.type.toLowerCase().includes("retry") || event.metadata?.retry === true;
85
+ }
86
+ function isRestart(event) {
87
+ return event.type.toLowerCase().includes("restart") || event.type.toLowerCase().includes("duplicate execution");
88
+ }
89
+ function isExternal429(event) {
90
+ return event.metadata?.status === 429 || String(event.metadata?.statusCode ?? "").startsWith("429");
91
+ }
92
+ function groupBy(list, select) {
93
+ return list.reduce((acc, item) => {
94
+ const key = select(item);
95
+ acc[key] = acc[key] ?? [];
96
+ acc[key].push(item);
97
+ return acc;
98
+ }, {});
99
+ }
100
+ function buildMatch(patternId, rootCauseEvent, evidence, confidence, metadata) {
101
+ return {
102
+ patternId,
103
+ rootCauseEvent,
104
+ evidence,
105
+ confidence,
106
+ metadata
107
+ };
108
+ }
@@ -0,0 +1,24 @@
1
+ const CLUSTER_WINDOW_MS = 15 * 60 * 1000;
2
+ export function buildIncidentClusters(events) {
3
+ const sorted = [...events].sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
4
+ const clusters = [];
5
+ for (const event of sorted) {
6
+ if (event.severity !== "error" && event.severity !== "warn") {
7
+ continue;
8
+ }
9
+ const cascade = sorted.filter((candidate) => {
10
+ const offset = candidate.timestamp.getTime() - event.timestamp.getTime();
11
+ return offset >= 0 && offset <= CLUSTER_WINDOW_MS && candidate !== event;
12
+ });
13
+ const windowEnd = cascade.length
14
+ ? cascade[cascade.length - 1].timestamp
15
+ : event.timestamp;
16
+ clusters.push({
17
+ triggerEvent: event,
18
+ cascade,
19
+ windowStart: event.timestamp,
20
+ windowEnd
21
+ });
22
+ }
23
+ return clusters;
24
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,14 @@
1
+ export async function explainIncident(input) {
2
+ const timelineSummary = input.timeline
3
+ .map((event) => `- ${event.timestamp.toISOString()} ${event.source} ${event.type}`)
4
+ .join("\n");
5
+ const evidenceSummary = input.evidence
6
+ .map((event) => `- ${event.timestamp.toISOString()} ${event.source} ${event.type}`)
7
+ .join("\n");
8
+ const explanation = `Detected pattern ${input.pattern} with confidence ${input.confidence.toFixed(2)}. Root cause event: ${input.rootCauseEvent.type}.`;
9
+ const suggestion = `Review ${input.rootCauseEvent.source} ${input.rootCauseEvent.type} around ${input.rootCauseEvent.timestamp.toISOString()} to mitigate.`;
10
+ return {
11
+ explanation,
12
+ suggestion
13
+ };
14
+ }