triagent 0.1.0-alpha9 → 0.1.0-beta2
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 +101 -1
- package/package.json +9 -3
- package/src/cli/config.ts +118 -2
- package/src/config.ts +23 -3
- package/src/index.ts +262 -6
- package/src/integrations/elasticsearch/client.ts +210 -0
- package/src/integrations/grafana/client.ts +186 -0
- package/src/integrations/kubernetes/multi-cluster.ts +199 -0
- package/src/integrations/kubernetes/types.ts +24 -0
- package/src/integrations/loki/client.ts +219 -0
- package/src/integrations/prometheus/client.ts +163 -0
- package/src/integrations/slack/client.ts +265 -0
- package/src/integrations/teams/client.ts +199 -0
- package/src/mastra/agents/debugger.ts +164 -109
- package/src/mastra/index.ts +2 -2
- package/src/mastra/tools/approval-store.ts +180 -0
- package/src/mastra/tools/cli.ts +94 -2
- package/src/mastra/tools/cost.ts +389 -0
- package/src/mastra/tools/logs.ts +210 -0
- package/src/mastra/tools/network.ts +253 -0
- package/src/mastra/tools/prometheus.ts +221 -0
- package/src/mastra/tools/remediation.ts +365 -0
- package/src/mastra/tools/runbook.ts +186 -0
- package/src/sandbox/bashlet.ts +76 -10
- package/src/server/routes/history.ts +207 -0
- package/src/server/routes/notifications.ts +236 -0
- package/src/server/webhook.ts +36 -2
- package/src/storage/index.ts +3 -0
- package/src/storage/investigation-history.ts +277 -0
- package/src/storage/runbook-index.ts +330 -0
- package/src/storage/types.ts +72 -0
- package/src/tui/app.tsx +278 -197
- package/src/tui/components/approval-dialog.tsx +147 -0
- package/src/tui/components/approval-modal.tsx +278 -0
- package/src/tui/components/centered-layout.tsx +33 -0
- package/src/tui/components/editor.tsx +87 -0
- package/src/tui/components/header.tsx +53 -0
- package/src/tui/components/index.ts +55 -0
- package/src/tui/components/message-item.tsx +131 -0
- package/src/tui/components/messages-panel.tsx +71 -0
- package/src/tui/components/status-badge.tsx +20 -0
- package/src/tui/components/status-bar.tsx +39 -0
- package/src/tui/components/styled-span.tsx +24 -0
- package/src/tui/components/timeline.tsx +223 -0
- package/src/tui/components/toast.tsx +104 -0
- package/src/tui/theme/index.ts +21 -0
- package/src/tui/theme/tokens.ts +180 -0
package/src/sandbox/bashlet.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { Bashlet } from "@bashlet/sdk";
|
|
1
|
+
import { Bashlet, type SshOptions } from "@bashlet/sdk";
|
|
2
2
|
import { $ } from "bun";
|
|
3
3
|
import { readFile as fsReadFile, readdir } from "fs/promises";
|
|
4
|
-
import type { Config } from "../config.js";
|
|
4
|
+
import type { Config, CodebaseEntry } from "../config.js";
|
|
5
5
|
|
|
6
6
|
export interface CommandResult {
|
|
7
7
|
stdout: string;
|
|
@@ -10,19 +10,54 @@ export interface CommandResult {
|
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
export interface SandboxOptions {
|
|
13
|
-
|
|
13
|
+
codebasePaths: CodebaseEntry[];
|
|
14
14
|
kubeConfigPath: string;
|
|
15
15
|
timeout?: number;
|
|
16
16
|
useHost?: boolean;
|
|
17
|
+
/** Backend to use: 'docker' (default) or 'ssh' */
|
|
18
|
+
backend?: "docker" | "ssh";
|
|
19
|
+
/** SSH configuration (required when backend='ssh') */
|
|
20
|
+
ssh?: {
|
|
21
|
+
host: string;
|
|
22
|
+
user: string;
|
|
23
|
+
port?: number;
|
|
24
|
+
keyFile?: string;
|
|
25
|
+
};
|
|
17
26
|
}
|
|
18
27
|
|
|
28
|
+
// Sandbox state
|
|
19
29
|
let bashletInstance: Bashlet | null = null;
|
|
20
30
|
let hostMode = false;
|
|
21
31
|
let hostWorkdir = "./";
|
|
32
|
+
let sshMode = false;
|
|
33
|
+
let sshWorkdir = "/workspace";
|
|
22
34
|
|
|
23
|
-
export function createSandbox(options: SandboxOptions): void {
|
|
35
|
+
export async function createSandbox(options: SandboxOptions): Promise<void> {
|
|
24
36
|
hostMode = options.useHost ?? false;
|
|
25
|
-
hostWorkdir = options.
|
|
37
|
+
hostWorkdir = options.codebasePaths[0]?.path || "./";
|
|
38
|
+
|
|
39
|
+
// Handle SSH backend mode
|
|
40
|
+
if (options.backend === "ssh" && options.ssh) {
|
|
41
|
+
sshMode = true;
|
|
42
|
+
|
|
43
|
+
const sshOptions: SshOptions = {
|
|
44
|
+
host: options.ssh.host,
|
|
45
|
+
user: options.ssh.user,
|
|
46
|
+
port: options.ssh.port,
|
|
47
|
+
keyFile: options.ssh.keyFile,
|
|
48
|
+
useControlMaster: true,
|
|
49
|
+
connectTimeout: 30,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
bashletInstance = new Bashlet({
|
|
53
|
+
backend: "ssh",
|
|
54
|
+
ssh: sshOptions,
|
|
55
|
+
workdir: sshWorkdir,
|
|
56
|
+
timeout: options.timeout || 120,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
26
61
|
|
|
27
62
|
if (hostMode) {
|
|
28
63
|
return;
|
|
@@ -32,9 +67,15 @@ export function createSandbox(options: SandboxOptions): void {
|
|
|
32
67
|
return;
|
|
33
68
|
}
|
|
34
69
|
|
|
70
|
+
// Mount each codebase at /workspace/<name>
|
|
71
|
+
const codebaseMounts = options.codebasePaths.map((entry) => ({
|
|
72
|
+
hostPath: entry.path,
|
|
73
|
+
guestPath: `/workspace/${entry.name}`,
|
|
74
|
+
}));
|
|
75
|
+
|
|
35
76
|
bashletInstance = new Bashlet({
|
|
36
77
|
mounts: [
|
|
37
|
-
|
|
78
|
+
...codebaseMounts,
|
|
38
79
|
{ hostPath: options.kubeConfigPath, guestPath: "/root/.kube" },
|
|
39
80
|
],
|
|
40
81
|
workdir: "/workspace",
|
|
@@ -50,7 +91,18 @@ export function isHostMode(): boolean {
|
|
|
50
91
|
return hostMode;
|
|
51
92
|
}
|
|
52
93
|
|
|
94
|
+
export function isRemoteMode(): boolean {
|
|
95
|
+
return sshMode;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function getRemoteInfo(): { target: string; workdir: string; sessionId: string } | null {
|
|
99
|
+
if (!sshMode || !bashletInstance) return null;
|
|
100
|
+
// Return a placeholder - actual SSH details are managed by bashlet
|
|
101
|
+
return { target: "ssh", workdir: sshWorkdir, sessionId: "bashlet-ssh" };
|
|
102
|
+
}
|
|
103
|
+
|
|
53
104
|
export async function execCommand(command: string): Promise<CommandResult> {
|
|
105
|
+
// Host mode: execute locally
|
|
54
106
|
if (hostMode) {
|
|
55
107
|
try {
|
|
56
108
|
const result = await $`sh -c ${command}`.cwd(hostWorkdir).nothrow().quiet();
|
|
@@ -68,6 +120,7 @@ export async function execCommand(command: string): Promise<CommandResult> {
|
|
|
68
120
|
}
|
|
69
121
|
}
|
|
70
122
|
|
|
123
|
+
// Sandbox or SSH mode: execute via bashlet
|
|
71
124
|
if (!bashletInstance) {
|
|
72
125
|
throw new Error("Sandbox not initialized. Call createSandbox first.");
|
|
73
126
|
}
|
|
@@ -89,6 +142,7 @@ export async function execCommand(command: string): Promise<CommandResult> {
|
|
|
89
142
|
}
|
|
90
143
|
|
|
91
144
|
export async function readFile(path: string): Promise<string> {
|
|
145
|
+
// Host mode: read locally
|
|
92
146
|
if (hostMode) {
|
|
93
147
|
const fullPath = path.startsWith("/") ? path : `${hostWorkdir}/${path}`;
|
|
94
148
|
try {
|
|
@@ -100,6 +154,7 @@ export async function readFile(path: string): Promise<string> {
|
|
|
100
154
|
}
|
|
101
155
|
}
|
|
102
156
|
|
|
157
|
+
// Sandbox or SSH mode: read via bashlet
|
|
103
158
|
if (!bashletInstance) {
|
|
104
159
|
throw new Error("Sandbox not initialized. Call createSandbox first.");
|
|
105
160
|
}
|
|
@@ -115,6 +170,7 @@ export async function readFile(path: string): Promise<string> {
|
|
|
115
170
|
}
|
|
116
171
|
|
|
117
172
|
export async function listDir(path: string): Promise<string[]> {
|
|
173
|
+
// Host mode: list locally
|
|
118
174
|
if (hostMode) {
|
|
119
175
|
const fullPath = path.startsWith("/") ? path : `${hostWorkdir}/${path}`;
|
|
120
176
|
try {
|
|
@@ -127,6 +183,7 @@ export async function listDir(path: string): Promise<string[]> {
|
|
|
127
183
|
}
|
|
128
184
|
}
|
|
129
185
|
|
|
186
|
+
// Sandbox or SSH mode: list via bashlet
|
|
130
187
|
if (!bashletInstance) {
|
|
131
188
|
throw new Error("Sandbox not initialized. Call createSandbox first.");
|
|
132
189
|
}
|
|
@@ -141,11 +198,20 @@ export async function listDir(path: string): Promise<string[]> {
|
|
|
141
198
|
}
|
|
142
199
|
}
|
|
143
200
|
|
|
144
|
-
export function initSandboxFromConfig(
|
|
145
|
-
|
|
146
|
-
|
|
201
|
+
export async function initSandboxFromConfig(
|
|
202
|
+
config: Config,
|
|
203
|
+
options: {
|
|
204
|
+
useHost?: boolean;
|
|
205
|
+
backend?: "docker" | "ssh";
|
|
206
|
+
ssh?: { host: string; user: string; port?: number; keyFile?: string };
|
|
207
|
+
} = {}
|
|
208
|
+
): Promise<void> {
|
|
209
|
+
await createSandbox({
|
|
210
|
+
codebasePaths: config.codebasePaths,
|
|
147
211
|
kubeConfigPath: config.kubeConfigPath,
|
|
148
212
|
timeout: 120,
|
|
149
|
-
useHost,
|
|
213
|
+
useHost: options.useHost,
|
|
214
|
+
backend: options.backend,
|
|
215
|
+
ssh: options.ssh,
|
|
150
216
|
});
|
|
151
217
|
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { getHistoryStore } from "../../storage/index.js";
|
|
4
|
+
import type { HistoryQueryOptions } from "../../storage/types.js";
|
|
5
|
+
|
|
6
|
+
const QueryParamsSchema = z.object({
|
|
7
|
+
limit: z.coerce.number().int().positive().max(100).optional(),
|
|
8
|
+
offset: z.coerce.number().int().nonnegative().optional(),
|
|
9
|
+
status: z.enum(["pending", "running", "completed", "failed"]).optional(),
|
|
10
|
+
cluster: z.string().optional(),
|
|
11
|
+
tags: z.string().optional(), // comma-separated
|
|
12
|
+
startDate: z.string().datetime().optional(),
|
|
13
|
+
endDate: z.string().datetime().optional(),
|
|
14
|
+
search: z.string().optional(),
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
export function createHistoryRoutes() {
|
|
18
|
+
const app = new Hono();
|
|
19
|
+
const store = getHistoryStore();
|
|
20
|
+
|
|
21
|
+
// List investigations with filtering
|
|
22
|
+
app.get("/", async (c) => {
|
|
23
|
+
try {
|
|
24
|
+
const query = c.req.query();
|
|
25
|
+
const parsed = QueryParamsSchema.safeParse(query);
|
|
26
|
+
|
|
27
|
+
if (!parsed.success) {
|
|
28
|
+
return c.json({ error: "Invalid query parameters", details: parsed.error.errors }, 400);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const options: HistoryQueryOptions = {
|
|
32
|
+
limit: parsed.data.limit,
|
|
33
|
+
offset: parsed.data.offset,
|
|
34
|
+
status: parsed.data.status,
|
|
35
|
+
cluster: parsed.data.cluster,
|
|
36
|
+
tags: parsed.data.tags?.split(",").map((t) => t.trim()),
|
|
37
|
+
startDate: parsed.data.startDate ? new Date(parsed.data.startDate) : undefined,
|
|
38
|
+
endDate: parsed.data.endDate ? new Date(parsed.data.endDate) : undefined,
|
|
39
|
+
searchQuery: parsed.data.search,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const investigations = await store.list(options);
|
|
43
|
+
|
|
44
|
+
return c.json({
|
|
45
|
+
investigations: investigations.map((inv) => ({
|
|
46
|
+
id: inv.id,
|
|
47
|
+
incident: inv.incident,
|
|
48
|
+
status: inv.status,
|
|
49
|
+
cluster: inv.cluster,
|
|
50
|
+
startedAt: inv.startedAt.toISOString(),
|
|
51
|
+
completedAt: inv.completedAt?.toISOString(),
|
|
52
|
+
tags: inv.tags,
|
|
53
|
+
toolCallCount: inv.toolCalls.length,
|
|
54
|
+
eventCount: inv.events.length,
|
|
55
|
+
})),
|
|
56
|
+
count: investigations.length,
|
|
57
|
+
});
|
|
58
|
+
} catch (error) {
|
|
59
|
+
return c.json(
|
|
60
|
+
{ error: "Internal server error", message: error instanceof Error ? error.message : String(error) },
|
|
61
|
+
500
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Get investigation statistics
|
|
67
|
+
app.get("/stats", async (c) => {
|
|
68
|
+
try {
|
|
69
|
+
const stats = await store.getStats();
|
|
70
|
+
return c.json(stats);
|
|
71
|
+
} catch (error) {
|
|
72
|
+
return c.json(
|
|
73
|
+
{ error: "Internal server error", message: error instanceof Error ? error.message : String(error) },
|
|
74
|
+
500
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// Get a specific investigation by ID
|
|
80
|
+
app.get("/:id", async (c) => {
|
|
81
|
+
try {
|
|
82
|
+
const id = c.req.param("id");
|
|
83
|
+
const investigation = await store.get(id);
|
|
84
|
+
|
|
85
|
+
if (!investigation) {
|
|
86
|
+
return c.json({ error: "Investigation not found" }, 404);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return c.json({
|
|
90
|
+
id: investigation.id,
|
|
91
|
+
incident: investigation.incident,
|
|
92
|
+
status: investigation.status,
|
|
93
|
+
cluster: investigation.cluster,
|
|
94
|
+
startedAt: investigation.startedAt.toISOString(),
|
|
95
|
+
completedAt: investigation.completedAt?.toISOString(),
|
|
96
|
+
result: investigation.result,
|
|
97
|
+
rawResult: investigation.rawResult,
|
|
98
|
+
error: investigation.error,
|
|
99
|
+
tags: investigation.tags,
|
|
100
|
+
toolCalls: investigation.toolCalls.map((tc) => ({
|
|
101
|
+
...tc,
|
|
102
|
+
timestamp: tc.timestamp.toISOString(),
|
|
103
|
+
})),
|
|
104
|
+
events: investigation.events.map((ev) => ({
|
|
105
|
+
...ev,
|
|
106
|
+
timestamp: ev.timestamp.toISOString(),
|
|
107
|
+
})),
|
|
108
|
+
});
|
|
109
|
+
} catch (error) {
|
|
110
|
+
return c.json(
|
|
111
|
+
{ error: "Internal server error", message: error instanceof Error ? error.message : String(error) },
|
|
112
|
+
500
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// Get events for an investigation
|
|
118
|
+
app.get("/:id/events", async (c) => {
|
|
119
|
+
try {
|
|
120
|
+
const id = c.req.param("id");
|
|
121
|
+
const investigation = await store.get(id);
|
|
122
|
+
|
|
123
|
+
if (!investigation) {
|
|
124
|
+
return c.json({ error: "Investigation not found" }, 404);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return c.json({
|
|
128
|
+
events: investigation.events.map((ev) => ({
|
|
129
|
+
...ev,
|
|
130
|
+
timestamp: ev.timestamp.toISOString(),
|
|
131
|
+
})),
|
|
132
|
+
});
|
|
133
|
+
} catch (error) {
|
|
134
|
+
return c.json(
|
|
135
|
+
{ error: "Internal server error", message: error instanceof Error ? error.message : String(error) },
|
|
136
|
+
500
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// Get tool calls for an investigation
|
|
142
|
+
app.get("/:id/toolcalls", async (c) => {
|
|
143
|
+
try {
|
|
144
|
+
const id = c.req.param("id");
|
|
145
|
+
const investigation = await store.get(id);
|
|
146
|
+
|
|
147
|
+
if (!investigation) {
|
|
148
|
+
return c.json({ error: "Investigation not found" }, 404);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return c.json({
|
|
152
|
+
toolCalls: investigation.toolCalls.map((tc) => ({
|
|
153
|
+
...tc,
|
|
154
|
+
timestamp: tc.timestamp.toISOString(),
|
|
155
|
+
})),
|
|
156
|
+
});
|
|
157
|
+
} catch (error) {
|
|
158
|
+
return c.json(
|
|
159
|
+
{ error: "Internal server error", message: error instanceof Error ? error.message : String(error) },
|
|
160
|
+
500
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// Delete an investigation
|
|
166
|
+
app.delete("/:id", async (c) => {
|
|
167
|
+
try {
|
|
168
|
+
const id = c.req.param("id");
|
|
169
|
+
const deleted = await store.delete(id);
|
|
170
|
+
|
|
171
|
+
if (!deleted) {
|
|
172
|
+
return c.json({ error: "Investigation not found" }, 404);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return c.json({ message: "Investigation deleted" });
|
|
176
|
+
} catch (error) {
|
|
177
|
+
return c.json(
|
|
178
|
+
{ error: "Internal server error", message: error instanceof Error ? error.message : String(error) },
|
|
179
|
+
500
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// Export investigation as JSON
|
|
185
|
+
app.get("/:id/export", async (c) => {
|
|
186
|
+
try {
|
|
187
|
+
const id = c.req.param("id");
|
|
188
|
+
const investigation = await store.get(id);
|
|
189
|
+
|
|
190
|
+
if (!investigation) {
|
|
191
|
+
return c.json({ error: "Investigation not found" }, 404);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
c.header("Content-Type", "application/json");
|
|
195
|
+
c.header("Content-Disposition", `attachment; filename="investigation-${id}.json"`);
|
|
196
|
+
|
|
197
|
+
return c.json(investigation);
|
|
198
|
+
} catch (error) {
|
|
199
|
+
return c.json(
|
|
200
|
+
{ error: "Internal server error", message: error instanceof Error ? error.message : String(error) },
|
|
201
|
+
500
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
return app;
|
|
207
|
+
}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { getSlackClient } from "../../integrations/slack/client.js";
|
|
4
|
+
import { getTeamsClient } from "../../integrations/teams/client.js";
|
|
5
|
+
import { getHistoryStore } from "../../storage/index.js";
|
|
6
|
+
|
|
7
|
+
const InvestigationNotificationSchema = z.object({
|
|
8
|
+
investigationId: z.string(),
|
|
9
|
+
channel: z.string().optional(),
|
|
10
|
+
includeDetails: z.boolean().default(true),
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
const AlertNotificationSchema = z.object({
|
|
14
|
+
name: z.string(),
|
|
15
|
+
severity: z.enum(["critical", "warning", "info"]),
|
|
16
|
+
message: z.string(),
|
|
17
|
+
source: z.string().optional(),
|
|
18
|
+
channel: z.string().optional(),
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const CustomMessageSchema = z.object({
|
|
22
|
+
text: z.string(),
|
|
23
|
+
channel: z.string().optional(),
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
export function createNotificationRoutes() {
|
|
27
|
+
const app = new Hono();
|
|
28
|
+
|
|
29
|
+
// Send investigation summary to Slack
|
|
30
|
+
app.post("/slack/investigation", async (c) => {
|
|
31
|
+
try {
|
|
32
|
+
const slackClient = getSlackClient();
|
|
33
|
+
if (!slackClient) {
|
|
34
|
+
return c.json({ error: "Slack not configured" }, 400);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const body = await c.req.json();
|
|
38
|
+
const parsed = InvestigationNotificationSchema.safeParse(body);
|
|
39
|
+
if (!parsed.success) {
|
|
40
|
+
return c.json({ error: "Invalid request", details: parsed.error.errors }, 400);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const { investigationId, channel, includeDetails } = parsed.data;
|
|
44
|
+
const historyStore = getHistoryStore();
|
|
45
|
+
const investigation = await historyStore.get(investigationId);
|
|
46
|
+
|
|
47
|
+
if (!investigation) {
|
|
48
|
+
return c.json({ error: "Investigation not found" }, 404);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const message = slackClient.buildInvestigationMessage({
|
|
52
|
+
id: investigation.id,
|
|
53
|
+
title: investigation.incident.title,
|
|
54
|
+
status: investigation.status,
|
|
55
|
+
summary: investigation.result?.summary,
|
|
56
|
+
severity: investigation.incident.severity,
|
|
57
|
+
rootCause: includeDetails ? investigation.result?.rootCause : undefined,
|
|
58
|
+
recommendations: includeDetails
|
|
59
|
+
? investigation.result?.recommendations?.map((r) => r.action)
|
|
60
|
+
: undefined,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
if (channel) {
|
|
64
|
+
message.channel = channel;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const result = await slackClient.sendMessage(message);
|
|
68
|
+
|
|
69
|
+
if (result.ok) {
|
|
70
|
+
return c.json({ success: true, ts: result.ts, channel: result.channel });
|
|
71
|
+
} else {
|
|
72
|
+
return c.json({ error: result.error || "Failed to send message" }, 500);
|
|
73
|
+
}
|
|
74
|
+
} catch (error) {
|
|
75
|
+
return c.json(
|
|
76
|
+
{ error: "Internal server error", message: error instanceof Error ? error.message : String(error) },
|
|
77
|
+
500
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// Send alert to Slack
|
|
83
|
+
app.post("/slack/alert", async (c) => {
|
|
84
|
+
try {
|
|
85
|
+
const slackClient = getSlackClient();
|
|
86
|
+
if (!slackClient) {
|
|
87
|
+
return c.json({ error: "Slack not configured" }, 400);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const body = await c.req.json();
|
|
91
|
+
const parsed = AlertNotificationSchema.safeParse(body);
|
|
92
|
+
if (!parsed.success) {
|
|
93
|
+
return c.json({ error: "Invalid request", details: parsed.error.errors }, 400);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const message = slackClient.buildAlertMessage({
|
|
97
|
+
...parsed.data,
|
|
98
|
+
timestamp: new Date(),
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
if (parsed.data.channel) {
|
|
102
|
+
message.channel = parsed.data.channel;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const result = await slackClient.sendMessage(message);
|
|
106
|
+
|
|
107
|
+
if (result.ok) {
|
|
108
|
+
return c.json({ success: true, ts: result.ts });
|
|
109
|
+
} else {
|
|
110
|
+
return c.json({ error: result.error || "Failed to send message" }, 500);
|
|
111
|
+
}
|
|
112
|
+
} catch (error) {
|
|
113
|
+
return c.json(
|
|
114
|
+
{ error: "Internal server error", message: error instanceof Error ? error.message : String(error) },
|
|
115
|
+
500
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// Send custom message to Slack
|
|
121
|
+
app.post("/slack/message", async (c) => {
|
|
122
|
+
try {
|
|
123
|
+
const slackClient = getSlackClient();
|
|
124
|
+
if (!slackClient) {
|
|
125
|
+
return c.json({ error: "Slack not configured" }, 400);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const body = await c.req.json();
|
|
129
|
+
const parsed = CustomMessageSchema.safeParse(body);
|
|
130
|
+
if (!parsed.success) {
|
|
131
|
+
return c.json({ error: "Invalid request", details: parsed.error.errors }, 400);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const result = await slackClient.sendMessage({
|
|
135
|
+
text: parsed.data.text,
|
|
136
|
+
channel: parsed.data.channel,
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
if (result.ok) {
|
|
140
|
+
return c.json({ success: true, ts: result.ts });
|
|
141
|
+
} else {
|
|
142
|
+
return c.json({ error: result.error || "Failed to send message" }, 500);
|
|
143
|
+
}
|
|
144
|
+
} catch (error) {
|
|
145
|
+
return c.json(
|
|
146
|
+
{ error: "Internal server error", message: error instanceof Error ? error.message : String(error) },
|
|
147
|
+
500
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// Send investigation summary to Teams
|
|
153
|
+
app.post("/teams/investigation", async (c) => {
|
|
154
|
+
try {
|
|
155
|
+
const teamsClient = getTeamsClient();
|
|
156
|
+
if (!teamsClient) {
|
|
157
|
+
return c.json({ error: "Teams not configured" }, 400);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const body = await c.req.json();
|
|
161
|
+
const parsed = InvestigationNotificationSchema.safeParse(body);
|
|
162
|
+
if (!parsed.success) {
|
|
163
|
+
return c.json({ error: "Invalid request", details: parsed.error.errors }, 400);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const { investigationId, includeDetails } = parsed.data;
|
|
167
|
+
const historyStore = getHistoryStore();
|
|
168
|
+
const investigation = await historyStore.get(investigationId);
|
|
169
|
+
|
|
170
|
+
if (!investigation) {
|
|
171
|
+
return c.json({ error: "Investigation not found" }, 404);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const message = teamsClient.buildInvestigationMessage({
|
|
175
|
+
id: investigation.id,
|
|
176
|
+
title: investigation.incident.title,
|
|
177
|
+
status: investigation.status,
|
|
178
|
+
summary: investigation.result?.summary,
|
|
179
|
+
severity: investigation.incident.severity,
|
|
180
|
+
rootCause: includeDetails ? investigation.result?.rootCause : undefined,
|
|
181
|
+
recommendations: includeDetails
|
|
182
|
+
? investigation.result?.recommendations?.map((r) => r.action)
|
|
183
|
+
: undefined,
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
const success = await teamsClient.sendMessage(message);
|
|
187
|
+
|
|
188
|
+
if (success) {
|
|
189
|
+
return c.json({ success: true });
|
|
190
|
+
} else {
|
|
191
|
+
return c.json({ error: "Failed to send message" }, 500);
|
|
192
|
+
}
|
|
193
|
+
} catch (error) {
|
|
194
|
+
return c.json(
|
|
195
|
+
{ error: "Internal server error", message: error instanceof Error ? error.message : String(error) },
|
|
196
|
+
500
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// Send alert to Teams
|
|
202
|
+
app.post("/teams/alert", async (c) => {
|
|
203
|
+
try {
|
|
204
|
+
const teamsClient = getTeamsClient();
|
|
205
|
+
if (!teamsClient) {
|
|
206
|
+
return c.json({ error: "Teams not configured" }, 400);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const body = await c.req.json();
|
|
210
|
+
const parsed = AlertNotificationSchema.safeParse(body);
|
|
211
|
+
if (!parsed.success) {
|
|
212
|
+
return c.json({ error: "Invalid request", details: parsed.error.errors }, 400);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const message = teamsClient.buildAlertMessage({
|
|
216
|
+
...parsed.data,
|
|
217
|
+
timestamp: new Date(),
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
const success = await teamsClient.sendMessage(message);
|
|
221
|
+
|
|
222
|
+
if (success) {
|
|
223
|
+
return c.json({ success: true });
|
|
224
|
+
} else {
|
|
225
|
+
return c.json({ error: "Failed to send message" }, 500);
|
|
226
|
+
}
|
|
227
|
+
} catch (error) {
|
|
228
|
+
return c.json(
|
|
229
|
+
{ error: "Internal server error", message: error instanceof Error ? error.message : String(error) },
|
|
230
|
+
500
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
return app;
|
|
236
|
+
}
|
package/src/server/webhook.ts
CHANGED
|
@@ -7,6 +7,9 @@ import {
|
|
|
7
7
|
buildIncidentPrompt,
|
|
8
8
|
type IncidentInput,
|
|
9
9
|
} from "../mastra/index.js";
|
|
10
|
+
import { createHistoryRoutes } from "./routes/history.js";
|
|
11
|
+
import { createNotificationRoutes } from "./routes/notifications.js";
|
|
12
|
+
import { getHistoryStore, type InvestigationHistory } from "../storage/index.js";
|
|
10
13
|
|
|
11
14
|
const IncidentRequestSchema = z.object({
|
|
12
15
|
title: z.string().min(1),
|
|
@@ -29,6 +32,13 @@ const investigations = new Map<string, Investigation>();
|
|
|
29
32
|
|
|
30
33
|
export function createWebhookServer() {
|
|
31
34
|
const app = new Hono();
|
|
35
|
+
const historyStore = getHistoryStore();
|
|
36
|
+
|
|
37
|
+
// Mount history routes
|
|
38
|
+
app.route("/history", createHistoryRoutes());
|
|
39
|
+
|
|
40
|
+
// Mount notification routes
|
|
41
|
+
app.route("/notifications", createNotificationRoutes());
|
|
32
42
|
|
|
33
43
|
// Health check
|
|
34
44
|
app.get("/health", (c) => {
|
|
@@ -134,6 +144,18 @@ async function runInvestigation(id: string): Promise<void> {
|
|
|
134
144
|
|
|
135
145
|
investigation.status = "running";
|
|
136
146
|
|
|
147
|
+
// Create persistent history record
|
|
148
|
+
const historyStore = getHistoryStore();
|
|
149
|
+
const historyRecord: InvestigationHistory = {
|
|
150
|
+
id,
|
|
151
|
+
incident: investigation.incident,
|
|
152
|
+
status: "running",
|
|
153
|
+
startedAt: investigation.startedAt,
|
|
154
|
+
events: [],
|
|
155
|
+
toolCalls: [],
|
|
156
|
+
};
|
|
157
|
+
await historyStore.save(historyRecord);
|
|
158
|
+
|
|
137
159
|
try {
|
|
138
160
|
const agent = getDebuggerAgent();
|
|
139
161
|
const prompt = buildIncidentPrompt(investigation.incident);
|
|
@@ -145,15 +167,21 @@ async function runInvestigation(id: string): Promise<void> {
|
|
|
145
167
|
{ role: "user", content: prompt },
|
|
146
168
|
], {
|
|
147
169
|
maxSteps: 20,
|
|
148
|
-
onStepFinish: ({ toolCalls }) => {
|
|
170
|
+
onStepFinish: async ({ toolCalls }) => {
|
|
149
171
|
if (toolCalls && toolCalls.length > 0) {
|
|
150
172
|
const toolCall = toolCalls[0];
|
|
151
|
-
const toolName = "toolName" in toolCall ? toolCall.toolName : "tool";
|
|
173
|
+
const toolName = "toolName" in toolCall ? String(toolCall.toolName) : "tool";
|
|
152
174
|
const args = "args" in toolCall ? toolCall.args : {};
|
|
153
175
|
console.log(`[Investigation ${id}] Tool: ${toolName}`);
|
|
154
176
|
if (args && typeof args === "object" && "command" in args) {
|
|
155
177
|
console.log(`[Investigation ${id}] $ ${args.command}`);
|
|
156
178
|
}
|
|
179
|
+
|
|
180
|
+
// Record tool call in history
|
|
181
|
+
await historyStore.addToolCall(id, {
|
|
182
|
+
toolName,
|
|
183
|
+
args: args as Record<string, unknown>,
|
|
184
|
+
});
|
|
157
185
|
}
|
|
158
186
|
},
|
|
159
187
|
});
|
|
@@ -162,12 +190,18 @@ async function runInvestigation(id: string): Promise<void> {
|
|
|
162
190
|
investigation.completedAt = new Date();
|
|
163
191
|
investigation.result = response.text;
|
|
164
192
|
|
|
193
|
+
// Update history with completion
|
|
194
|
+
await historyStore.updateStatus(id, "completed", undefined, response.text);
|
|
195
|
+
|
|
165
196
|
console.log(`[Investigation ${id}] Completed`);
|
|
166
197
|
} catch (error) {
|
|
167
198
|
investigation.status = "failed";
|
|
168
199
|
investigation.completedAt = new Date();
|
|
169
200
|
investigation.error = error instanceof Error ? error.message : String(error);
|
|
170
201
|
|
|
202
|
+
// Update history with failure
|
|
203
|
+
await historyStore.updateStatus(id, "failed", undefined, undefined, investigation.error);
|
|
204
|
+
|
|
171
205
|
console.error(`[Investigation ${id}] Failed:`, investigation.error);
|
|
172
206
|
}
|
|
173
207
|
}
|