triagent 0.1.0-alpha1 → 0.1.0-alpha12

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 CHANGED
@@ -5,7 +5,17 @@ AI-powered Kubernetes debugging agent with terminal UI.
5
5
  ## Installation
6
6
 
7
7
  ```bash
8
- bun install triagent
8
+ # bun
9
+ bun install -g triagent
10
+
11
+ # npm
12
+ npm install -g triagent
13
+
14
+ # yarn
15
+ yarn global add triagent
16
+
17
+ # pnpm
18
+ pnpm add -g triagent
9
19
  ```
10
20
 
11
21
  ## Usage
@@ -20,14 +30,99 @@ triagent --webhook-only
20
30
 
21
31
  ## Configuration
22
32
 
23
- Set the following environment variables:
33
+ Configuration can be set via CLI commands or environment variables. CLI config takes precedence over environment variables.
34
+
35
+ ### CLI Config
36
+
37
+ ```bash
38
+ # Set configuration values
39
+ triagent config set <key> <value>
40
+
41
+ # Get a configuration value
42
+ triagent config get <key>
43
+
44
+ # List all configuration values
45
+ triagent config list
46
+
47
+ # Show config file path
48
+ triagent config path
49
+ ```
50
+
51
+ ### Config Keys
52
+
53
+ | Key | Description | Default |
54
+ |-----|-------------|---------|
55
+ | `aiProvider` | AI provider (`openai`, `anthropic`, `google`) | `anthropic` |
56
+ | `aiModel` | Model ID (e.g., `gpt-4o`, `claude-sonnet-4-20250514`) | Provider default |
57
+ | `apiKey` | API key for the provider | - |
58
+ | `baseUrl` | Custom API base URL (for proxies or local models) | - |
59
+ | `webhookPort` | Webhook server port | `3000` |
60
+ | `codebasePath` | Path to single codebase (legacy) | `./` |
61
+ | `kubeConfigPath` | Kubernetes config path | `~/.kube` |
62
+
63
+ ### Multiple Codebases
64
+
65
+ For applications spanning multiple repositories, configure `codebasePaths` in `~/.config/triagent/config.json`:
66
+
67
+ ```json
68
+ {
69
+ "codebasePaths": [
70
+ { "name": "frontend", "path": "/path/to/frontend-repo" },
71
+ { "name": "backend", "path": "/path/to/backend-repo" },
72
+ { "name": "infra", "path": "/path/to/infrastructure" }
73
+ ]
74
+ }
75
+ ```
76
+
77
+ Each codebase is mounted at `/workspace/<name>` in the sandbox. The model can access any codebase as needed during investigation.
78
+
79
+ ### Custom Instructions (TRIAGENT.md)
80
+
81
+ Create `~/.config/triagent/TRIAGENT.md` to provide custom instructions to the model. These instructions are prepended to the default system prompt.
82
+
83
+ Example `TRIAGENT.md`:
84
+
85
+ ```markdown
86
+ ## Project Context
87
+
88
+ This is a microservices e-commerce platform with the following services:
89
+ - frontend: Next.js app in /workspace/frontend
90
+ - api: Go backend in /workspace/backend
91
+ - infra: Terraform configs in /workspace/infra
92
+
93
+ ## Investigation Priorities
94
+
95
+ 1. Always check the api service logs first for 5xx errors
96
+ 2. The frontend service talks to api via internal DNS: api.default.svc.cluster.local
97
+ 3. Common issues: Redis connection timeouts, PostgreSQL connection pool exhaustion
98
+ ```
99
+
100
+ ### Environment Variables
101
+
102
+ | Variable | Description |
103
+ |----------|-------------|
104
+ | `AI_PROVIDER` | AI provider (`openai`, `anthropic`, `google`) |
105
+ | `AI_MODEL` | Model ID |
106
+ | `AI_BASE_URL` | Custom API base URL |
107
+ | `OPENAI_API_KEY` | OpenAI API key |
108
+ | `ANTHROPIC_API_KEY` | Anthropic API key |
109
+ | `GOOGLE_GENERATIVE_AI_API_KEY` | Google AI API key |
110
+ | `WEBHOOK_PORT` | Webhook server port |
111
+ | `CODEBASE_PATH` | Path to codebase |
112
+ | `KUBE_CONFIG_PATH` | Kubernetes config path |
113
+
114
+ ### Examples
24
115
 
25
116
  ```bash
26
- ANTHROPIC_API_KEY=your-api-key
27
- # or
28
- OPENAI_API_KEY=your-api-key
29
- # or
30
- GOOGLE_GENERATIVE_AI_API_KEY=your-api-key
117
+ # Configure with Anthropic (default)
118
+ triagent config set apiKey sk-ant-...
119
+
120
+ # Configure with OpenAI
121
+ triagent config set aiProvider openai
122
+ triagent config set apiKey sk-proj-...
123
+
124
+ # Use a custom API endpoint (e.g., proxy or local model)
125
+ triagent config set baseUrl https://your-proxy.example.com/v1
31
126
  ```
32
127
 
33
128
  ## Development
package/bunfig.toml CHANGED
@@ -1 +1,5 @@
1
1
  preload = ["@opentui/solid/preload"]
