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
package/src/tui/app.tsx CHANGED
@@ -1,19 +1,21 @@
1
1
  /* @jsxImportSource @opentui/solid */
2
2
  import { render } from "@opentui/solid";
3
- import { createSignal, For, Show, onMount } from "solid-js";
4
- import { createTextAttributes } from "@opentui/core";
5
- import "opentui-spinner/solid";
3
+ import { createSignal, onMount, type JSX } from "solid-js";
4
+ import { exec } from "child_process";
5
+ import { promisify } from "util";
6
6
  import { getDebuggerAgent, buildIncidentPrompt } from "../mastra/index.js";
7
7
  import type { IncidentInput } from "../mastra/agents/debugger.js";
8
-
9
- interface Message {
10
- id: string;
11
- role: "user" | "assistant" | "tool";
12
- content: string;
13
- timestamp: Date;
14
- toolName?: string;
15
- command?: string;
16
- }
8
+ import { approvalStore } from "../mastra/tools/approval-store.js";
9
+ import { ToastProvider, toastSuccess, toastError, toastWarning, toastInfo } from "./components/toast.js";
10
+ import { ApprovalDialogProvider, useApprovalDialog } from "./components/approval-dialog.js";
11
+ import { Header } from "./components/header.js";
12
+ import { MessagesPanel, type Message } from "./components/messages-panel.js";
13
+ import { Editor } from "./components/editor.js";
14
+ import { StatusBar } from "./components/status-bar.js";
15
+ import { type AppStatus, type RiskLevel } from "./theme/index.js";
16
+ import { loadConfig } from "../config.js";
17
+
18
+ const execAsync = promisify(exec);
17
19
 
18
20
  // Conversation history for multi-turn debugging
