triagent 0.1.0-alpha10 → 0.1.0-alpha13

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
@@ -57,9 +57,46 @@ triagent config path
57
57
  | `apiKey` | API key for the provider | - |
58
58
  | `baseUrl` | Custom API base URL (for proxies or local models) | - |
59
59
  | `webhookPort` | Webhook server port | `3000` |
60
- | `codebasePath` | Path to codebase | `./` |
60
+ | `codebasePath` | Path to single codebase (legacy) | `./` |
61
61
  | `kubeConfigPath` | Kubernetes config path | `~/.kube` |
62
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
+
63
100
  ### Environment Variables
64
101
 
65
102
  | Variable | Description |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "triagent",
3
- "version": "0.1.0-alpha10",
3
+ "version": "0.1.0-alpha13",
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,6 +50,8 @@
46
50
  "@opentui/core": "^0.1.72",
47
51
  "@opentui/solid": "^0.1.72",
48
52
  "hono": "^4.6.0",
53
+ "marked": "^17.0.1",
54
+ "marked-terminal": "^7.3.0",
49
55
  "opentui-spinner": "^0.0.6",
50
56
  "solid-js": "1.9.9",
51
57
  "triagent": "^0.1.0-alpha1",
@@ -53,6 +59,7 @@
53
59
  },
54
60
  "devDependencies": {
55
61
  "@types/babel__core": "^7.20.5",
62
+ "@types/marked-terminal": "^6.1.1",
56
63
  "@types/node": "^22.0.0",
57
64
  "bun-types": "^1.2.0",
58
65
  "typescript": "^5.7.0"
package/src/cli/config.ts CHANGED
@@ -3,23 +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;
10
15
  baseUrl?: string;
11
16
  webhookPort?: number;
12
- codebasePath?: string;
17
+ codebasePath?: string; // Deprecated: use codebasePaths instead
18
+ codebasePaths?: CodebaseEntry[];
13
19
  kubeConfigPath?: string;
14
20
  }
15
21
 
16
22
  const CONFIG_DIR = join(homedir(), ".config", "triagent");
17
23
  const CONFIG_FILE = join(CONFIG_DIR, "config.json");
24
+ const TRIAGENT_MD_FILE = join(CONFIG_DIR, "TRIAGENT.md");
18
25
 
19
26
  export async function getConfigPath(): Promise<string> {
20
27
  return CONFIG_FILE;
21
28
  }
22
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
+
23
43
  export async function loadStoredConfig(): Promise<StoredConfig> {
24
44
  try {
25
45
  const content = await readFile(CONFIG_FILE, "utf-8");
@@ -40,7 +60,7 @@ export async function setConfigValue(key: keyof StoredConfig, value: string | nu
40
60
  await saveStoredConfig(config);
41
61
  }
42
62
 
43
- export async function getConfigValue(key: keyof StoredConfig): Promise<string | number | undefined> {
63
+ export async function getConfigValue(key: keyof StoredConfig): Promise<StoredConfig[keyof StoredConfig]> {
44
64
  const config = await loadStoredConfig();
45
65
  return config[key];
46
66
  }
package/src/config.ts CHANGED
@@ -1,22 +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),
13
18
  baseUrl: z.string().url().optional(),
14
19
  webhookPort: z.number().int().positive().default(3000),
15
- codebasePath: z.string().min(1).default("./"),
20
+ codebasePaths: z.array(CodebaseEntrySchema).min(1),
16
21
  kubeConfigPath: z.string().min(1).default("~/.kube"),
17
22
  });
18
23
 
19
24
  export type Config = z.infer<typeof ConfigSchema>;
25
+ export type { CodebaseEntry };
20
26
 