2
+
3
+ [jsx]
4
+ runtime = "automatic"
5
+ importSource = "@opentui/solid"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "triagent",
3
- "version": "0.1.0-alpha1",
3
+ "version": "0.1.0-alpha12",
4
4
  "description": "AI-powered Kubernetes debugging agent with terminal UI",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -27,7 +27,11 @@
27
27
  ],
28
28
  "repository": {
29
29
  "type": "git",
30
- "url": "git+https://github.com/OWNER/triagent.git"
30
+ "url": "git+https://github.com/ServiceWeave/triagent.git"
31
+ },
32
+ "homepage": "https://github.com/ServiceWeave/triagent#readme",
33
+ "bugs": {
34
+ "url": "https://github.com/ServiceWeave/triagent/issues"
31
35
  },
32
36
  "license": "MIT",
33
37
  "scripts": {
@@ -46,12 +50,15 @@
46
50
  "@opentui/core": "^0.1.72",
47
51
  "@opentui/solid": "^0.1.72",
48
52
  "hono": "^4.6.0",
49
- "solid-js": "^1.9.10",
53
+ "opentui-spinner": "^0.0.6",
54
+ "solid-js": "1.9.9",
55
+ "triagent": "^0.1.0-alpha1",
50
56
  "zod": "^3.24.0"
51
57
  },
52
58
  "devDependencies": {
53
- "typescript": "^5.7.0",
59
+ "@types/babel__core": "^7.20.5",
54
60
  "@types/node": "^22.0.0",
55
- "bun-types": "^1.2.0"
61
+ "bun-types": "^1.2.0",
62
+ "typescript": "^5.7.0"
56
63
  }
57
64
  }
package/src/cli/config.ts CHANGED
@@ -3,22 +3,43 @@ import { homedir } from "os";
3
3
  import { join } from "path";
4
4
  import type { AIProvider } from "../config.js";
5
5
 
6
+ export interface CodebaseEntry {
7
+ name: string;
8
+ path: string;
9
+ }
10
+
6
11
  export interface StoredConfig {
7
12
  aiProvider?: AIProvider;
8
13
  aiModel?: string;
9
14
  apiKey?: string;
15
+ baseUrl?: string;
10
16
  webhookPort?: number;
11
- codebasePath?: string;
17
+ codebasePath?: string; // Deprecated: use codebasePaths instead
18
+ codebasePaths?: CodebaseEntry[];
12
19
  kubeConfigPath?: string;
13
20
  }
14
21
 
15
22
  const CONFIG_DIR = join(homedir(), ".config", "triagent");
16
23
  const CONFIG_FILE = join(CONFIG_DIR, "config.json");
24
+ const TRIAGENT_MD_FILE = join(CONFIG_DIR, "TRIAGENT.md");
17
25
 
18
26
  export async function getConfigPath(): Promise<string> {
19
27
  return CONFIG_FILE;
20
28
  }
21
29
 
