kubeagent 0.1.0

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 (57) hide show
  1. package/LICENSE +72 -0
  2. package/README.md +154 -0
  3. package/dist/auth.d.ts +23 -0
  4. package/dist/auth.js +162 -0
  5. package/dist/cli.d.ts +2 -0
  6. package/dist/cli.js +447 -0
  7. package/dist/config.d.ts +50 -0
  8. package/dist/config.js +79 -0
  9. package/dist/debug.d.ts +10 -0
  10. package/dist/debug.js +18 -0
  11. package/dist/diagnoser/index.d.ts +17 -0
  12. package/dist/diagnoser/index.js +251 -0
  13. package/dist/diagnoser/tools.d.ts +119 -0
  14. package/dist/diagnoser/tools.js +108 -0
  15. package/dist/kb/loader.d.ts +1 -0
  16. package/dist/kb/loader.js +41 -0
  17. package/dist/kb/writer.d.ts +11 -0
  18. package/dist/kb/writer.js +36 -0
  19. package/dist/kubectl-config.d.ts +7 -0
  20. package/dist/kubectl-config.js +47 -0
  21. package/dist/kubectl.d.ts +13 -0
  22. package/dist/kubectl.js +57 -0
  23. package/dist/monitor/checks.d.ts +71 -0
  24. package/dist/monitor/checks.js +167 -0
  25. package/dist/monitor/index.d.ts +7 -0
  26. package/dist/monitor/index.js +126 -0
  27. package/dist/monitor/types.d.ts +11 -0
  28. package/dist/monitor/types.js +1 -0
  29. package/dist/notify/index.d.ts +5 -0
  30. package/dist/notify/index.js +40 -0
  31. package/dist/notify/setup.d.ts +4 -0
  32. package/dist/notify/setup.js +88 -0
  33. package/dist/notify/slack.d.ts +4 -0
  34. package/dist/notify/slack.js +76 -0
  35. package/dist/notify/telegram.d.ts +8 -0
  36. package/dist/notify/telegram.js +63 -0
  37. package/dist/notify/webhook.d.ts +3 -0
  38. package/dist/notify/webhook.js +49 -0
  39. package/dist/onboard/cluster-scan.d.ts +42 -0
  40. package/dist/onboard/cluster-scan.js +103 -0
  41. package/dist/onboard/code-scan.d.ts +9 -0
  42. package/dist/onboard/code-scan.js +114 -0
  43. package/dist/onboard/index.d.ts +1 -0
  44. package/dist/onboard/index.js +328 -0
  45. package/dist/onboard/interview.d.ts +12 -0
  46. package/dist/onboard/interview.js +71 -0
  47. package/dist/onboard/project-matcher.d.ts +25 -0
  48. package/dist/onboard/project-matcher.js +149 -0
  49. package/dist/orchestrator.d.ts +3 -0
  50. package/dist/orchestrator.js +222 -0
  51. package/dist/proxy-client.d.ts +15 -0
  52. package/dist/proxy-client.js +72 -0
  53. package/dist/render.d.ts +5 -0
  54. package/dist/render.js +143 -0
  55. package/dist/verifier.d.ts +9 -0
  56. package/dist/verifier.js +17 -0
  57. package/package.json +39 -0