19
21
  interface ConversationMessage {
@@ -33,11 +35,6 @@ function formatHistoryAsPrompt(history: ConversationMessage[], newMessage: strin
33
35
  return `Previous conversation:\n${historyText}\n\nUser: ${newMessage}`;
34
36
  }
35
37
 
36
- type AppStatus = "idle" | "investigating" | "complete" | "error";
37
-
38
- const ATTR_DIM = createTextAttributes({ dim: true });
39
- const ATTR_BOLD = createTextAttributes({ bold: true });
40
-
41
38
  function buildDisplayCommand(toolName: string, args: unknown): string | undefined {
42
39
  if (!args || typeof args !== "object") return undefined;
43
40
 
@@ -45,11 +42,9 @@ function buildDisplayCommand(toolName: string, args: unknown): string | undefine
45
42
 
46
43
  switch (toolName) {
47
44
  case "cli":
48
- // CLI tool has direct command
49
45
  return "command" in a ? String(a.command) : undefined;
50
46
 
51
47
  case "git": {
52
- // Build git command: git <command> [args...] [path]
53
48
  if (!("command" in a)) return undefined;
54
49
  const parts = ["git", String(a.command)];
55
50
  if ("args" in a && Array.isArray(a.args)) {
@@ -62,7 +57,6 @@ function buildDisplayCommand(toolName: string, args: unknown): string | undefine
62
57
  }
63
58
 
64
59
  case "filesystem": {
65
- // Build filesystem display: <operation> <path> [pattern]
66
60
  if (!("operation" in a)) return undefined;
67
61
  const op = String(a.operation);
68
62
  const path = "path" in a ? String(a.path) : "";
@@ -79,18 +73,51 @@ function buildDisplayCommand(toolName: string, args: unknown): string | undefine
79
73
  }
80
74
 
81
75
  default:
82
- // Fallback: try to use command if it exists
83
76
  return "command" in a ? String(a.command) : undefined;
84
77
  }
85
78
  }
86
79
 
87
- function App() {
80
+ // Pending approval state for HITL
81
+ interface PendingApprovalState {
82
+ approvalId: string;
83
+ command: string;
84
+ riskLevel: RiskLevel;
85
+ selectedOption: number;
86
+ }
87
+
88
+ function AppContent() {
88
89
  const [messages, setMessages] = createSignal<Message[]>([]);
89
90
  const [conversationHistory, setConversationHistory] = createSignal<ConversationMessage[]>([]);
90
91
  const [status, setStatus] = createSignal<AppStatus>("idle");
91
92
  const [currentTool, setCurrentTool] = createSignal<string | null>(null);
92
93
  const [inputValue, setInputValue] = createSignal("");
93
- const [error, setError] = createSignal<string | null>(null);
94
+ const [kubeContext, setKubeContext] = createSignal<string>("loading...");
95
+ const [modelName, setModelName] = createSignal<string>("loading...");
96
+
97
+ // HITL approval state
98
+ const [pendingApproval, setPendingApproval] = createSignal<PendingApprovalState | null>(null);
99
+
100
+ // Approval dialog hook
101
+ const { showApproval } = useApprovalDialog();
102
+
103
+ // Fetch kubectl context and config on mount
104
+ onMount(async () => {
105
+ // Load kubectl context
106
+ try {
107
+ const { stdout } = await execAsync("kubectl config current-context");
108
+ setKubeContext(stdout.trim());
109
+ } catch {
110
+ setKubeContext("not connected");
111
+ }
112
+
113
+ // Load model name from config
114
+ try {
115
+ const config = await loadConfig();
116
+ setModelName(config.aiModel);
117
+ } catch {
118
+ setModelName("unknown");
119
+ }
120
+ });
94
121
 
95
122
  const addMessage = (msg: Omit<Message, "id" | "timestamp">) => {
96
123
  setMessages((prev) => [
@@ -103,27 +130,160 @@ function App() {
103
130
  ]);
104
131
  };
105
132
 
133
+ const handleApprovalDecision = async (approved: boolean) => {
134
+ const approval = pendingApproval();
135
+ if (!approval) return;
136
+
137
+ if (approved) {
138
+ const token = approvalStore.approve(approval.approvalId);
139
+ if (!token) {
140
+ addMessage({
141
+ role: "assistant",
142
+ content: "Approval expired. Please try the operation again.",
143
+ });
144
+ setPendingApproval(null);
145
+ setStatus("complete");
146
+ toastWarning("Approval expired", "Please retry the operation");
147
+ return;
148
+ }
149
+
150
+ addMessage({
151
+ role: "user",
152
+ content: `Approved: ${approval.command}`,
153
+ });
154
+ toastSuccess("Command approved");
155
+
156
+ setPendingApproval(null);
157
+ const approvalMessage = `User approved the command. The approval token is: ${token}. Please execute the command: ${approval.command} with approvalToken: "${token}"`;
158
+
159
+ setConversationHistory((prev) => [
160
+ ...prev,
161
+ { role: "user", content: approvalMessage },
162
+ ]);
163
+
164
+ setStatus("investigating");
165
+ continueWithApproval(approvalMessage);
166
+ } else {
167
+ approvalStore.reject(approval.approvalId);
168
+ addMessage({
169
+ role: "user",
170
+ content: `Rejected: ${approval.command}`,
171
+ });
172
+ addMessage({
173
+ role: "assistant",
174
+ content: "Command rejected by user. How would you like to proceed?",
175
+ });
176
+ setPendingApproval(null);
177
+ setStatus("complete");
178
+ toastWarning("Command rejected");
179
+ }
180
+ };
181
+
182
+ // Handle approval using dialog overlay
183
+ const handleApprovalRequest = async (approvalData: PendingApprovalState) => {
184
+ setPendingApproval(approvalData);
185
+ setStatus("awaiting_approval");
186
+
187
+ const approved = await showApproval({
188
+ command: approvalData.command,
189
+ riskLevel: approvalData.riskLevel,
190
+ description: `This ${approvalData.riskLevel}-risk operation requires your approval.`,
191
+ });
192
+
193
+ await handleApprovalDecision(approved);
194
+ };
195
+
196
+ const continueWithApproval = async (message: string) => {
197
+ try {
198
+ const agent = getDebuggerAgent();
199
+ let assistantContent = "";
200
+
201
+ const prompt = formatHistoryAsPrompt(conversationHistory(), message);
202
+
203
+ const stream = await agent.stream(prompt, {
204
+ maxSteps: 20,
205
+ onStepFinish: ({ toolCalls, toolResults }) => {
206
+ if (toolCalls && toolCalls.length > 0) {
207
+ const toolCallChunk = toolCalls[0] as { payload?: { toolName?: string; args?: unknown } };
208
+ const toolName = toolCallChunk?.payload?.toolName ?? "tool";
209
+ const args = toolCallChunk?.payload?.args ?? {};
210
+
211
+ const command = buildDisplayCommand(toolName, args);
212
+
213
+ addMessage({
214
+ role: "tool",
215
+ content: command ? `$ ${command}` : `Executing ${toolName}...`,
216
+ toolName,
217
+ command,
218
+ });
219
+ // Note: Tool is already complete when onStepFinish fires
220
+ }
221
+
222
+ if (toolResults && toolResults.length > 0) {
223
+ for (const toolResult of toolResults) {
224
+ const tr = toolResult as any;
225
+ const data = tr?.payload?.result ?? tr?.result ?? tr;
226
+
227
+ if (data?.requiresApproval && data?.approvalId) {
228
+ handleApprovalRequest({
229
+ approvalId: data.approvalId,
230
+ command: data.command || "unknown command",
231
+ riskLevel: (data.riskLevel as RiskLevel) || "medium",
232
+ selectedOption: 0,
233
+ });
234
+ }
235
+ }
236
+ }
237
+ },
238
+ });
239
+
240
+ for await (const chunk of stream.textStream) {
241
+ assistantContent += chunk;
242
+ }
243
+
244
+ if (status() !== "awaiting_approval") {
245
+ addMessage({
246
+ role: "assistant",
247
+ content: assistantContent,
248
+ });
249
+
250
+ setConversationHistory((prev) => [
251
+ ...prev,
252
+ { role: "assistant", content: assistantContent },
253
+ ]);
254
+
255
+ setStatus("complete");
256
+ toastSuccess("Command executed");
257
+ }
258
+ setCurrentTool(null);
259
+ } catch (err) {
260
+ const errorMsg = err instanceof Error ? err.message : String(err);
261
+ setStatus("error");
262
+ toastError("Execution failed", errorMsg);
263
+ addMessage({
264
+ role: "assistant",
265
+ content: `Error: ${errorMsg}`,
266
+ });
267
+ }
268
+ };
269
+
106
270
  const investigate = async (incident: IncidentInput) => {
107
271
  setStatus("investigating");
108
- setError(null);
109
272
  setCurrentTool(null);
273
+ toastInfo("Investigation started", incident.title);
110
274
 
111
- // Add user message to UI
112
275
  addMessage({
113
276
  role: "user",
114
277
  content: incident.description,
115
278
  });
116
279
 
117
- // Build prompt: use full incident prompt for first message, include history for follow-ups
118
280
  const isFirstMessage = conversationHistory().length === 0;
119
281
  const userContent = isFirstMessage
120
282
  ? buildIncidentPrompt(incident)
121
283
  : incident.description;
122
284
 
123
- // Format prompt with conversation history
124
285
  const prompt = formatHistoryAsPrompt(conversationHistory(), userContent);
125
286
 
126
- // Add user message to conversation history
127
287
  setConversationHistory((prev) => [
128
288
  ...prev,
129
289
  { role: "user", content: userContent },
@@ -131,29 +291,43 @@ function App() {
131
291
 
132
292
  try {
133
293
  const agent = getDebuggerAgent();
134
-
135
294
  let assistantContent = "";
136
295
 
137
- // Send the formatted prompt to the agent
138
296
  const stream = await agent.stream(prompt, {
139
297
  maxSteps: 20,
140
- onStepFinish: ({ toolCalls }) => {
298
+ onStepFinish: (stepResult) => {
299
+ const { toolCalls, toolResults } = stepResult;
300
+
141
301
  if (toolCalls && toolCalls.length > 0) {
142
- const toolCall = toolCalls[0] as { payload?: { toolName?: string; args?: unknown } };
143
- const payload = toolCall.payload;
144
- const toolName = payload?.toolName ?? "tool";
145
- const args = payload?.args ?? {};
302
+ const toolCallChunk = toolCalls[0] as { payload?: { toolName?: string; args?: unknown } };
303
+ const toolName = toolCallChunk?.payload?.toolName ?? "tool";
304
+ const args = toolCallChunk?.payload?.args ?? {};
146
305
 
147
- // Build display command based on tool type
148
306
  const command = buildDisplayCommand(toolName, args);
149
307
 
150
- setCurrentTool(toolName);
151
308
  addMessage({
152
309
  role: "tool",
153
310
  content: command ? `$ ${command}` : `Executing ${toolName}...`,
154
311
  toolName,
155
312
  command,
156
313
  });
314
+ // Note: Tool is already complete when onStepFinish fires
315
+ }
316
+
317
+ if (toolResults && toolResults.length > 0) {
318
+ for (const toolResult of toolResults) {
319
+ const tr = toolResult as any;
320
+ const data = tr?.payload?.result ?? tr?.result ?? tr;
321
+
322
+ if (data?.requiresApproval && data?.approvalId) {
323
+ handleApprovalRequest({
324
+ approvalId: data.approvalId,
325
+ command: data.command || "unknown command",
326
+ riskLevel: (data.riskLevel as RiskLevel) || "medium",
327
+ selectedOption: 0,
328
+ });
329
+ }
330
+ }
157
331
  }
158
332
  },
159
333
  });
@@ -162,24 +336,25 @@ function App() {
162
336
  assistantContent += chunk;
163
337
  }
164
338
 
165
- // Add assistant response to UI
166
- addMessage({
167
- role: "assistant",
168
- content: assistantContent,
169
- });
339
+ if (status() !== "awaiting_approval") {
340
+ addMessage({
341
+ role: "assistant",
342
+ content: assistantContent,
343
+ });
170
344
 
171
- // Add assistant response to conversation history
172
- setConversationHistory((prev) => [
173
- ...prev,
174
- { role: "assistant", content: assistantContent },
175
- ]);
345
+ setConversationHistory((prev) => [
346
+ ...prev,
347
+ { role: "assistant", content: assistantContent },
348
+ ]);
176
349
 
177
- setStatus("complete");
350
+ setStatus("complete");
351
+ toastSuccess("Investigation complete");
352
+ }
178
353
  setCurrentTool(null);
179
354
  } catch (err) {
180
355
  const errorMsg = err instanceof Error ? err.message : String(err);
181
- setError(errorMsg);
182
356
  setStatus("error");
357
+ toastError("Investigation failed", errorMsg);
183
358
  addMessage({
184
359
  role: "assistant",
185
360
  content: `Error: ${errorMsg}`,
@@ -203,165 +378,70 @@ function App() {
203
378
  setInputValue(value);
204
379
  };
205
380
 
206
- const getStatusColor = (): string => {
207
- switch (status()) {
208
- case "investigating":
209
- return "yellow";
210
- case "complete":
211
- return "green";
212
- case "error":
213
- return "red";
214
- default:
215
- return "gray";
381
+ const handleApprovalInput = (value: string) => {
382
+ if (value.toLowerCase() === "y") {
383
+ handleApprovalDecision(true);
384
+ } else if (value.toLowerCase() === "n") {
385
+ handleApprovalDecision(false);
216
386
  }
217
387
  };
218
388
 
219
- const getStatusText = (): string => {
220
- switch (status()) {
221
- case "investigating":
222
- return currentTool() ? `Running: ${currentTool()}` : "Investigating...";
223
- case "complete":
224
- return "Complete";
225
- case "error":
226
- return "Error";
227
- default:
228
- return "Ready";
389
+ const handleApprovalKeyDown = (key: { name: string }) => {
390
+ const approval = pendingApproval();
391
+ if (!approval) return;
392
+
393
+ if (key.name === "up" || key.name === "down") {
394
+ setPendingApproval({ ...approval, selectedOption: approval.selectedOption === 0 ? 1 : 0 });
395
+ } else if (key.name === "return") {
396
+ handleApprovalDecision(approval.selectedOption === 0);
229
397
  }
230
398
  };
231
399
 
232
400
  return (
233
401
  <box flexDirection="column" width="100%" height="100%">
234
402
  {/* Header */}
235
- <box
236
- borderStyle="single"
237
- borderColor="red"
238
- paddingLeft={2}
239
- paddingRight={2}
240
- flexDirection="row"
241
- justifyContent="space-between"
242
- >
243
- <text fg="red" attributes={ATTR_BOLD}>
244
- TRIAGENT
245
- </text>
246
- <text fg="gray">Kubernetes Debugging Agent</text>
247
- <text fg={getStatusColor()} attributes={ATTR_BOLD}>
248
- [{getStatusText()}]
249
- </text>
250
- </box>
251
-
252
- {/* Messages Area */}
253
- <scrollbox
254
- flexGrow={1}
255
- borderStyle="single"
256
- borderColor="gray"
257
- paddingLeft={1}
258
- paddingRight={1}
259
- stickyScroll
260
- stickyStart="bottom"
261
- >
262
- <box flexDirection="column" gap={1}>
263
- <Show
264
- when={messages().length > 0}
265
- fallback={
266
- <box paddingTop={2} paddingBottom={2}>
267
- <text fg="gray" attributes={ATTR_DIM}>
268
- Enter an incident description to start investigating...
269
- </text>
270
- </box>
271
- }
272
- >
273
- <For each={messages()}>
274
- {(msg) => (
275
- <box flexDirection="column" marginBottom={1}>
276
- <Show when={msg.role === "user"}>
277
- <box flexDirection="row" gap={1}>
278
- <text fg="cyan" attributes={ATTR_BOLD}>
279
- You:
280
- </text>
281
- <text fg="white">{msg.content}</text>
282
- </box>
283
- </Show>
284
- <Show when={msg.role === "tool"}>
285
- <box flexDirection="row" gap={1} alignItems="center">
286
- <Show
287
- when={status() === "investigating" && msg.id === messages().filter(m => m.role === "tool").at(-1)?.id}
288
- fallback={<text fg="green">✓</text>}
289
- >
290
- <spinner name="dots" color="blue" />
291
- </Show>
292
- <text fg="blue" attributes={ATTR_DIM}>
293
- [{msg.toolName}]
294
- </text>
295
- <text fg="gray" attributes={ATTR_DIM}>
296
- {msg.content}
297
- </text>
298
- </box>
299
- </Show>
300
- <Show when={msg.role === "assistant"}>
301
- <box flexDirection="column">
302
- <text fg="green" attributes={ATTR_BOLD}>
303
- Triagent:
304
- </text>
305
- <text fg="white" wrapMode="word">
306
- {msg.content}
307
- </text>
308
- </box>
309
- </Show>
310
- </box>
311
- )}
312
- </For>
313
- </Show>
314
- </box>
315
- </scrollbox>
316
-
317
- {/* Input Area */}
318
- <box
319
- borderStyle="single"
320
- borderColor={status() === "investigating" ? "yellow" : "cyan"}
321
- paddingLeft={1}
322
- paddingRight={1}
323
- flexDirection="row"
324
- gap={1}
325
- >
326
- <text fg="cyan" attributes={ATTR_BOLD}>
327
- {">"}
328
- </text>
329
- <input
330
- flexGrow={1}
331
- focused={true}
332
- value={inputValue()}
333
- onInput={handleInput}
334
- onSubmit={handleSubmit}
335
- placeholder={
336
- status() === "investigating"
337
- ? "Investigating..."
338
- : "Describe the incident..."
339
- }
340
- textColor="white"
341
- placeholderColor="gray"
342
- focusedTextColor="white"
343
- focusedBackgroundColor="#1a1a1a"
344
- />
345
- </box>
346
-
347
- {/* Footer */}
348
- <box
349
- paddingLeft={2}
350
- paddingRight={2}
351
- flexDirection="row"
352
- justifyContent="space-between"
353
- >
354
- <text fg="gray" attributes={ATTR_DIM}>
355
- Press Enter to submit | Ctrl+C to quit
356
- </text>
357
- <text fg="gray" attributes={ATTR_DIM}>
358
- {messages().length} messages
359
- </text>
360
- </box>
403
+ <Header
404
+ status={status()}
405
+ currentTool={currentTool()}
406
+ kubeContext={kubeContext()}
407
+ modelName={modelName()}
408
+ />
409
+
410
+ {/* Messages Panel */}
411
+ <MessagesPanel
412
+ messages={messages()}
413
+ status={status()}
414
+ />
415
+
416
+ {/* Editor */}
417
+ <Editor
418
+ status={status()}
419
+ value={inputValue()}
420
+ onInput={handleInput}
421
+ onSubmit={handleSubmit}
422
+ onApprovalInput={handleApprovalInput}
423
+ onApprovalKeyDown={handleApprovalKeyDown}
424
+ />
425
+
426
+ {/* Status Bar */}
427
+ <StatusBar
428
+ status={status()}
429
+ messageCount={messages().length}
430
+ />
361
431
  </box>
362
432
  );
363
433
  }
364
434
 
435
+ function App() {
436
+ return (
437
+ <ToastProvider>
438
+ <ApprovalDialogProvider>
439
+ <AppContent />
440
+ </ApprovalDialogProvider>
441
+ </ToastProvider>
442
+ );
443
+ }
444
+
365
445
  export interface TUIHandle {
366
446
  shutdown: () => void;
367
447
  handleWebhookIncident: (incident: IncidentInput) => Promise<string>;
@@ -372,13 +452,13 @@ export async function runTUI(): Promise<TUIHandle> {
372
452
 
373
453
  return {
374
454
  shutdown: () => {
375
- // The render function handles cleanup
376
455
  process.exit(0);
377
456
  },
378
457
  handleWebhookIncident: async (incident: IncidentInput) => {
379
- // For webhook mode, we'd need to integrate differently
380
- // This is a placeholder for now
381
458
  return "Webhook incident handling not yet implemented in TUI mode";
382
459
  },
383
460
  };
384
461
  }
462
+
463
+ // Export Message type for use in other components
464
+ export type { Message } from "./components/messages-panel.js";