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.
Files changed (47) hide show
  1. package/README.md +101 -1
  2. package/package.json +9 -3
  3. package/src/cli/config.ts +118 -2
  4. package/src/config.ts +23 -3
  5. package/src/index.ts +262 -6
  6. package/src/integrations/elasticsearch/client.ts +210 -0
  7. package/src/integrations/grafana/client.ts +186 -0
  8. package/src/integrations/kubernetes/multi-cluster.ts +199 -0
  9. package/src/integrations/kubernetes/types.ts +24 -0
  10. package/src/integrations/loki/client.ts +219 -0
  11. package/src/integrations/prometheus/client.ts +163 -0
  12. package/src/integrations/slack/client.ts +265 -0
  13. package/src/integrations/teams/client.ts +199 -0
  14. package/src/mastra/agents/debugger.ts +164 -109
  15. package/src/mastra/index.ts +2 -2
  16. package/src/mastra/tools/approval-store.ts +180 -0
  17. package/src/mastra/tools/cli.ts +94 -2
  18. package/src/mastra/tools/cost.ts +389 -0
  19. package/src/mastra/tools/logs.ts +210 -0
  20. package/src/mastra/tools/network.ts +253 -0
  21. package/src/mastra/tools/prometheus.ts +221 -0
  22. package/src/mastra/tools/remediation.ts +365 -0
  23. package/src/mastra/tools/runbook.ts +186 -0
  24. package/src/sandbox/bashlet.ts +76 -10
  25. package/src/server/routes/history.ts +207 -0
  26. package/src/server/routes/notifications.ts +236 -0
  27. package/src/server/webhook.ts +36 -2
  28. package/src/storage/index.ts +3 -0
  29. package/src/storage/investigation-history.ts +277 -0
  30. package/src/storage/runbook-index.ts +330 -0
  31. package/src/storage/types.ts +72 -0
  32. package/src/tui/app.tsx +278 -197
  33. package/src/tui/components/approval-dialog.tsx +147 -0
  34. package/src/tui/components/approval-modal.tsx +278 -0
  35. package/src/tui/components/centered-layout.tsx +33 -0
  36. package/src/tui/components/editor.tsx +87 -0
  37. package/src/tui/components/header.tsx +53 -0
  38. package/src/tui/components/index.ts +55 -0
  39. package/src/tui/components/message-item.tsx +131 -0
  40. package/src/tui/components/messages-panel.tsx +71 -0
  41. package/src/tui/components/status-badge.tsx +20 -0
  42. package/src/tui/components/status-bar.tsx +39 -0
  43. package/src/tui/components/styled-span.tsx +24 -0
  44. package/src/tui/components/timeline.tsx +223 -0
  45. package/src/tui/components/toast.tsx +104 -0
  46. package/src/tui/theme/index.ts +21 -0
  47. package/src/tui/theme/tokens.ts +180 -0
@@ -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
- codebasePath: string;
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.codebasePath;
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
- { hostPath: options.codebasePath, guestPath: "/workspace" },
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(config: Config, useHost: boolean = false): void {
145
- createSandbox({
146
- codebasePath: config.codebasePath,
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
+ }
@@ -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";