@@ -0,0 +1,251 @@
1
+ import chalk from "chalk";
2
+ import readline from "node:readline";
3
+ import { buildSystemPrompt } from "../kb/loader.js";
4
+ import { loadAuth } from "../auth.js";
5
+ import { proxyRequest } from "../proxy-client.js";
6
+ import { dbg } from "../debug.js";
7
+ import { getLogs, describeResource, restartPod, rolloutRestart, scaleDeployment, getEvents, setResources, kubectlGetLogsSchema, kubectlDescribeSchema, restartPodSchema, rolloutRestartSchema, scaleDeploymentSchema, getEventsSchema, setResourcesSchema, } from "./tools.js";
8
+ import { zodToJsonSchema } from "zod-to-json-schema";
9
+ const MAX_TOOL_OUTPUT_CHARS = 8000;
10
+ const HAIKU = "claude-haiku-4-5-20251001";
11
+ const SONNET = "claude-sonnet-4-6";
12
+ function selectModel(issues) {
13
+ if (issues.some((i) => i.kind === "node_not_ready" || i.kind === "node_pressure"))
14
+ return SONNET;
15
+ if (issues.length >= 3)
16
+ return SONNET;
17
+ const maxRestarts = Math.max(0, ...issues.map((i) => i.details.restartCount ?? 0));
18
+ if (maxRestarts > 20)
19
+ return SONNET;
20
+ if (issues.filter((i) => i.kind === "pod_oom").length >= 2)
21
+ return SONNET;
22
+ return HAIKU;
23
+ }
24
+ async function createMessage(params) {
25
+ const auth = loadAuth();
26
+ if (!auth?.apiKey) {
27
+ throw new Error("Not logged in. Run 'kubeagent login' to get started.");
28
+ }
29
+ const result = await proxyRequest(auth, params);
30
+ return result;
31
+ }
32
+ function truncateOutput(output) {
33
+ if (output.length <= MAX_TOOL_OUTPUT_CHARS)
34
+ return output;
35
+ const half = Math.floor(MAX_TOOL_OUTPUT_CHARS / 2);
36
+ return output.slice(0, half) + "\n\n... [truncated] ...\n\n" + output.slice(-half);
37
+ }
38
+ async function askUserQuestion(question, choices) {
39
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
40
+ return new Promise((resolve) => {
41
+ let prompt = `\n${chalk.cyan("?")} ${chalk.bold(question)}\n`;
42
+ if (choices && choices.length > 0) {
43
+ choices.forEach((c, i) => {
44
+ prompt += ` ${chalk.dim(`${i + 1}.`)} ${c}\n`;
45
+ });
46
+ prompt += chalk.dim(`Enter number (1-${choices.length}) or type your own answer: `);
47
+ }
48
+ else {
49
+ prompt += chalk.dim("> ");
50
+ }
51
+ rl.question(prompt, (answer) => {
52
+ rl.close();
53
+ const trimmed = answer.trim();
54
+ if (choices && choices.length > 0) {
55
+ const idx = parseInt(trimmed, 10) - 1;
56
+ if (idx >= 0 && idx < choices.length) {
57
+ resolve(choices[idx]);
58
+ return;
59
+ }
60
+ }
61
+ resolve(trimmed || "(no answer provided)");
62
+ });
63
+ });
64
+ }
65
+ function makeToolDefs() {
66
+ return [
67
+ {
68
+ name: "ask_user_question",
69
+ description: "Ask the user a clarifying question. Use when context is needed before diagnosing — e.g., a resource doesn't match any known deployment, or you need to know if an issue is intentional. Provide `choices` for multiple-choice questions, or omit for open-ended input.",
70
+ input_schema: {
71
+ type: "object",
72
+ properties: {
73
+ question: { type: "string", description: "The question to ask the user" },
74
+ choices: {
75
+ type: "array",
76
+ items: { type: "string" },
77
+ description: "Optional list of choices. If provided, the user picks by number or types a free-form answer.",
78
+ },
79
+ },
80
+ required: ["question"],
81
+ },
82
+ },
83
+ {
84
+ name: "get_logs",
85
+ description: "Get logs from a pod. Use to investigate errors, crashes, OOM kills.",
86
+ input_schema: zodToJsonSchema(kubectlGetLogsSchema),
87
+ },
88
+ {
89
+ name: "describe_resource",
90
+ description: "Describe a Kubernetes resource (pod, deployment, service, node). Shows events, conditions, configuration.",
91
+ input_schema: zodToJsonSchema(kubectlDescribeSchema),
92
+ },
93
+ {
94
+ name: "restart_pod",
95
+ description: "Delete a pod. If the pod is owned by a Deployment or DaemonSet it will be recreated; orphan pods are permanently deleted. Requires user approval.",
96
+ input_schema: zodToJsonSchema(restartPodSchema),
97
+ },
98
+ {
99
+ name: "rollout_restart",
100
+ description: "Rollout restart a deployment. Requires user approval.",
101
+ input_schema: zodToJsonSchema(rolloutRestartSchema),
102
+ },
103
+ {
104
+ name: "scale_deployment",
105
+ description: "Scale a deployment to a specific number of replicas. Scaling to 0 requires approval.",
106
+ input_schema: zodToJsonSchema(scaleDeploymentSchema),
107
+ },
108
+ {
109
+ name: "get_events",
110
+ description: "Get recent events in a namespace. Useful for understanding what happened.",
111
+ input_schema: zodToJsonSchema(getEventsSchema),
112
+ },
113
+ {
114
+ name: "set_resources",
115
+ description: "Change CPU/memory requests and limits on a deployment. Triggers a rolling restart. Requires user approval.",
116
+ input_schema: zodToJsonSchema(setResourcesSchema),
117
+ },
118
+ ];
119
+ }
120
+ function makeToolExecutors(noInteractive, onQuestion) {
121
+ return {
122
+ ask_user_question: async (input) => {
123
+ const { question, choices } = input;
124
+ // Always broadcast to notification channels so remote users see the question
125
+ await onQuestion?.(question, choices);
126
+ if (noInteractive) {
127
+ return `[non-interactive mode] Unable to ask: "${question}" — proceed with best judgement using available data only.`;
128
+ }
129
+ return askUserQuestion(question, choices);
130
+ },
131
+ get_logs: (input, ctx) => getLogs(input, ctx),
132
+ describe_resource: (input, ctx) => describeResource(input, ctx),
133
+ restart_pod: (input, ctx) => restartPod(input, ctx),
134
+ rollout_restart: (input, ctx) => rolloutRestart(input, ctx),
135
+ scale_deployment: (input, ctx) => scaleDeployment(input, ctx),
136
+ get_events: (input, ctx) => getEvents(input, ctx),
137
+ set_resources: (input, ctx) => setResources(input, ctx),
138
+ };
139
+ }
140
+ export async function diagnose(issues, kbDir, clusterContext, options) {
141
+ const systemPrompt = buildSystemPrompt(kbDir);
142
+ const tools = makeToolDefs();
143
+ const issuesSummary = issues
144
+ .map((i) => `[${i.severity}] ${i.kind}: ${i.message}`)
145
+ .join("\n");
146
+ const userMessage = `The following issues were detected in the cluster:
147
+
148
+ ${issuesSummary}
149
+
150
+ Please:
151
+ 1. Investigate using the available tools (get logs, describe resources, check events)
152
+ 2. Determine the root cause
153
+ 3. Propose a fix
154
+ 4. Define a verification contract — what should be true after the fix is applied
155
+
156
+ If the fix is a safe action (restart, scale up), apply it directly.
157
+ If it requires a risky action (rollback, delete, scale to zero), propose it but do not apply.`;
158
+ const messages = [
159
+ { role: "user", content: userMessage },
160
+ ];
161
+ const toolExecutors = makeToolExecutors(options?.noInteractive ?? false, options?.onQuestion);
162
+ const mutatingTools = new Set(["restart_pod", "rollout_restart", "scale_deployment", "set_resources"]);
163
+ // Merge hardcoded read-only safe tools with user-configured safe actions
164
+ const effectiveSafeActions = new Set([
165
+ ...["get_logs", "get_events", "describe_resource", "ask_user_question"],
166
+ ...(options?.safeActions ?? []),
167
+ ]);
168
+ let appliedAction;
169
+ const model = selectModel(issues);
170
+ dbg("model", `selected ${model} for ${issues.length} issue(s): ${issues.map(i => i.kind).join(", ")}`);
171
+ // Agentic loop
172
+ let lastResponse;
173
+ for (let turn = 0; turn < 10; turn++) {
174
+ const params = {
175
+ model,
176
+ max_tokens: 16000,
177
+ system: systemPrompt,
178
+ tools,
179
+ messages,
180
+ };
181
+ // adaptive thinking only supported on Sonnet+, not Haiku
182
+ if (model === SONNET) {
183
+ params.thinking = { type: "adaptive" };
184
+ }
185
+ const response = await createMessage(params);
186
+ lastResponse = response;
187
+ if (response.stop_reason === "end_turn")
188
+ break;
189
+ if (response.stop_reason === "tool_use") {
190
+ messages.push({ role: "assistant", content: response.content });
191
+ const toolResults = [];
192
+ for (const block of response.content) {
193
+ if (block.type === "tool_use") {
194
+ const executor = toolExecutors[block.name];
195
+ if (!executor) {
196
+ toolResults.push({
197
+ type: "tool_result",
198
+ tool_use_id: block.id,
199
+ content: `Unknown tool: ${block.name}`,
200
+ is_error: true,
201
+ });
202
+ continue;
203
+ }
204
+ // Check if action requires approval
205
+ if (!effectiveSafeActions.has(block.name) && options?.onApproval) {
206
+ const approved = await options.onApproval(block.name, block.input);
207
+ if (!approved) {
208
+ toolResults.push({
209
+ type: "tool_result",
210
+ tool_use_id: block.id,
211
+ content: "Action denied by user. Propose an alternative.",
212
+ });
213
+ continue;
214
+ }
215
+ }
216
+ try {
217
+ const result = await executor(block.input, { context: clusterContext });
218
+ // Track mutating actions for the verifier
219
+ if (mutatingTools.has(block.name)) {
220
+ appliedAction = {
221
+ name: block.name,
222
+ safe: effectiveSafeActions.has(block.name),
223
+ params: block.input,
224
+ };
225
+ }
226
+ toolResults.push({
227
+ type: "tool_result",
228
+ tool_use_id: block.id,
229
+ content: truncateOutput(result),
230
+ });
231
+ }
232
+ catch (err) {
233
+ toolResults.push({
234
+ type: "tool_result",
235
+ tool_use_id: block.id,
236
+ content: `Error: ${err.message}`,
237
+ is_error: true,
238
+ });
239
+ }
240
+ }
241
+ }
242
+ messages.push({ role: "user", content: toolResults });
243
+ }
244
+ }
245
+ // Extract final text
246
+ const analysis = lastResponse?.content
247
+ .filter((b) => b.type === "text")
248
+ .map((b) => b.text)
249
+ .join("\n") ?? "No analysis produced.";
250
+ return { analysis, action: appliedAction };
251
+ }
@@ -0,0 +1,119 @@
1
+ import { z } from "zod";
2
+ export declare const safeActions: Set<string>;
3
+ export declare const riskyActions: Set<string>;
4
+ export declare function isActionSafe(action: string): boolean;
5
+ export declare const kubectlGetLogsSchema: z.ZodObject<{
6
+ namespace: z.ZodString;
7
+ pod: z.ZodString;
8
+ container: z.ZodOptional<z.ZodString>;
9
+ tail: z.ZodDefault<z.ZodNumber>;
10
+ }, "strip", z.ZodTypeAny, {
11
+ namespace: string;
12
+ pod: string;
13
+ tail: number;
14
+ container?: string | undefined;
15
+ }, {
16
+ namespace: string;
17
+ pod: string;
18
+ container?: string | undefined;
19
+ tail?: number | undefined;
20
+ }>;
21
+ export declare const kubectlDescribeSchema: z.ZodObject<{
22
+ namespace: z.ZodString;
23
+ resource_type: z.ZodEnum<["pod", "deployment", "service", "statefulset", "daemonset", "replicaset", "job", "cronjob", "ingress", "node", "persistentvolumeclaim", "configmap", "endpoints"]>;
24
+ name: z.ZodString;
25
+ }, "strip", z.ZodTypeAny, {
26
+ name: string;
27
+ namespace: string;
28
+ resource_type: "pod" | "deployment" | "service" | "statefulset" | "daemonset" | "replicaset" | "job" | "cronjob" | "ingress" | "node" | "persistentvolumeclaim" | "configmap" | "endpoints";
29
+ }, {
30
+ name: string;
31
+ namespace: string;
32
+ resource_type: "pod" | "deployment" | "service" | "statefulset" | "daemonset" | "replicaset" | "job" | "cronjob" | "ingress" | "node" | "persistentvolumeclaim" | "configmap" | "endpoints";
33
+ }>;
34
+ export declare const restartPodSchema: z.ZodObject<{
35
+ namespace: z.ZodString;
36
+ pod: z.ZodString;
37
+ }, "strip", z.ZodTypeAny, {
38
+ namespace: string;
39
+ pod: string;
40
+ }, {
41
+ namespace: string;
42
+ pod: string;
43
+ }>;
44
+ export declare const rolloutRestartSchema: z.ZodObject<{
45
+ namespace: z.ZodString;
46
+ deployment: z.ZodString;
47
+ }, "strip", z.ZodTypeAny, {
48
+ namespace: string;
49
+ deployment: string;
50
+ }, {
51
+ namespace: string;
52
+ deployment: string;
53
+ }>;
54
+ export declare const scaleDeploymentSchema: z.ZodObject<{
55
+ namespace: z.ZodString;
56
+ deployment: z.ZodString;
57
+ replicas: z.ZodNumber;
58
+ }, "strip", z.ZodTypeAny, {
59
+ namespace: string;
60
+ deployment: string;
61
+ replicas: number;
62
+ }, {
63
+ namespace: string;
64
+ deployment: string;
65
+ replicas: number;
66
+ }>;
67
+ export declare const getEventsSchema: z.ZodObject<{
68
+ namespace: z.ZodString;
69
+ }, "strip", z.ZodTypeAny, {
70
+ namespace: string;
71
+ }, {
72
+ namespace: string;
73
+ }>;
74
+ export declare const setResourcesSchema: z.ZodObject<{
75
+ namespace: z.ZodString;
76
+ deployment: z.ZodString;
77
+ container: z.ZodOptional<z.ZodString>;
78
+ memory_request: z.ZodOptional<z.ZodString>;
79
+ memory_limit: z.ZodOptional<z.ZodString>;
80
+ cpu_request: z.ZodOptional<z.ZodString>;
81
+ cpu_limit: z.ZodOptional<z.ZodString>;
82
+ }, "strip", z.ZodTypeAny, {
83
+ namespace: string;
84
+ deployment: string;
85
+ container?: string | undefined;
86
+ memory_request?: string | undefined;
87
+ memory_limit?: string | undefined;
88
+ cpu_request?: string | undefined;
89
+ cpu_limit?: string | undefined;
90
+ }, {
91
+ namespace: string;
92
+ deployment: string;
93
+ container?: string | undefined;
94
+ memory_request?: string | undefined;
95
+ memory_limit?: string | undefined;
96
+ cpu_request?: string | undefined;
97
+ cpu_limit?: string | undefined;
98
+ }>;
99
+ export declare function getLogs(input: z.infer<typeof kubectlGetLogsSchema>, context?: {
100
+ context?: string;
101
+ }): Promise<string>;
102
+ export declare function describeResource(input: z.infer<typeof kubectlDescribeSchema>, context?: {
103
+ context?: string;
104
+ }): Promise<string>;
105
+ export declare function restartPod(input: z.infer<typeof restartPodSchema>, context?: {
106
+ context?: string;
107
+ }): Promise<string>;
108
+ export declare function rolloutRestart(input: z.infer<typeof rolloutRestartSchema>, context?: {
109
+ context?: string;
110
+ }): Promise<string>;
111
+ export declare function scaleDeployment(input: z.infer<typeof scaleDeploymentSchema>, context?: {
112
+ context?: string;
113
+ }): Promise<string>;
114
+ export declare function getEvents(input: z.infer<typeof getEventsSchema>, context?: {
115
+ context?: string;
116
+ }): Promise<string>;
117
+ export declare function setResources(input: z.infer<typeof setResourcesSchema>, context?: {
118
+ context?: string;
119
+ }): Promise<string>;
@@ -0,0 +1,108 @@
1
+ import { z } from "zod";
2
+ import { kubectl } from "../kubectl.js";
3
+ export const safeActions = new Set([
4
+ "get_logs",
5
+ "get_events",
6
+ "describe_resource",
7
+ "ask_user_question",
8
+ ]);
9
+ export const riskyActions = new Set([
10
+ "scale_deployment",
11
+ "set_resources",
12
+ "rollback_deployment",
13
+ "scale_to_zero",
14
+ "delete_resource",
15
+ "edit_configmap",
16
+ "apply_manifest",
17
+ ]);
18
+ export function isActionSafe(action) {
19
+ return safeActions.has(action);
20
+ }
21
+ // Tool input schemas for Claude API
22
+ export const kubectlGetLogsSchema = z.object({
23
+ namespace: z.string().describe("Kubernetes namespace"),
24
+ pod: z.string().describe("Pod name"),
25
+ container: z.string().optional().describe("Container name (if multi-container pod)"),
26
+ tail: z.number().default(50).describe("Number of log lines to return"),
27
+ });
28
+ const ALLOWED_RESOURCE_TYPES = [
29
+ "pod", "deployment", "service", "statefulset", "daemonset",
30
+ "replicaset", "job", "cronjob", "ingress", "node",
31
+ "persistentvolumeclaim", "configmap", "endpoints",
32
+ ];
33
+ export const kubectlDescribeSchema = z.object({
34
+ namespace: z.string().describe("Kubernetes namespace"),
35
+ resource_type: z.enum(ALLOWED_RESOURCE_TYPES).describe("Resource type to describe"),
36
+ name: z.string().describe("Resource name"),
37
+ });
38
+ export const restartPodSchema = z.object({
39
+ namespace: z.string().describe("Kubernetes namespace"),
40
+ pod: z.string().describe("Pod name to delete (deployment will recreate it)"),
41
+ });
42
+ export const rolloutRestartSchema = z.object({
43
+ namespace: z.string().describe("Kubernetes namespace"),
44
+ deployment: z.string().describe("Deployment name to rollout restart"),
45
+ });
46
+ export const scaleDeploymentSchema = z.object({
47
+ namespace: z.string().describe("Kubernetes namespace"),
48
+ deployment: z.string().describe("Deployment name"),
49
+ replicas: z.number().min(0).max(20).describe("Target replica count"),
50
+ });
51
+ export const getEventsSchema = z.object({
52
+ namespace: z.string().describe("Kubernetes namespace"),
53
+ });
54
+ export const setResourcesSchema = z.object({
55
+ namespace: z.string().describe("Kubernetes namespace"),
56
+ deployment: z.string().describe("Deployment name"),
57
+ container: z.string().optional().describe("Container name (omit for single-container pods)"),
58
+ memory_request: z.string().optional().describe("Memory request e.g. '128Mi'"),
59
+ memory_limit: z.string().optional().describe("Memory limit e.g. '512Mi'"),
60
+ cpu_request: z.string().optional().describe("CPU request e.g. '100m'"),
61
+ cpu_limit: z.string().optional().describe("CPU limit e.g. '500m'"),
62
+ });
63
+ // Tool implementations
64
+ export async function getLogs(input, context) {
65
+ const args = ["logs", input.pod, "-n", input.namespace, "--tail", String(input.tail)];
66
+ if (input.container)
67
+ args.push("-c", input.container);
68
+ return kubectl(args, { context: context?.context });
69
+ }
70
+ export async function describeResource(input, context) {
71
+ return kubectl(["describe", input.resource_type, input.name, "-n", input.namespace], { context: context?.context });
72
+ }
73
+ export async function restartPod(input, context) {
74
+ await kubectl(["delete", "pod", input.pod, "-n", input.namespace], { context: context?.context });
75
+ return `Pod ${input.pod} deleted. Deployment will recreate it.`;
76
+ }
77
+ export async function rolloutRestart(input, context) {
78
+ await kubectl(["rollout", "restart", `deployment/${input.deployment}`, "-n", input.namespace], { context: context?.context });
79
+ return `Rollout restart initiated for deployment/${input.deployment}`;
80
+ }
81
+ export async function scaleDeployment(input, context) {
82
+ await kubectl(["scale", `deployment/${input.deployment}`, "--replicas", String(input.replicas), "-n", input.namespace], { context: context?.context });
83
+ return `Scaled deployment/${input.deployment} to ${input.replicas} replicas`;
84
+ }
85
+ export async function getEvents(input, context) {
86
+ return kubectl(["get", "events", "-n", input.namespace, "--sort-by=.lastTimestamp"], { context: context?.context });
87
+ }
88
+ export async function setResources(input, context) {
89
+ const args = ["set", "resources", `deployment/${input.deployment}`, "-n", input.namespace];
90
+ if (input.container)
91
+ args.push("-c", input.container);
92
+ const requests = [];
93
+ if (input.memory_request)
94
+ requests.push(`memory=${input.memory_request}`);
95
+ if (input.cpu_request)
96
+ requests.push(`cpu=${input.cpu_request}`);
97
+ if (requests.length)
98
+ args.push(`--requests=${requests.join(",")}`);
99
+ const limits = [];
100
+ if (input.memory_limit)
101
+ limits.push(`memory=${input.memory_limit}`);
102
+ if (input.cpu_limit)
103
+ limits.push(`cpu=${input.cpu_limit}`);
104
+ if (limits.length)
105
+ args.push(`--limits=${limits.join(",")}`);
106
+ await kubectl(args, { context: context?.context });
107
+ return `Resources updated on deployment/${input.deployment}. Rolling restart triggered automatically.`;
108
+ }
@@ -0,0 +1 @@
1
+ export declare function buildSystemPrompt(kbDir: string): string;
@@ -0,0 +1,41 @@
1
+ import { existsSync, readFileSync, readdirSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ export function buildSystemPrompt(kbDir) {
4
+ if (!existsSync(kbDir)) {
5
+ return "No knowledge base found. Run `kubeagent onboard` to create one.";
6
+ }
7
+ const sections = [];
8
+ sections.push("You are KubeAgent, an AI-powered Kubernetes management assistant.", "You have access to the following knowledge about this cluster and its projects.", "Use this context to diagnose issues and propose fixes.", "");
9
+ // Load cluster.md
10
+ const clusterPath = join(kbDir, "cluster.md");
11
+ if (existsSync(clusterPath)) {
12
+ sections.push("## Cluster Information\n");
13
+ sections.push(readFileSync(clusterPath, "utf-8"));
14
+ sections.push("");
15
+ }
16
+ // Load project files
17
+ const projectsDir = join(kbDir, "projects");
18
+ if (existsSync(projectsDir)) {
19
+ const files = readdirSync(projectsDir).filter((f) => f.endsWith(".md"));
20
+ if (files.length > 0) {
21
+ sections.push("## Projects\n");
22
+ for (const file of files) {
23
+ sections.push(readFileSync(join(projectsDir, file), "utf-8"));
24
+ sections.push("");
25
+ }
26
+ }
27
+ }
28
+ // Load runbooks
29
+ const runbooksDir = join(kbDir, "runbooks");
30
+ if (existsSync(runbooksDir)) {
31
+ const files = readdirSync(runbooksDir).filter((f) => f.endsWith(".md"));
32
+ if (files.length > 0) {
33
+ sections.push("## Runbooks (learned remediation patterns)\n");
34
+ for (const file of files) {
35
+ sections.push(readFileSync(join(runbooksDir, file), "utf-8"));
36
+ sections.push("");
37
+ }
38
+ }
39
+ }
40
+ return sections.join("\n");
41
+ }
@@ -0,0 +1,11 @@
1
+ export declare function ensureKbDir(kbDir: string): void;
2
+ export declare function writeClusterKb(kbDir: string, content: string): void;
3
+ export declare function writeProjectKb(kbDir: string, projectName: string, content: string): void;
4
+ export declare function writeRunbook(kbDir: string, name: string, content: string): void;
5
+ /**
6
+ * Write a gave-up issue to the unresolved/ directory.
7
+ * Files are named by issue key so repeated give-ups overwrite rather than accumulate.
8
+ * The agent reads these as KB context on future runs.
9
+ */
10
+ export declare function writeUnresolved(kbDir: string, issueKey: string, content: string): void;
11
+ export declare function writeIncident(kbDir: string, date: string, name: string, content: string): void;
@@ -0,0 +1,36 @@
1
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ export function ensureKbDir(kbDir) {
4
+ for (const sub of ["", "projects", "runbooks", "incidents", "unresolved"]) {
5
+ const dir = join(kbDir, sub);
6
+ if (!existsSync(dir)) {
7
+ mkdirSync(dir, { recursive: true });
8
+ }
9
+ }
10
+ }
11
+ export function writeClusterKb(kbDir, content) {
12
+ ensureKbDir(kbDir);
13
+ writeFileSync(join(kbDir, "cluster.md"), content);
14
+ }
15
+ export function writeProjectKb(kbDir, projectName, content) {
16
+ ensureKbDir(kbDir);
17
+ writeFileSync(join(kbDir, "projects", `${projectName}.md`), content);
18
+ }
19
+ export function writeRunbook(kbDir, name, content) {
20
+ ensureKbDir(kbDir);
21
+ writeFileSync(join(kbDir, "runbooks", `${name}.md`), content);
22
+ }
23
+ /**
24
+ * Write a gave-up issue to the unresolved/ directory.
25
+ * Files are named by issue key so repeated give-ups overwrite rather than accumulate.
26
+ * The agent reads these as KB context on future runs.
27
+ */
28
+ export function writeUnresolved(kbDir, issueKey, content) {
29
+ ensureKbDir(kbDir);
30
+ const safeName = issueKey.replace(/[^a-z0-9_-]/gi, "_");
31
+ writeFileSync(join(kbDir, "unresolved", `${safeName}.md`), content);
32
+ }
33
+ export function writeIncident(kbDir, date, name, content) {
34
+ ensureKbDir(kbDir);
35
+ writeFileSync(join(kbDir, "incidents", `${date}-${name}.md`), content);
36
+ }
@@ -0,0 +1,7 @@
1
+ export interface KubeContext {
2
+ name: string;
3
+ cluster: string;
4
+ current: boolean;
5
+ }
6
+ export declare function listContexts(): Promise<KubeContext[]>;
7
+ export declare function pickContext(): Promise<string>;
@@ -0,0 +1,47 @@
1
+ import readline from "node:readline";
2
+ import chalk from "chalk";
3
+ import { kubectl } from "./kubectl.js";
4
+ export async function listContexts() {
5
+ const raw = await kubectl(["config", "get-contexts", "--no-headers", "-o", "name"]);
6
+ const names = raw.trim().split("\n").filter(Boolean);
7
+ const currentRaw = await kubectl(["config", "current-context"]).catch(() => "");
8
+ const current = currentRaw.trim();
9
+ return names.map((name) => ({ name, cluster: name, current: name === current }));
10
+ }
11
+ async function ask(question) {
12
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
13
+ return new Promise((resolve) => {
14
+ rl.question(chalk.cyan(`${question} `), (answer) => {
15
+ rl.close();
16
+ resolve(answer.trim());
17
+ });
18
+ });
19
+ }
20
+ export async function pickContext() {
21
+ let contexts;
22
+ try {
23
+ contexts = await listContexts();
24
+ }
25
+ catch {
26
+ return ask("Kubernetes context to use:");
27
+ }
28
+ if (contexts.length === 0) {
29
+ return ask("Kubernetes context to use:");
30
+ }
31
+ console.log(chalk.bold("\nAvailable Kubernetes contexts:\n"));
32
+ contexts.forEach((ctx, i) => {
33
+ const marker = ctx.current ? chalk.green(" (current)") : "";
34
+ console.log(` ${chalk.cyan(String(i + 1))}. ${ctx.name}${marker}`);
35
+ });
36
+ console.log();
37
+ const defaultIdx = contexts.findIndex((c) => c.current);
38
+ const defaultLabel = defaultIdx >= 0 ? ` [${defaultIdx + 1}]` : "";
39
+ const answer = await ask(`Select context${defaultLabel}:`);
40
+ if (!answer && defaultIdx >= 0)
41
+ return contexts[defaultIdx].name;
42
+ const num = parseInt(answer, 10);
43
+ if (!isNaN(num) && num >= 1 && num <= contexts.length) {
44
+ return contexts[num - 1].name;
45
+ }
46
+ return answer || contexts[defaultIdx]?.name || "default";
47
+ }
@@ -0,0 +1,13 @@
1
+ export declare class KubectlError extends Error {
2
+ readonly stderr?: string | undefined;
3
+ readonly exitCode?: number | undefined;
4
+ constructor(message: string, stderr?: string | undefined, exitCode?: number | undefined);
5
+ }
6
+ export declare function parseKubectlJson(raw: string): unknown;
7
+ export interface KubectlOptions {
8
+ context?: string;
9
+ namespace?: string;
10
+ timeout?: number;
11
+ }
12
+ export declare function kubectl(args: string[], options?: KubectlOptions): Promise<string>;
13
+ export declare function kubectlJson(args: string[], options?: KubectlOptions): Promise<unknown>;