triagent 0.1.0-alpha13 → 0.1.0-alpha18

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 (34) hide show
  1. package/package.json +3 -4
  2. package/src/cli/config.ts +96 -0
  3. package/src/index.ts +201 -3
  4. package/src/integrations/elasticsearch/client.ts +210 -0
  5. package/src/integrations/grafana/client.ts +186 -0
  6. package/src/integrations/kubernetes/multi-cluster.ts +199 -0
  7. package/src/integrations/kubernetes/types.ts +24 -0
  8. package/src/integrations/loki/client.ts +219 -0
  9. package/src/integrations/prometheus/client.ts +163 -0
  10. package/src/integrations/slack/client.ts +265 -0
  11. package/src/integrations/teams/client.ts +199 -0
  12. package/src/mastra/agents/debugger.ts +152 -108
  13. package/src/mastra/tools/approval-store.ts +180 -0
  14. package/src/mastra/tools/cli.ts +94 -2
  15. package/src/mastra/tools/cost.ts +389 -0
  16. package/src/mastra/tools/logs.ts +210 -0
  17. package/src/mastra/tools/network.ts +253 -0
  18. package/src/mastra/tools/prometheus.ts +221 -0
  19. package/src/mastra/tools/remediation.ts +365 -0
  20. package/src/mastra/tools/runbook.ts +186 -0
  21. package/src/server/routes/history.ts +207 -0
  22. package/src/server/routes/notifications.ts +236 -0
  23. package/src/server/webhook.ts +36 -2
  24. package/src/storage/index.ts +3 -0
  25. package/src/storage/investigation-history.ts +277 -0
  26. package/src/storage/runbook-index.ts +330 -0
  27. package/src/storage/types.ts +72 -0
  28. package/src/tui/app.tsx +492 -76
  29. package/src/tui/components/approval-dialog.tsx +156 -0
  30. package/src/tui/components/approval-modal.tsx +278 -0
  31. package/src/tui/components/index.ts +38 -0
  32. package/src/tui/components/styled-span.tsx +24 -0
  33. package/src/tui/components/timeline.tsx +223 -0
  34. package/src/tui/components/toast.tsx +101 -0
@@ -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
+ }
@@ -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
  }
@@ -0,0 +1,3 @@
1
+ export * from "./types.js";
2
+ export * from "./investigation-history.js";
3
+ export * from "./runbook-index.js";