30
+ export async function getTriagentMdPath(): Promise<string> {
31
+ return TRIAGENT_MD_FILE;
32
+ }
33
+
34
+ export async function loadTriagentMd(): Promise<string | null> {
35
+ try {
36
+ const content = await readFile(TRIAGENT_MD_FILE, "utf-8");
37
+ return content.trim();
38
+ } catch {
39
+ return null;
40
+ }
41
+ }
42
+
22
43
  export async function loadStoredConfig(): Promise<StoredConfig> {
23
44
  try {
24
45
  const content = await readFile(CONFIG_FILE, "utf-8");
@@ -39,7 +60,7 @@ export async function setConfigValue(key: keyof StoredConfig, value: string | nu
39
60
  await saveStoredConfig(config);
40
61
  }
41
62
 
42
- export async function getConfigValue(key: keyof StoredConfig): Promise<string | number | undefined> {
63
+ export async function getConfigValue(key: keyof StoredConfig): Promise<StoredConfig[keyof StoredConfig]> {
43
64
  const config = await loadStoredConfig();
44
65
  return config[key];
45
66
  }
package/src/config.ts CHANGED
@@ -1,21 +1,28 @@
1
1
  import { z } from "zod";
2
2
  import { resolve } from "path";
3
3
  import { homedir } from "os";
4
- import { loadStoredConfig, type StoredConfig } from "./cli/config.js";
4
+ import { loadStoredConfig, type StoredConfig, type CodebaseEntry } from "./cli/config.js";
5
5
 
6
6
  const AIProviderSchema = z.enum(["openai", "anthropic", "google"]);
7
7
  export type AIProvider = z.infer<typeof AIProviderSchema>;
8
8
 
9
+ const CodebaseEntrySchema = z.object({
10
+ name: z.string().min(1),
11
+ path: z.string().min(1),
12
+ });
13
+
9
14
  const ConfigSchema = z.object({
10
15
  aiProvider: AIProviderSchema,
11
16
  aiModel: z.string().min(1),
12
17
  apiKey: z.string().min(1),
18
+ baseUrl: z.string().url().optional(),
13
19
  webhookPort: z.number().int().positive().default(3000),
14
- codebasePath: z.string().min(1).default("./"),
20
+ codebasePaths: z.array(CodebaseEntrySchema).min(1),
15
21
  kubeConfigPath: z.string().min(1).default("~/.kube"),
16
22
  });
17
23
 
18
24
  export type Config = z.infer<typeof ConfigSchema>;
25
+ export type { CodebaseEntry };
19
26
 
20
27
  function expandPath(path: string): string {
21
28
  if (path.startsWith("~")) {
@@ -38,6 +45,20 @@ function getApiKey(provider: AIProvider, stored: StoredConfig): string {
38
45
  }
39
46
  }
40
47
 
48
+ function resolveCodebasePaths(stored: StoredConfig): CodebaseEntry[] {
49
+ // Priority: codebasePaths array > legacy codebasePath > default
50
+ if (stored.codebasePaths && stored.codebasePaths.length > 0) {
51
+ return stored.codebasePaths.map((entry) => ({
52
+ name: entry.name,
53
+ path: expandPath(entry.path),
54
+ }));
55
+ }
56
+
57
+ // Backward compatibility: convert single codebasePath to array
58
+ const legacyPath = process.env.CODEBASE_PATH || stored.codebasePath || "./";
59
+ return [{ name: "workspace", path: expandPath(legacyPath) }];
60
+ }
61
+
41
62
  export async function loadConfig(): Promise<Config> {
42
63
  const stored = await loadStoredConfig();
43
64
 
@@ -47,8 +68,9 @@ export async function loadConfig(): Promise<Config> {
47
68
  aiProvider: provider,
48
69
  aiModel: process.env.AI_MODEL || stored.aiModel || getDefaultModel(provider),
49
70
  apiKey: getApiKey(provider, stored),
71
+ baseUrl: process.env.AI_BASE_URL || stored.baseUrl || undefined,
50
72
  webhookPort: parseInt(process.env.WEBHOOK_PORT || String(stored.webhookPort || 3000), 10),
51
- codebasePath: expandPath(process.env.CODEBASE_PATH || stored.codebasePath || "./"),
73
+ codebasePaths: resolveCodebasePaths(stored),
52
74
  kubeConfigPath: expandPath(process.env.KUBE_CONFIG_PATH || stored.kubeConfigPath || "~/.kube"),
53
75
  };
54
76
 
package/src/index.ts CHANGED
@@ -1,8 +1,10 @@
1
1
  #!/usr/bin/env bun
2
+ // Load solid JSX plugin before any TSX imports
3
+ import "@opentui/solid/preload";
4
+
2
5
  import { loadConfig } from "./config.js";
3
6
  import { initSandboxFromConfig } from "./sandbox/bashlet.js";
4
7
  import { createMastraInstance, buildIncidentPrompt, getDebuggerAgent } from "./mastra/index.js";
5
- import { runTUI } from "./tui/app.jsx";
6
8
  import { startWebhookServer } from "./server/webhook.js";
7
9
  import {
8
10
  loadStoredConfig,
@@ -84,10 +86,18 @@ CONFIG KEYS:
84
86
  aiProvider - AI provider (openai, anthropic, google)
85
87
  aiModel - Model ID (e.g., gpt-4o, claude-sonnet-4-20250514)
86
88
  apiKey - API key for the provider
89
+ baseUrl - Custom API base URL (for proxies or local models)
87
90
  webhookPort - Webhook server port (default: 3000)
88
- codebasePath - Path to codebase (default: ./)
91
+ codebasePath - Path to codebase (default: ./) - for single codebase
89
92
  kubeConfigPath - Kubernetes config path (default: ~/.kube)
90
93
 
94
+ For multiple codebases, edit ~/.config/triagent/config.json directly:
95
+ "codebasePaths": [
96
+ { "name": "frontend", "path": "/path/to/frontend" },
97
+ { "name": "backend", "path": "/path/to/backend" }
98
+ ]
99
+ Each codebase will be mounted at /workspace/<name> in the sandbox.
100
+
91
101
  MODES:
92
102
  Interactive (default):
93
103
  Run with no arguments to start the interactive TUI.
@@ -109,6 +119,7 @@ MODES:
109
119
  ENVIRONMENT VARIABLES:
110
120
  AI_PROVIDER - AI provider (openai, anthropic, google)
111
121
  AI_MODEL - Model ID (e.g., gpt-4o, claude-3-5-sonnet)
122
+ AI_BASE_URL - Custom API base URL (for proxies or local models)
112
123
  OPENAI_API_KEY - OpenAI API key
113
124
  ANTHROPIC_API_KEY - Anthropic API key
114
125
  GOOGLE_GENERATIVE_AI_API_KEY - Google AI API key
@@ -152,7 +163,12 @@ async function runDirectIncident(description: string): Promise<void> {
152
163
  if (toolCalls && toolCalls.length > 0) {
153
164
  const toolCall = toolCalls[0];
154
165
  const toolName = "toolName" in toolCall ? toolCall.toolName : "tool";
155
- console.log(`\n[Tool: ${toolName}]\n`);
166
+ const args = "args" in toolCall ? toolCall.args : {};
167
+ console.log(`\n[Tool: ${toolName}]`);
168
+ if (args && typeof args === "object" && "command" in args) {
169
+ console.log(`$ ${args.command}`);
170
+ }
171
+ console.log();
156
172
  }
157
173
  },
158
174
  });
@@ -174,6 +190,7 @@ async function handleConfigCommand(args: CliArgs): Promise<void> {
174
190
  "aiProvider",
175
191
  "aiModel",
176
192
  "apiKey",
193
+ "baseUrl",
177
194
  "webhookPort",
178
195
  "codebasePath",
179
196
  "kubeConfigPath",
@@ -270,7 +287,7 @@ async function main(): Promise<void> {
270
287
  // Initialize sandbox and Mastra
271
288
  try {
272
289
  initSandboxFromConfig(config, args.host);
273
- createMastraInstance(config);
290
+ await createMastraInstance(config);
274
291
  if (args.host) {
275
292
  console.log("⚠️ Running in host mode (no sandbox)\n");
276
293
  }
@@ -289,6 +306,8 @@ async function main(): Promise<void> {
289
306
  } else {
290
307
  // Interactive TUI mode
291
308
  console.log("Starting Triagent TUI...\n");
309
+ // Dynamic import to ensure solid plugin is loaded first
310
+ const { runTUI } = await import("./tui/app.jsx");
292
311
  const tui = await runTUI();
293
312
 
294
313
  // Handle graceful shutdown
@@ -1,20 +1,23 @@
1
1
  import { Agent } from "@mastra/core/agent";
2
2
  import { z } from "zod";
3
- import { kubectlTool } from "../tools/kubectl.js";
3
+ import { cliTool } from "../tools/cli.js";
4
4
  import { gitTool } from "../tools/git.js";
5
5
  import { filesystemTool } from "../tools/filesystem.js";
6
+ import { loadTriagentMd } from "../../cli/config.js";
6
7
  import type { Config } from "../../config.js";
7
8
 
8
9
  const DEBUGGER_INSTRUCTIONS = `You are an expert Kubernetes debugging agent named Triagent. Your role is to investigate and diagnose issues in Kubernetes clusters by analyzing resources, logs, code, and git history.
9
10
 
10
11
  ## Your Capabilities
11
12
 
12
- 1. **Kubernetes Inspection** (kubectl tool):
13
- - Get resource status (pods, deployments, services, configmaps)
14
- - Describe resources for detailed information
15
- - Fetch container logs
16
- - Check resource usage (top)
17
- - Review cluster events
13
+ 1. **CLI Access** (cli tool):
14
+ - Run any shell command including kubectl, grep, awk, jq, curl, etc.
15
+ - Pipe commands together for powerful filtering and processing
16
+ - Examples:
17
+ - \`kubectl get pods -A | grep inventory\`
18
+ - \`kubectl logs deploy/myapp --tail 100 | grep -i error\`
19
+ - \`kubectl get pods -o json | jq '.items[].metadata.name'\`
20
+ - \`kubectl describe pod mypod | grep -A10 Events\`
18
21
 
19
22
  2. **Code Analysis** (filesystem tool):
20
23
  - Read source code files
@@ -27,6 +30,39 @@ const DEBUGGER_INSTRUCTIONS = `You are an expert Kubernetes debugging agent name
27
30
  - Show specific commit details
28
31
  - Blame files to find who changed what
29
32
 
33
+ ## Resource Discovery Strategy
34
+
35
+ When asked to find resources for a service (e.g., "inventory service"), DO NOT simply try one label like \`app=inventory\` and give up if not found. Instead, use a systematic discovery approach:
36
+
37
+ 1. **Search by partial name match using grep**:
38
+ - \`kubectl get pods -A | grep -i inventory\`
39
+ - \`kubectl get deploy,svc -A | grep -i inventory\`
40
+ - This finds resources with "inventory" anywhere in the name (e.g., \`inventory-api\`, \`svc-inventory\`)
41
+
42
+ 2. **If grep returns no results, list all resources to browse**:
43
+ - \`kubectl get pods,deploy,svc -A\` to see everything
44
+ - \`kubectl get pods -n <namespace>\` if namespace is known
45
+
46
+ 3. **Try common label patterns**:
47
+ - \`kubectl get pods -A -l app=inventory\`
48
+ - \`kubectl get pods -A -l app.kubernetes.io/name=inventory\`
49
+ - \`kubectl get pods -A -l component=inventory\`
50
+
51
+ 4. **Follow the resource chain**:
52
+ - Found a Service? \`kubectl describe svc <name> | grep Selector\` then find pods with that selector
53
+ - Found a Deployment? \`kubectl get pods -l app=<deployment-name>\`
54
+ - Use \`kubectl get endpoints <svc-name>\` to see which pods back a service
55
+
56
+ 5. **Check events for context**:
57
+ - \`kubectl get events -A --sort-by='.lastTimestamp' | grep -i inventory\`
58
+ - \`kubectl get events -A --sort-by='.lastTimestamp' | head -20\` for recent cluster activity
59
+
60
+ 6. **When you find a potential match**:
61
+ - \`kubectl describe <resource> <name>\` to confirm it's the right one
62
+ - Check related resources (pods for a deployment, endpoints for a service)
63
+
64
+ Always report what you searched for and what you found, even if it's not an exact match. The user can confirm if you found the right resource.
65
+
30
66
  ## Investigation Process
31
67
 
32
68
  When given an incident, follow this systematic approach:
@@ -36,26 +72,32 @@ When given an incident, follow this systematic approach:
36
72
  - What symptoms are being observed
37
73
  - When the issue started (if known)
38
74
 
39
- 2. **Check Cluster State**:
40
- - Get pod status for affected services
41
- - Check for recent events
75
+ 2. **Discover Relevant Resources**:
76
+ - Use the Resource Discovery Strategy above to find the affected resources
77
+ - Don't assume exact names or labels - search broadly first
78
+ - Follow the resource chain (Service → Deployment → Pods → Containers)
79
+
80
+ 3. **Check Cluster State**:
81
+ - Get pod status for discovered resources
82
+ - Check for recent events related to those resources
42
83
  - Look at resource usage
43
84
 
44
- 3. **Analyze Logs**:
45
- - Fetch logs from affected pods
85
+ 4. **Analyze Logs**:
86
+ - Fetch logs from affected pods (use \`--tail 100\` to get recent logs)
46
87
  - Look for errors, exceptions, or unusual patterns
88
+ - If multiple containers, check each one
47
89
 
48
- 4. **Investigate Recent Changes**:
90
+ 5. **Investigate Recent Changes**:
49
91
  - Check git log for recent commits
50
92
  - Review diffs of suspicious changes
51
93
  - Correlate timing with when issues started
52
94
 
53
- 5. **Examine Code**:
95
+ 6. **Examine Code**:
54
96
  - Read relevant configuration files
55
97
  - Check application code if needed
56
98
  - Look for misconfigurations
57
99
 
58
- 6. **Synthesize Findings**:
100
+ 7. **Synthesize Findings**:
59
101
  - Identify the root cause
60
102
  - List affected resources
61
103
  - Provide actionable recommendations
@@ -121,17 +163,30 @@ export const InvestigationResultSchema = z.object({
121
163
 
122
164
  export type InvestigationResult = z.infer<typeof InvestigationResultSchema>;
123
165
 
124
- export function createDebuggerAgent(config: Config) {
125
- // Construct model string based on provider
126
- const modelString = `${config.aiProvider}/${config.aiModel}`;
166
+ export async function createDebuggerAgent(config: Config) {
167
+ // Load user instructions from ~/.config/triagent/TRIAGENT.md if present
168
+ const userInstructions = await loadTriagentMd();
169
+
170
+ // Combine user instructions with default instructions
171
+ const instructions = userInstructions
172
+ ? `## User-Provided Instructions\n\n${userInstructions}\n\n---\n\n${DEBUGGER_INSTRUCTIONS}`
173
+ : DEBUGGER_INSTRUCTIONS;
174
+
175
+ // Construct model config with API key and optional base URL
176
+ const modelId = `${config.aiProvider}/${config.aiModel}` as const;
177
+ const modelConfig = {
178
+ id: modelId,
179
+ apiKey: config.apiKey,
180
+ ...(config.baseUrl && { url: config.baseUrl }),
181
+ };
127
182
 
128
183
  return new Agent({
129
184
  id: "kubernetes-debugger",
130
185
  name: "Kubernetes Debugger",
131
- instructions: DEBUGGER_INSTRUCTIONS,
132
- model: modelString as any, // Mastra handles model routing
186
+ instructions,
187
+ model: modelConfig as any, // Mastra handles model routing
133
188
  tools: {
134
- kubectl: kubectlTool,
189
+ cli: cliTool,
135
190
  git: gitTool,
136
191
  filesystem: filesystemTool,
137
192
  },
@@ -4,12 +4,12 @@ import type { Config } from "../config.js";
4
4
 
5
5
  let mastraInstance: Mastra | null = null;
6
6
 
7
- export function createMastraInstance(config: Config): Mastra {
7
+ export async function createMastraInstance(config: Config): Promise<Mastra> {
8
8
  if (mastraInstance) {
9
9
  return mastraInstance;
10
10
  }
11
11
 
12
- const debuggerAgent = createDebuggerAgent(config);
12
+ const debuggerAgent = await createDebuggerAgent(config);
13
13
 
14
14
  mastraInstance = new Mastra({
15
15
  agents: {
@@ -0,0 +1,65 @@
1
+ import { createTool } from "@mastra/core/tools";
2
+ import { z } from "zod";
3
+ import { execCommand } from "../../sandbox/bashlet.js";
4
+
5
+ interface CliOutput {
6
+ success: boolean;
7
+ output: string;
8
+ error?: string;
9
+ }
10
+
11
+ function filterSensitiveData(output: string): string {
12
+ // Redact potential secrets, tokens, and passwords
13
+ return output
14
+ .replace(
15
+ /(password|secret|token|key|credential)[\s:=]+["']?[^\s"'\n]+["']?/gi,
16
+ "$1: [REDACTED]"
17
+ )
18
+ .replace(/Bearer\s+[^\s]+/gi, "Bearer [REDACTED]")
19
+ .replace(/-----BEGIN[^-]+-----[\s\S]*?-----END[^-]+-----/g, "[REDACTED CERTIFICATE/KEY]");
20
+ }
21
+
22
+ export const cliTool = createTool({
23
+ id: "cli",
24
+ description: `Execute shell commands in the sandbox environment.
25
+ Use this to run any CLI commands including kubectl, grep, awk, jq, curl, etc.
26
+ Supports pipes and command chaining.
27
+
28
+ Examples:
29
+ - List all pods: kubectl get pods -A
30
+ - Find pods by name: kubectl get pods -A | grep inventory
31
+ - Get logs with filtering: kubectl logs deployment/myapp -n prod --tail 100 | grep -i error
32
+ - Check resource usage: kubectl top pods -n default
33
+ - Describe and search: kubectl describe pod mypod | grep -A5 "Events"
34
+ - JSON processing: kubectl get pods -o json | jq '.items[].metadata.name'`,
35
+
36
+ inputSchema: z.object({
37
+ command: z.string().describe("The shell command to execute"),
38
+ }),
39
+
40
+ execute: async ({ command }): Promise<CliOutput> => {
41
+ try {
42
+ const result = await execCommand(command);
43
+
44
+ if (result.exitCode !== 0) {
45
+ return {
46
+ success: false,
47
+ output: result.stdout ? filterSensitiveData(result.stdout) : "",
48
+ error: result.stderr || `Command failed with exit code ${result.exitCode}`,
49
+ };
50
+ }
51
+
52
+ return {
53
+ success: true,
54
+ output: filterSensitiveData(result.stdout),
55
+ error: result.stderr ? filterSensitiveData(result.stderr) : undefined,
56
+ };
57
+ } catch (error) {
58
+ return {
59
+ success: false,
60
+ output: "",
61
+ error: error instanceof Error ? error.message : String(error),
62
+ };
63
+ }
64
+ },
65
+ });
@@ -1,7 +1,7 @@
1
1
  import { Bashlet } 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,7 +10,7 @@ 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;
@@ -22,7 +22,8 @@ let hostWorkdir = "./";
22
22
 
23
23
  export function createSandbox(options: SandboxOptions): void {
24
24
  hostMode = options.useHost ?? false;
25
- hostWorkdir = options.codebasePath;
25
+ // Use first codebase as default working directory
26
+ hostWorkdir = options.codebasePaths[0]?.path || "./";
26
27
 
27
28
  if (hostMode) {
28
29
  return;
@@ -32,9 +33,15 @@ export function createSandbox(options: SandboxOptions): void {
32
33
  return;
33
34
  }
34
35
 
36
+ // Mount each codebase at /workspace/<name>
37
+ const codebaseMounts = options.codebasePaths.map((entry) => ({
38
+ hostPath: entry.path,
39
+ guestPath: `/workspace/${entry.name}`,
40
+ }));
41
+
35
42
  bashletInstance = new Bashlet({
36
43
  mounts: [
37
- { hostPath: options.codebasePath, guestPath: "/workspace" },
44
+ ...codebaseMounts,
38
45
  { hostPath: options.kubeConfigPath, guestPath: "/root/.kube" },
39
46
  ],
40
47
  workdir: "/workspace",
@@ -143,7 +150,7 @@ export async function listDir(path: string): Promise<string[]> {
143
150
 
144
151
  export function initSandboxFromConfig(config: Config, useHost: boolean = false): void {
145
152
  createSandbox({
146
- codebasePath: config.codebasePath,
153
+ codebasePaths: config.codebasePaths,
147
154
  kubeConfigPath: config.kubeConfigPath,
148
155
  timeout: 120,
149
156
  useHost,
@@ -149,7 +149,11 @@ async function runInvestigation(id: string): Promise<void> {
149
149
  if (toolCalls && toolCalls.length > 0) {
150
150
  const toolCall = toolCalls[0];
151
151
  const toolName = "toolName" in toolCall ? toolCall.toolName : "tool";
152
+ const args = "args" in toolCall ? toolCall.args : {};
152
153
  console.log(`[Investigation ${id}] Tool: ${toolName}`);
154
+ if (args && typeof args === "object" && "command" in args) {
155
+ console.log(`[Investigation ${id}] $ ${args.command}`);
156
+ }
153
157
  }
154
158
  },
155
159
  });
package/src/tui/app.tsx CHANGED
@@ -1,6 +1,8 @@
1
+ /* @jsxImportSource @opentui/solid */
1
2
  import { render } from "@opentui/solid";
2
3
  import { createSignal, For, Show, onMount } from "solid-js";
3
4
  import { createTextAttributes } from "@opentui/core";
5
+ import "opentui-spinner/solid";
4
6
  import { getDebuggerAgent, buildIncidentPrompt } from "../mastra/index.js";
5
7
  import type { IncidentInput } from "../mastra/agents/debugger.js";
6
8
 
@@ -10,6 +12,25 @@ interface Message {
10
12
  content: string;
11
13
  timestamp: Date;
12
14
  toolName?: string;
15
+ command?: string;
16
+ }
17
+
18
+ // Conversation history for multi-turn debugging
19
+ interface ConversationMessage {
20
+ role: "user" | "assistant";
21
+ content: string;
22
+ }
23
+
24
+ function formatHistoryAsPrompt(history: ConversationMessage[], newMessage: string): string {
25
+ if (history.length === 0) {
26
+ return newMessage;
27
+ }
28
+
29
+ const historyText = history
30
+ .map((msg) => `${msg.role === "user" ? "User" : "Assistant"}: ${msg.content}`)
31
+ .join("\n\n");
32
+
33
+ return `Previous conversation:\n${historyText}\n\nUser: ${newMessage}`;
13
34
  }
14
35
 
15
36
  type AppStatus = "idle" | "investigating" | "complete" | "error";
@@ -17,8 +38,55 @@ type AppStatus = "idle" | "investigating" | "complete" | "error";
17
38
  const ATTR_DIM = createTextAttributes({ dim: true });
18
39
  const ATTR_BOLD = createTextAttributes({ bold: true });
19
40
 
41
+ function buildDisplayCommand(toolName: string, args: unknown): string | undefined {
42
+ if (!args || typeof args !== "object") return undefined;
43
+
44
+ const a = args as Record<string, unknown>;
45
+
46
+ switch (toolName) {
47
+ case "cli":
48
+ // CLI tool has direct command
49
+ return "command" in a ? String(a.command) : undefined;
50
+
51
+ case "git": {
52
+ // Build git command: git <command> [args...] [path]
53
+ if (!("command" in a)) return undefined;
54
+ const parts = ["git", String(a.command)];
55
+ if ("args" in a && Array.isArray(a.args)) {
56
+ parts.push(...a.args.map(String));
57
+ }
58
+ if ("path" in a && a.path) {
59
+ parts.push(String(a.path));
60
+ }
61
+ return parts.join(" ");
62
+ }
63
+
64
+ case "filesystem": {
65
+ // Build filesystem display: <operation> <path> [pattern]
66
+ if (!("operation" in a)) return undefined;
67
+ const op = String(a.operation);
68
+ const path = "path" in a ? String(a.path) : "";
69
+ if (op === "search" && "pattern" in a) {
70
+ return `grep "${a.pattern}" ${path}`;
71
+ }
72
+ if (op === "read") {
73
+ return `cat ${path}`;
74
+ }
75
+ if (op === "list") {
76
+ return `ls ${path}`;
77
+ }
78
+ return `${op} ${path}`;
79
+ }
80
+
81
+ default:
82
+ // Fallback: try to use command if it exists
83
+ return "command" in a ? String(a.command) : undefined;
84
+ }
85
+ }
86
+
20
87
  function App() {
21
88
  const [messages, setMessages] = createSignal<Message[]>([]);
89
+ const [conversationHistory, setConversationHistory] = createSignal<ConversationMessage[]>([]);
22
90
  const [status, setStatus] = createSignal<AppStatus>("idle");
23
91
  const [currentTool, setCurrentTool] = createSignal<string | null>(null);
24
92
  const [inputValue, setInputValue] = createSignal("");
@@ -40,29 +108,50 @@ function App() {
40
108
  setError(null);
41
109
  setCurrentTool(null);
42
110
 
111
+ // Add user message to UI
43
112
  addMessage({
44
113
  role: "user",
45
114
  content: incident.description,
46
115
  });
47
116
 
117
+ // Build prompt: use full incident prompt for first message, include history for follow-ups
118
+ const isFirstMessage = conversationHistory().length === 0;
119
+ const userContent = isFirstMessage
120
+ ? buildIncidentPrompt(incident)
121
+ : incident.description;
122
+
123
+ // Format prompt with conversation history
124
+ const prompt = formatHistoryAsPrompt(conversationHistory(), userContent);
125
+
126
+ // Add user message to conversation history
127
+ setConversationHistory((prev) => [
128
+ ...prev,
129
+ { role: "user", content: userContent },
130
+ ]);
131
+
48
132
  try {
49
133
  const agent = getDebuggerAgent();
50
- const prompt = buildIncidentPrompt(incident);
51
134
 
52
135
  let assistantContent = "";
53
136
 
137
+ // Send the formatted prompt to the agent
54
138
  const stream = await agent.stream(prompt, {
55
139
  maxSteps: 20,
56
140
  onStepFinish: ({ toolCalls }) => {
57
141
  if (toolCalls && toolCalls.length > 0) {
58
- const toolCall = toolCalls[0];
59
- const toolName =
60
- "toolName" in toolCall ? String(toolCall.toolName) : "tool";
142
+ const toolCall = toolCalls[0] as { toolName?: string; args?: unknown };
143
+ const toolName = toolCall.toolName ?? "tool";
144
+ const args = toolCall.args ?? {};
145
+
146
+ // Build display command based on tool type
147
+ const command = buildDisplayCommand(toolName, args);
148
+
61
149
  setCurrentTool(toolName);
62
150
  addMessage({
63
151
  role: "tool",
64
- content: `Executing ${toolName}...`,
152
+ content: command ? `$ ${command}` : `Executing ${toolName}...`,
65
153
  toolName,
154
+ command,
66
155
  });
67
156
  }
68
157
  },
@@ -72,11 +161,18 @@ function App() {
72
161
  assistantContent += chunk;
73
162
  }
74
163
 
164
+ // Add assistant response to UI
75
165
  addMessage({
76
166
  role: "assistant",
77
167
  content: assistantContent,
78
168
  });
79
169
 
170
+ // Add assistant response to conversation history
171
+ setConversationHistory((prev) => [
172
+ ...prev,
173
+ { role: "assistant", content: assistantContent },
174
+ ]);
175
+
80
176
  setStatus("complete");
81
177
  setCurrentTool(null);
82
178
  } catch (err) {
@@ -185,7 +281,13 @@ function App() {
185
281
  </box>
186
282
  </Show>
187
283
  <Show when={msg.role === "tool"}>
188
- <box flexDirection="row" gap={1}>
284
+ <box flexDirection="row" gap={1} alignItems="center">
285
+ <Show
286
+ when={status() === "investigating" && msg.id === messages().filter(m => m.role === "tool").at(-1)?.id}
287
+ fallback={<text fg="green">✓</text>}
288
+ >
289
+ <spinner name="dots" color="blue" />
290
+ </Show>
189
291
  <text fg="blue" attributes={ATTR_DIM}>
190
292
  [{msg.toolName}]
191
293
  </text>
@@ -1,107 +0,0 @@
1
- import { createTool } from "@mastra/core/tools";
2
- import { z } from "zod";
3
- import { execCommand } from "../../sandbox/bashlet.js";
4
-
5
- const ALLOWED_COMMANDS = ["get", "describe", "logs", "top", "events"] as const;
6
-
7
- const KubectlInputSchema = z.object({
8
- command: z.enum(ALLOWED_COMMANDS).describe("The kubectl command to run"),
9
- resource: z
10
- .string()
11
- .optional()
12
- .describe(
13
- "Resource type (e.g., pods, deployments, services, configmaps, secrets)"
14
- ),
15
- name: z.string().optional().describe("Specific resource name"),
16
- namespace: z
17
- .string()
18
- .optional()
19
- .describe("Kubernetes namespace (defaults to current context namespace)"),
20
- flags: z
21
- .array(z.string())
22
- .optional()
23
- .describe(
24
- "Additional flags (e.g., ['-o', 'yaml'], ['--tail', '100'], ['-l', 'app=myapp'])"
25
- ),
26
- });
27
-
28
- // Output type (no schema validation to allow error returns)
29
- interface KubectlOutput {
30
- success: boolean;
31
- output: string;
32
- error?: string;
33
- }
34
-
35
- function filterSensitiveData(output: string): string {
36
- // Redact potential secrets, tokens, and passwords
37
- return output
38
- .replace(
39
- /(password|secret|token|key|credential)[\s:=]+["']?[^\s"'\n]+["']?/gi,
40
- "$1: [REDACTED]"
41
- )
42
- .replace(/Bearer\s+[^\s]+/gi, "Bearer [REDACTED]")
43
- .replace(/-----BEGIN[^-]+-----[\s\S]*?-----END[^-]+-----/g, "[REDACTED CERTIFICATE/KEY]");
44
- }
45
-
46
- export const kubectlTool = createTool({
47
- id: "kubectl",
48
- description: `Execute kubectl commands to inspect Kubernetes resources.
49
- Available commands: ${ALLOWED_COMMANDS.join(", ")}.
50
- Use this to get information about pods, deployments, services, logs, and cluster events.
51
- Examples:
52
- - Get all pods: { command: "get", resource: "pods", flags: ["-A"] }
53
- - Get pod logs: { command: "logs", name: "my-pod", namespace: "default", flags: ["--tail", "100"] }
54
- - Describe deployment: { command: "describe", resource: "deployment", name: "my-app" }
55
- - Get events: { command: "events", namespace: "production", flags: ["--sort-by", ".lastTimestamp"] }`,
56
-
57
- inputSchema: KubectlInputSchema,
58
-
59
- execute: async (inputData): Promise<KubectlOutput> => {
60
- const { command, resource, name, namespace, flags } = inputData;
61
-
62
- // Build kubectl command
63
- const parts = ["kubectl", command];
64
-
65
- if (resource) {
66
- parts.push(resource);
67
- }
68
-
69
- if (name) {
70
- parts.push(name);
71
- }
72
-
73
- if (namespace) {
74
- parts.push("-n", namespace);
75
- }
76
-
77
- if (flags && flags.length > 0) {
78
- parts.push(...flags);
79
- }
80
-
81
- const fullCommand = parts.join(" ");
82
-
83
- try {
84
- const result = await execCommand(fullCommand);
85
-
86
- if (result.exitCode !== 0) {
87
- return {
88
- success: false,
89
- output: "",
90
- error: result.stderr || `Command failed with exit code ${result.exitCode}`,
91
- };
92
- }
93
-
94
- return {
95
- success: true,
96
- output: filterSensitiveData(result.stdout),
97
- error: result.stderr ? filterSensitiveData(result.stderr) : undefined,
98
- };
99
- } catch (error) {
100
- return {
101
- success: false,
102
- output: "",
103
- error: error instanceof Error ? error.message : String(error),
104
- };
105
- }
106
- },
107
- });