triagent 0.1.0-alpha8 → 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 -198
  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
@@ -0,0 +1,163 @@
1
+ import type { PrometheusConfig } from "../../cli/config.js";
2
+
3
+ export interface PrometheusQueryResult {
4
+ status: "success" | "error";
5
+ data?: {
6
+ resultType: "vector" | "matrix" | "scalar" | "string";
7
+ result: Array<{
8
+ metric: Record<string, string>;
9
+ value?: [number, string];
10
+ values?: Array<[number, string]>;
11
+ }>;
12
+ };
13
+ error?: string;
14
+ errorType?: string;
15
+ }
16
+
17
+ export interface PrometheusAlert {
18
+ labels: Record<string, string>;
19
+ annotations: Record<string, string>;
20
+ state: "firing" | "pending" | "inactive";
21
+ activeAt: string;
22
+ value: string;
23
+ }
24
+
25
+ export interface PrometheusTarget {
26
+ labels: Record<string, string>;
27
+ scrapePool: string;
28
+ scrapeUrl: string;
29
+ health: "up" | "down" | "unknown";
30
+ lastScrape: string;
31
+ lastError: string;
32
+ }
33
+
34
+ export class PrometheusClient {
35
+ private baseUrl: string;
36
+ private authToken?: string;
37
+
38
+ constructor(config: PrometheusConfig) {
39
+ this.baseUrl = config.url.replace(/\/$/, "");
40
+ this.authToken = config.auth?.token;
41
+ }
42
+
43
+ private async request<T>(endpoint: string, params?: Record<string, string>): Promise<T> {
44
+ const url = new URL(`${this.baseUrl}${endpoint}`);
45
+ if (params) {
46
+ for (const [key, value] of Object.entries(params)) {
47
+ url.searchParams.set(key, value);
48
+ }
49
+ }
50
+
51
+ const headers: Record<string, string> = {
52
+ "Accept": "application/json",
53
+ };
54
+
55
+ if (this.authToken) {
56
+ headers["Authorization"] = `Bearer ${this.authToken}`;
57
+ }
58
+
59
+ const response = await fetch(url.toString(), { headers });
60
+ if (!response.ok) {
61
+ throw new Error(`Prometheus API error: ${response.status} ${response.statusText}`);
62
+ }
63
+
64
+ return response.json() as Promise<T>;
65
+ }
66
+
67
+ async query(query: string, time?: string): Promise<PrometheusQueryResult> {
68
+ const params: Record<string, string> = { query };
69
+ if (time) {
70
+ params.time = time;
71
+ }
72
+ return this.request<PrometheusQueryResult>("/api/v1/query", params);
73
+ }
74
+
75
+ async queryRange(
76
+ query: string,
77
+ start: string,
78
+ end: string,
79
+ step: string
80
+ ): Promise<PrometheusQueryResult> {
81
+ return this.request<PrometheusQueryResult>("/api/v1/query_range", {
82
+ query,
83
+ start,
84
+ end,
85
+ step,
86
+ });
87
+ }
88
+
89
+ async getAlerts(): Promise<{ alerts: PrometheusAlert[] }> {
90
+ const response = await this.request<{
91
+ status: string;
92
+ data: { alerts: PrometheusAlert[] };
93
+ }>("/api/v1/alerts");
94
+ return response.data;
95
+ }
96
+
97
+ async getTargets(): Promise<{ activeTargets: PrometheusTarget[] }> {
98
+ const response = await this.request<{
99
+ status: string;
100
+ data: { activeTargets: PrometheusTarget[] };
101
+ }>("/api/v1/targets");
102
+ return response.data;
103
+ }
104
+
105
+ async getRules(): Promise<{ groups: Array<{ name: string; rules: unknown[] }> }> {
106
+ const response = await this.request<{
107
+ status: string;
108
+ data: { groups: Array<{ name: string; rules: unknown[] }> };
109
+ }>("/api/v1/rules");
110
+ return response.data;
111
+ }
112
+
113
+ async getMetadata(metric?: string): Promise<Record<string, unknown[]>> {
114
+ const params: Record<string, string> = {};
115
+ if (metric) {
116
+ params.metric = metric;
117
+ }
118
+ const response = await this.request<{
119
+ status: string;
120
+ data: Record<string, unknown[]>;
121
+ }>("/api/v1/metadata", params);
122
+ return response.data;
123
+ }
124
+
125
+ formatQueryResult(result: PrometheusQueryResult): string {
126
+ if (result.status !== "success" || !result.data) {
127
+ return `Error: ${result.error || "Unknown error"}`;
128
+ }
129
+
130
+ const lines: string[] = [];
131
+ for (const item of result.data.result) {
132
+ const labels = Object.entries(item.metric)
133
+ .map(([k, v]) => `${k}="${v}"`)
134
+ .join(", ");
135
+
136
+ if (item.value) {
137
+ const [timestamp, value] = item.value;
138
+ lines.push(`{${labels}} => ${value} @${new Date(timestamp * 1000).toISOString()}`);
139
+ } else if (item.values) {
140
+ lines.push(`{${labels}}:`);
141
+ for (const [timestamp, value] of item.values.slice(-5)) {
142
+ lines.push(` ${new Date(timestamp * 1000).toISOString()}: ${value}`);
143
+ }
144
+ }
145
+ }
146
+
147
+ return lines.join("\n") || "No data";
148
+ }
149
+ }
150
+
151
+ // Singleton instance
152
+ let prometheusClient: PrometheusClient | null = null;
153
+
154
+ export function getPrometheusClient(): PrometheusClient | null {
155
+ return prometheusClient;
156
+ }
157
+
158
+ export function initPrometheusClient(config?: PrometheusConfig): PrometheusClient | null {
159
+ if (config) {
160
+ prometheusClient = new PrometheusClient(config);
161
+ }
162
+ return prometheusClient;
163
+ }
@@ -0,0 +1,265 @@
1
+ import type { SlackConfig } from "../../cli/config.js";
2
+
3
+ export interface SlackMessage {
4
+ channel?: string;
5
+ text: string;
6
+ blocks?: SlackBlock[];
7
+ attachments?: SlackAttachment[];
8
+ thread_ts?: string;
9
+ }
10
+
11
+ export interface SlackBlock {
12
+ type: "section" | "divider" | "header" | "context" | "actions";
13
+ text?: { type: "mrkdwn" | "plain_text"; text: string };
14
+ fields?: Array<{ type: "mrkdwn" | "plain_text"; text: string }>;
15
+ elements?: SlackBlockElement[];
16
+ accessory?: SlackBlockElement;
17
+ }
18
+
19
+ export interface SlackBlockElement {
20
+ type: "button" | "static_select" | "image" | "mrkdwn" | "plain_text";
21
+ text?: { type: "plain_text" | "mrkdwn"; text: string; emoji?: boolean };
22
+ action_id?: string;
23
+ url?: string;
24
+ value?: string;
25
+ style?: "primary" | "danger";
26
+ image_url?: string;
27
+ alt_text?: string;
28
+ }
29
+
30
+ export interface SlackAttachment {
31
+ color?: string;
32
+ fallback?: string;
33
+ title?: string;
34
+ text?: string;
35
+ fields?: Array<{ title: string; value: string; short?: boolean }>;
36
+ footer?: string;
37
+ ts?: number;
38
+ }
39
+
40
+ export interface SlackResponse {
41
+ ok: boolean;
42
+ error?: string;
43
+ ts?: string;
44
+ channel?: string;
45
+ }
46
+
47
+ export class SlackClient {
48
+ private webhookUrl: string;
49
+ private botToken?: string;
50
+ private defaultChannel?: string;
51
+
52
+ constructor(config: SlackConfig) {
53
+ this.webhookUrl = config.webhookUrl;
54
+ this.botToken = config.botToken;
55
+ this.defaultChannel = config.defaultChannel;
56
+ }
57
+
58
+ async sendWebhook(message: SlackMessage): Promise<boolean> {
59
+ try {
60
+ const response = await fetch(this.webhookUrl, {
61
+ method: "POST",
62
+ headers: { "Content-Type": "application/json" },
63
+ body: JSON.stringify(message),
64
+ });
65
+
66
+ return response.ok;
67
+ } catch {
68
+ return false;
69
+ }
70
+ }
71
+
72
+ async sendMessage(message: SlackMessage): Promise<SlackResponse> {
73
+ if (!this.botToken) {
74
+ // Fall back to webhook
75
+ const success = await this.sendWebhook(message);
76
+ return { ok: success };
77
+ }
78
+
79
+ const channel = message.channel || this.defaultChannel;
80
+ if (!channel) {
81
+ return { ok: false, error: "No channel specified" };
82
+ }
83
+
84
+ try {
85
+ const response = await fetch("https://slack.com/api/chat.postMessage", {
86
+ method: "POST",
87
+ headers: {
88
+ "Content-Type": "application/json",
89
+ "Authorization": `Bearer ${this.botToken}`,
90
+ },
91
+ body: JSON.stringify({
92
+ ...message,
93
+ channel,
94
+ }),
95
+ });
96
+
97
+ return await response.json() as SlackResponse;
98
+ } catch (error) {
99
+ return { ok: false, error: error instanceof Error ? error.message : String(error) };
100
+ }
101
+ }
102
+
103
+ async updateMessage(channel: string, ts: string, message: SlackMessage): Promise<SlackResponse> {
104
+ if (!this.botToken) {
105
+ return { ok: false, error: "Bot token required for message updates" };
106
+ }
107
+
108
+ try {
109
+ const response = await fetch("https://slack.com/api/chat.update", {
110
+ method: "POST",
111
+ headers: {
112
+ "Content-Type": "application/json",
113
+ "Authorization": `Bearer ${this.botToken}`,
114
+ },
115
+ body: JSON.stringify({
116
+ ...message,
117
+ channel,
118
+ ts,
119
+ }),
120
+ });
121
+
122
+ return await response.json() as SlackResponse;
123
+ } catch (error) {
124
+ return { ok: false, error: error instanceof Error ? error.message : String(error) };
125
+ }
126
+ }
127
+
128
+ buildInvestigationMessage(investigation: {
129
+ id: string;
130
+ title: string;
131
+ status: string;
132
+ summary?: string;
133
+ severity?: string;
134
+ rootCause?: string;
135
+ recommendations?: string[];
136
+ url?: string;
137
+ }): SlackMessage {
138
+ const statusEmoji = {
139
+ pending: "⏳",
140
+ running: "🔄",
141
+ completed: "✅",
142
+ failed: "❌",
143
+ }[investigation.status] || "❓";
144
+
145
+ const severityColor = {
146
+ critical: "#dc3545",
147
+ warning: "#ffc107",
148
+ info: "#17a2b8",
149
+ }[investigation.severity || "info"] || "#6c757d";
150
+
151
+ const blocks: SlackBlock[] = [
152
+ {
153
+ type: "header",
154
+ text: { type: "plain_text", text: `${statusEmoji} Investigation: ${investigation.title}` },
155
+ },
156
+ {
157
+ type: "section",
158
+ fields: [
159
+ { type: "mrkdwn", text: `*Status:*\n${investigation.status}` },
160
+ { type: "mrkdwn", text: `*ID:*\n\`${investigation.id}\`` },
161
+ ],
162
+ },
163
+ ];
164
+
165
+ if (investigation.summary) {
166
+ blocks.push({
167
+ type: "section",
168
+ text: { type: "mrkdwn", text: `*Summary:*\n${investigation.summary}` },
169
+ });
170
+ }
171
+
172
+ if (investigation.rootCause) {
173
+ blocks.push({
174
+ type: "section",
175
+ text: { type: "mrkdwn", text: `*Root Cause:*\n${investigation.rootCause}` },
176
+ });
177
+ }
178
+
179
+ if (investigation.recommendations && investigation.recommendations.length > 0) {
180
+ blocks.push({
181
+ type: "section",
182
+ text: {
183
+ type: "mrkdwn",
184
+ text: `*Recommendations:*\n${investigation.recommendations.map((r, i) => `${i + 1}. ${r}`).join("\n")}`,
185
+ },
186
+ });
187
+ }
188
+
189
+ if (investigation.url) {
190
+ blocks.push({
191
+ type: "actions",
192
+ elements: [
193
+ {
194
+ type: "button",
195
+ text: { type: "plain_text", text: "View Details", emoji: true },
196
+ url: investigation.url,
197
+ },
198
+ ],
199
+ });
200
+ }
201
+
202
+ return {
203
+ text: `Investigation ${investigation.status}: ${investigation.title}`,
204
+ blocks,
205
+ attachments: [
206
+ {
207
+ color: severityColor,
208
+ fallback: investigation.summary || investigation.title,
209
+ },
210
+ ],
211
+ };
212
+ }
213
+
214
+ buildAlertMessage(alert: {
215
+ name: string;
216
+ severity: string;
217
+ message: string;
218
+ source?: string;
219
+ timestamp?: Date;
220
+ }): SlackMessage {
221
+ const severityEmoji = {
222
+ critical: "🔴",
223
+ warning: "🟡",
224
+ info: "🔵",
225
+ }[alert.severity] || "⚪";
226
+
227
+ const severityColor = {
228
+ critical: "#dc3545",
229
+ warning: "#ffc107",
230
+ info: "#17a2b8",
231
+ }[alert.severity] || "#6c757d";
232
+
233
+ return {
234
+ text: `${severityEmoji} Alert: ${alert.name}`,
235
+ attachments: [
236
+ {
237
+ color: severityColor,
238
+ fallback: alert.message,
239
+ title: `${severityEmoji} ${alert.name}`,
240
+ text: alert.message,
241
+ fields: [
242
+ { title: "Severity", value: alert.severity.toUpperCase(), short: true },
243
+ { title: "Source", value: alert.source || "Unknown", short: true },
244
+ ],
245
+ footer: "Triagent",
246
+ ts: Math.floor((alert.timestamp || new Date()).getTime() / 1000),
247
+ },
248
+ ],
249
+ };
250
+ }
251
+ }
252
+
253
+ // Singleton instance
254
+ let slackClient: SlackClient | null = null;
255
+
256
+ export function getSlackClient(): SlackClient | null {
257
+ return slackClient;
258
+ }
259
+
260
+ export function initSlackClient(config?: SlackConfig): SlackClient | null {
261
+ if (config) {
262
+ slackClient = new SlackClient(config);
263
+ }
264
+ return slackClient;
265
+ }
@@ -0,0 +1,199 @@
1
+ import type { TeamsConfig } from "../../cli/config.js";
2
+
3
+ export interface TeamsMessage {
4
+ "@type": "MessageCard";
5
+ "@context": "http://schema.org/extensions";
6
+ themeColor?: string;
7
+ summary: string;
8
+ sections?: TeamsSection[];
9
+ potentialAction?: TeamsAction[];
10
+ }
11
+
12
+ export interface TeamsSection {
13
+ activityTitle?: string;
14
+ activitySubtitle?: string;
15
+ activityImage?: string;
16
+ facts?: Array<{ name: string; value: string }>;
17
+ markdown?: boolean;
18
+ text?: string;
19
+ }
20
+
21
+ export interface TeamsAction {
22
+ "@type": "OpenUri" | "ActionCard" | "HttpPOST";
23
+ name: string;
24
+ targets?: Array<{ os: string; uri: string }>;
25
+ inputs?: TeamsInput[];
26
+ actions?: TeamsAction[];
27
+ target?: string;
28
+ body?: string;
29
+ }
30
+
31
+ export interface TeamsInput {
32
+ "@type": "TextInput" | "MultichoiceInput";
33
+ id: string;
34
+ title: string;
35
+ isMultiline?: boolean;
36
+ isRequired?: boolean;
37
+ choices?: Array<{ display: string; value: string }>;
38
+ }
39
+
40
+ export class TeamsClient {
41
+ private webhookUrl: string;
42
+
43
+ constructor(config: TeamsConfig) {
44
+ this.webhookUrl = config.webhookUrl;
45
+ }
46
+
47
+ async sendMessage(message: TeamsMessage): Promise<boolean> {
48
+ try {
49
+ const response = await fetch(this.webhookUrl, {
50
+ method: "POST",
51
+ headers: { "Content-Type": "application/json" },
52
+ body: JSON.stringify(message),
53
+ });
54
+
55
+ return response.ok;
56
+ } catch {
57
+ return false;
58
+ }
59
+ }
60
+
61
+ buildInvestigationMessage(investigation: {
62
+ id: string;
63
+ title: string;
64
+ status: string;
65
+ summary?: string;
66
+ severity?: string;
67
+ rootCause?: string;
68
+ recommendations?: string[];
69
+ url?: string;
70
+ }): TeamsMessage {
71
+ const statusEmoji = {
72
+ pending: "⏳",
73
+ running: "🔄",
74
+ completed: "✅",
75
+ failed: "❌",
76
+ }[investigation.status] || "❓";
77
+
78
+ const themeColor = {
79
+ critical: "dc3545",
80
+ warning: "ffc107",
81
+ info: "17a2b8",
82
+ completed: "28a745",
83
+ failed: "dc3545",
84
+ }[investigation.severity || investigation.status] || "6c757d";
85
+
86
+ const facts: Array<{ name: string; value: string }> = [
87
+ { name: "Status", value: `${statusEmoji} ${investigation.status}` },
88
+ { name: "ID", value: investigation.id },
89
+ ];
90
+
91
+ if (investigation.severity) {
92
+ facts.push({ name: "Severity", value: investigation.severity.toUpperCase() });
93
+ }
94
+
95
+ const sections: TeamsSection[] = [
96
+ {
97
+ activityTitle: `Investigation: ${investigation.title}`,
98
+ activitySubtitle: `Status: ${investigation.status}`,
99
+ facts,
100
+ markdown: true,
101
+ },
102
+ ];
103
+
104
+ if (investigation.summary) {
105
+ sections.push({
106
+ text: `**Summary:** ${investigation.summary}`,
107
+ markdown: true,
108
+ });
109
+ }
110
+
111
+ if (investigation.rootCause) {
112
+ sections.push({
113
+ text: `**Root Cause:** ${investigation.rootCause}`,
114
+ markdown: true,
115
+ });
116
+ }
117
+
118
+ if (investigation.recommendations && investigation.recommendations.length > 0) {
119
+ const recText = investigation.recommendations
120
+ .map((r, i) => `${i + 1}. ${r}`)
121
+ .join("\n\n");
122
+ sections.push({
123
+ text: `**Recommendations:**\n\n${recText}`,
124
+ markdown: true,
125
+ });
126
+ }
127
+
128
+ const actions: TeamsAction[] = [];
129
+ if (investigation.url) {
130
+ actions.push({
131
+ "@type": "OpenUri",
132
+ name: "View Details",
133
+ targets: [{ os: "default", uri: investigation.url }],
134
+ });
135
+ }
136
+
137
+ return {
138
+ "@type": "MessageCard",
139
+ "@context": "http://schema.org/extensions",
140
+ themeColor,
141
+ summary: `Investigation ${investigation.status}: ${investigation.title}`,
142
+ sections,
143
+ potentialAction: actions.length > 0 ? actions : undefined,
144
+ };
145
+ }
146
+
147
+ buildAlertMessage(alert: {
148
+ name: string;
149
+ severity: string;
150
+ message: string;
151
+ source?: string;
152
+ timestamp?: Date;
153
+ }): TeamsMessage {
154
+ const severityEmoji = {
155
+ critical: "🔴",
156
+ warning: "🟡",
157
+ info: "🔵",
158
+ }[alert.severity] || "⚪";
159
+
160
+ const themeColor = {
161
+ critical: "dc3545",
162
+ warning: "ffc107",
163
+ info: "17a2b8",
164
+ }[alert.severity] || "6c757d";
165
+
166
+ return {
167
+ "@type": "MessageCard",
168
+ "@context": "http://schema.org/extensions",
169
+ themeColor,
170
+ summary: `${severityEmoji} Alert: ${alert.name}`,
171
+ sections: [
172
+ {
173
+ activityTitle: `${severityEmoji} ${alert.name}`,
174
+ activitySubtitle: alert.source || "Triagent",
175
+ facts: [
176
+ { name: "Severity", value: alert.severity.toUpperCase() },
177
+ { name: "Time", value: (alert.timestamp || new Date()).toISOString() },
178
+ ],
179
+ text: alert.message,
180
+ markdown: true,
181
+ },
182
+ ],
183
+ };
184
+ }
185
+ }
186
+
187
+ // Singleton instance
188
+ let teamsClient: TeamsClient | null = null;
189
+
190
+ export function getTeamsClient(): TeamsClient | null {
191
+ return teamsClient;
192
+ }
193
+
194
+ export function initTeamsClient(config?: TeamsConfig): TeamsClient | null {
195
+ if (config) {
196
+ teamsClient = new TeamsClient(config);
197
+ }
198
+ return teamsClient;
199
+ }