21
27
  function expandPath(path: string): string {
22
28
  if (path.startsWith("~")) {
@@ -39,6 +45,20 @@ function getApiKey(provider: AIProvider, stored: StoredConfig): string {
39
45
  }
40
46
  }
41
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
+
42
62
  export async function loadConfig(): Promise<Config> {
43
63
  const stored = await loadStoredConfig();
44
64
 
@@ -50,7 +70,7 @@ export async function loadConfig(): Promise<Config> {
50
70
  apiKey: getApiKey(provider, stored),
51
71
  baseUrl: process.env.AI_BASE_URL || stored.baseUrl || undefined,
52
72
  webhookPort: parseInt(process.env.WEBHOOK_PORT || String(stored.webhookPort || 3000), 10),
53
- codebasePath: expandPath(process.env.CODEBASE_PATH || stored.codebasePath || "./"),
73
+ codebasePaths: resolveCodebasePaths(stored),
54
74
  kubeConfigPath: expandPath(process.env.KUBE_CONFIG_PATH || stored.kubeConfigPath || "~/.kube"),
55
75
  };
56
76
 
package/src/index.ts CHANGED
@@ -88,9 +88,16 @@ CONFIG KEYS:
88
88
  apiKey - API key for the provider
89
89
  baseUrl - Custom API base URL (for proxies or local models)
90
90
  webhookPort - Webhook server port (default: 3000)
91
- codebasePath - Path to codebase (default: ./)
91
+ codebasePath - Path to codebase (default: ./) - for single codebase
92
92
  kubeConfigPath - Kubernetes config path (default: ~/.kube)
93
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
+
94
101
  MODES:
95
102
  Interactive (default):
96
103
  Run with no arguments to start the interactive TUI.
@@ -280,7 +287,7 @@ async function main(): Promise<void> {
280
287
  // Initialize sandbox and Mastra
281
288
  try {
282
289
  initSandboxFromConfig(config, args.host);
283
- createMastraInstance(config);
290
+ await createMastraInstance(config);
284
291
  if (args.host) {
285
292
  console.log("⚠️ Running in host mode (no sandbox)\n");
286
293
  }
@@ -3,6 +3,7 @@ import { z } from "zod";
3
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.
@@ -162,7 +163,15 @@ export const InvestigationResultSchema = z.object({
162
163
 
163
164
  export type InvestigationResult = z.infer<typeof InvestigationResultSchema>;
164
165
 
165
- export function createDebuggerAgent(config: Config) {
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
+
166
175
  // Construct model config with API key and optional base URL
167
176
  const modelId = `${config.aiProvider}/${config.aiModel}` as const;
168
177
  const modelConfig = {
@@ -174,7 +183,7 @@ export function createDebuggerAgent(config: Config) {
174
183
  return new Agent({
175
184
  id: "kubernetes-debugger",
176
185
  name: "Kubernetes Debugger",
177
- instructions: DEBUGGER_INSTRUCTIONS,
186
+ instructions,
178
187
  model: modelConfig as any, // Mastra handles model routing
179
188
  tools: {
180
189
  cli: cliTool,
@@ -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: {
@@ -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,
package/src/tui/app.tsx CHANGED
@@ -3,9 +3,22 @@ import { render } from "@opentui/solid";
3
3
  import { createSignal, For, Show, onMount } from "solid-js";
4
4
  import { createTextAttributes } from "@opentui/core";
5
5
  import "opentui-spinner/solid";
6
+ import { marked } from "marked";
7
+ import { markedTerminal } from "marked-terminal";
6
8
  import { getDebuggerAgent, buildIncidentPrompt } from "../mastra/index.js";
7
9
  import type { IncidentInput } from "../mastra/agents/debugger.js";
8
10
 
11
+ // Configure marked with terminal renderer
12
+ marked.use(markedTerminal() as any);
13
+
14
+ function renderMarkdown(content: string): string {
15
+ try {
16
+ return marked(content) as string;
17
+ } catch {
18
+ return content;
19
+ }
20
+ }
21
+
9
22
  interface Message {
10
23
  id: string;
11
24
  role: "user" | "assistant" | "tool";
@@ -302,7 +315,7 @@ function App() {
302
315
  Triagent:
303
316
  </text>
304
317
  <text fg="white" wrapMode="word">
305
- {msg.content}
318
+ {renderMarkdown(msg.content)}
306
319
  </text>
307
320
  </box>
308
321
  </Show>