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.
- package/LICENSE +72 -0
- package/README.md +154 -0
- package/dist/auth.d.ts +23 -0
- package/dist/auth.js +162 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +447 -0
- package/dist/config.d.ts +50 -0
- package/dist/config.js +79 -0
- package/dist/debug.d.ts +10 -0
- package/dist/debug.js +18 -0
- package/dist/diagnoser/index.d.ts +17 -0
- package/dist/diagnoser/index.js +251 -0
- package/dist/diagnoser/tools.d.ts +119 -0
- package/dist/diagnoser/tools.js +108 -0
- package/dist/kb/loader.d.ts +1 -0
- package/dist/kb/loader.js +41 -0
- package/dist/kb/writer.d.ts +11 -0
- package/dist/kb/writer.js +36 -0
- package/dist/kubectl-config.d.ts +7 -0
- package/dist/kubectl-config.js +47 -0
- package/dist/kubectl.d.ts +13 -0
- package/dist/kubectl.js +57 -0
- package/dist/monitor/checks.d.ts +71 -0
- package/dist/monitor/checks.js +167 -0
- package/dist/monitor/index.d.ts +7 -0
- package/dist/monitor/index.js +126 -0
- package/dist/monitor/types.d.ts +11 -0
- package/dist/monitor/types.js +1 -0
- package/dist/notify/index.d.ts +5 -0
- package/dist/notify/index.js +40 -0
- package/dist/notify/setup.d.ts +4 -0
- package/dist/notify/setup.js +88 -0
- package/dist/notify/slack.d.ts +4 -0
- package/dist/notify/slack.js +76 -0
- package/dist/notify/telegram.d.ts +8 -0
- package/dist/notify/telegram.js +63 -0
- package/dist/notify/webhook.d.ts +3 -0
- package/dist/notify/webhook.js +49 -0
- package/dist/onboard/cluster-scan.d.ts +42 -0
- package/dist/onboard/cluster-scan.js +103 -0
- package/dist/onboard/code-scan.d.ts +9 -0
- package/dist/onboard/code-scan.js +114 -0
- package/dist/onboard/index.d.ts +1 -0
- package/dist/onboard/index.js +328 -0
- package/dist/onboard/interview.d.ts +12 -0
- package/dist/onboard/interview.js +71 -0
- package/dist/onboard/project-matcher.d.ts +25 -0
- package/dist/onboard/project-matcher.js +149 -0
- package/dist/orchestrator.d.ts +3 -0
- package/dist/orchestrator.js +222 -0
- package/dist/proxy-client.d.ts +15 -0
- package/dist/proxy-client.js +72 -0
- package/dist/render.d.ts +5 -0
- package/dist/render.js +143 -0
- package/dist/verifier.d.ts +9 -0
- package/dist/verifier.js +17 -0
- package/package.json +39 -0
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { diagnose } from "./diagnoser/index.js";
|
|
3
|
+
import { verify } from "./verifier.js";
|
|
4
|
+
import { sendNotification } from "./notify/index.js";
|
|
5
|
+
import { writeIncident, writeUnresolved } from "./kb/writer.js";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { configDir } from "./config.js";
|
|
8
|
+
import { loadAuth } from "./auth.js";
|
|
9
|
+
import { reportIncident } from "./proxy-client.js";
|
|
10
|
+
import { renderMarkdown, sectionHeader, commandBox } from "./render.js";
|
|
11
|
+
import { broadcastQuestion } from "./notify/index.js";
|
|
12
|
+
import readline from "node:readline";
|
|
13
|
+
const MAX_ATTEMPTS = 3;
|
|
14
|
+
const issueHistory = new Map();
|
|
15
|
+
function issueKey(issue) {
|
|
16
|
+
return `${issue.kind}:${issue.namespace ?? ""}:${issue.resource ?? ""}`;
|
|
17
|
+
}
|
|
18
|
+
function getOrCreate(key) {
|
|
19
|
+
if (!issueHistory.has(key)) {
|
|
20
|
+
issueHistory.set(key, { attempts: 0, gaveUp: false, reason: "" });
|
|
21
|
+
}
|
|
22
|
+
return issueHistory.get(key);
|
|
23
|
+
}
|
|
24
|
+
function extractReason(analysis) {
|
|
25
|
+
const lines = analysis.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
26
|
+
const rcIdx = lines.findIndex((l) => /root cause/i.test(l));
|
|
27
|
+
const candidate = rcIdx >= 0 ? lines[rcIdx + 1] ?? lines[0] : lines[0];
|
|
28
|
+
return candidate?.replace(/^[#*>\-]+\s*/, "").slice(0, 120) ?? "Unknown";
|
|
29
|
+
}
|
|
30
|
+
function severityIcon(severity) {
|
|
31
|
+
switch (severity) {
|
|
32
|
+
case "critical": return chalk.red("✖");
|
|
33
|
+
case "warning": return chalk.yellow("▲");
|
|
34
|
+
default: return chalk.blue("ℹ");
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function severityLabel(severity) {
|
|
38
|
+
switch (severity) {
|
|
39
|
+
case "critical": return chalk.bgRed.white(" CRITICAL ");
|
|
40
|
+
case "warning": return chalk.bgYellow.black(" WARNING ");
|
|
41
|
+
default: return chalk.bgBlue.white(" INFO ");
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function printIssue(issue, prefix = " ") {
|
|
45
|
+
const icon = severityIcon(issue.severity);
|
|
46
|
+
const label = severityLabel(issue.severity);
|
|
47
|
+
const ns = issue.namespace ? chalk.dim(`[${issue.namespace}]`) : "";
|
|
48
|
+
console.log(`${prefix}${icon} ${issue.message} ${ns} ${label}`);
|
|
49
|
+
}
|
|
50
|
+
function formatApprovalCommand(action, params) {
|
|
51
|
+
const p = params;
|
|
52
|
+
switch (action) {
|
|
53
|
+
case "set_resources": {
|
|
54
|
+
const parts = ["kubectl set resources", `deployment/${p.deployment}`, "-n", p.namespace];
|
|
55
|
+
const req = [];
|
|
56
|
+
const lim = [];
|
|
57
|
+
if (p.memory_request)
|
|
58
|
+
req.push(`memory=${p.memory_request}`);
|
|
59
|
+
if (p.cpu_request)
|
|
60
|
+
req.push(`cpu=${p.cpu_request}`);
|
|
61
|
+
if (p.memory_limit)
|
|
62
|
+
lim.push(`memory=${p.memory_limit}`);
|
|
63
|
+
if (p.cpu_limit)
|
|
64
|
+
lim.push(`cpu=${p.cpu_limit}`);
|
|
65
|
+
if (p.container)
|
|
66
|
+
parts.push("-c", String(p.container));
|
|
67
|
+
if (req.length)
|
|
68
|
+
parts.push(`--requests=${req.join(",")}`);
|
|
69
|
+
if (lim.length)
|
|
70
|
+
parts.push(`--limits=${lim.join(",")}`);
|
|
71
|
+
return parts.join(" ");
|
|
72
|
+
}
|
|
73
|
+
case "scale_deployment":
|
|
74
|
+
return `kubectl scale deployment/${p.deployment} -n ${p.namespace} --replicas=${p.replicas}`;
|
|
75
|
+
case "rollout_restart":
|
|
76
|
+
return `kubectl rollout restart deployment/${p.deployment} -n ${p.namespace}`;
|
|
77
|
+
case "restart_pod":
|
|
78
|
+
return `kubectl delete pod ${p.pod} -n ${p.namespace}`;
|
|
79
|
+
default:
|
|
80
|
+
return `${action} ${JSON.stringify(params)}`;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
async function askApproval(action, params) {
|
|
84
|
+
const cmd = formatApprovalCommand(action, params);
|
|
85
|
+
console.log("\n" + chalk.yellow(" Action requires approval:"));
|
|
86
|
+
commandBox(cmd);
|
|
87
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
88
|
+
return new Promise((resolve) => {
|
|
89
|
+
rl.question(chalk.yellow(" Run this? [y/N] "), (answer) => {
|
|
90
|
+
rl.close();
|
|
91
|
+
resolve(answer.toLowerCase() === "y");
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
export async function handleIssues(issues, config, clusterContext, noInteractive = false) {
|
|
96
|
+
const kbDir = join(configDir(), "clusters", clusterContext ?? "default");
|
|
97
|
+
const dateStr = new Date().toISOString().slice(0, 10);
|
|
98
|
+
// ── Issues detected ──────────────────────────────────────
|
|
99
|
+
const critCount = issues.filter((i) => i.severity === "critical").length;
|
|
100
|
+
const warnCount = issues.filter((i) => i.severity === "warning").length;
|
|
101
|
+
const parts = [
|
|
102
|
+
critCount ? chalk.red(`${critCount} critical`) : "",
|
|
103
|
+
warnCount ? chalk.yellow(`${warnCount} warning`) : "",
|
|
104
|
+
].filter(Boolean).join(chalk.dim(", "));
|
|
105
|
+
sectionHeader(`${issues.length} issue${issues.length !== 1 ? "s" : ""} detected ${parts}`, chalk.red.bold);
|
|
106
|
+
const gaveUpIssues = issues.filter((i) => getOrCreate(issueKey(i)).gaveUp);
|
|
107
|
+
const actionableIssues = issues.filter((i) => !getOrCreate(issueKey(i)).gaveUp);
|
|
108
|
+
for (const issue of actionableIssues) {
|
|
109
|
+
printIssue(issue);
|
|
110
|
+
}
|
|
111
|
+
for (const issue of gaveUpIssues) {
|
|
112
|
+
const record = issueHistory.get(issueKey(issue));
|
|
113
|
+
printIssue(issue);
|
|
114
|
+
console.log(chalk.dim(` ↳ Skipped — gave up after ${MAX_ATTEMPTS} attempts: ${record.reason}`));
|
|
115
|
+
}
|
|
116
|
+
if (actionableIssues.length === 0)
|
|
117
|
+
return;
|
|
118
|
+
await sendNotification(actionableIssues, config, clusterContext);
|
|
119
|
+
// ── Diagnosing ────────────────────────────────────────────
|
|
120
|
+
sectionHeader("Diagnosing", chalk.blue.bold);
|
|
121
|
+
const result = await diagnose(actionableIssues, kbDir, clusterContext, {
|
|
122
|
+
autoFix: config.remediation.auto_fix,
|
|
123
|
+
safeActions: config.remediation.safe_actions,
|
|
124
|
+
noInteractive,
|
|
125
|
+
onApproval: noInteractive ? undefined : askApproval,
|
|
126
|
+
onQuestion: (question, choices) => broadcastQuestion(question, choices, config, clusterContext),
|
|
127
|
+
});
|
|
128
|
+
// ── Analysis ──────────────────────────────────────────────
|
|
129
|
+
sectionHeader("Analysis", chalk.green.bold);
|
|
130
|
+
console.log(renderMarkdown(result.analysis));
|
|
131
|
+
// Attempt tracking / gave-up logic
|
|
132
|
+
const reason = extractReason(result.analysis);
|
|
133
|
+
for (const issue of actionableIssues) {
|
|
134
|
+
const record = getOrCreate(issueKey(issue));
|
|
135
|
+
record.attempts += 1;
|
|
136
|
+
record.reason = reason;
|
|
137
|
+
if (record.attempts >= MAX_ATTEMPTS) {
|
|
138
|
+
record.gaveUp = true;
|
|
139
|
+
console.log("\n " + chalk.red("✖ Gave up") + chalk.dim(` on "${issue.message}" after ${MAX_ATTEMPTS} attempts`));
|
|
140
|
+
console.log(chalk.dim(` Reason: ${reason}`));
|
|
141
|
+
console.log(chalk.dim(` Logged: ${kbDir}/unresolved/`));
|
|
142
|
+
console.log(chalk.dim(" Restart the CLI or fix manually to retry."));
|
|
143
|
+
const key = issueKey(issue);
|
|
144
|
+
const content = [
|
|
145
|
+
`# Unresolved: ${issue.message}`,
|
|
146
|
+
"",
|
|
147
|
+
`**Issue key:** \`${key}\``,
|
|
148
|
+
`**First seen:** ${new Date().toISOString()}`,
|
|
149
|
+
`**Attempts:** ${MAX_ATTEMPTS}`,
|
|
150
|
+
`**Severity:** ${issue.severity}`,
|
|
151
|
+
"",
|
|
152
|
+
"## Why the agent gave up",
|
|
153
|
+
reason,
|
|
154
|
+
"",
|
|
155
|
+
"## Last full analysis",
|
|
156
|
+
result.analysis,
|
|
157
|
+
"",
|
|
158
|
+
"## TODO",
|
|
159
|
+
"- [ ] Investigate root cause manually",
|
|
160
|
+
"- [ ] Add a runbook to `runbooks/` with the fix steps",
|
|
161
|
+
"- [ ] Once fixed, delete this file so the agent re-diagnoses if it reappears",
|
|
162
|
+
].join("\n");
|
|
163
|
+
writeUnresolved(kbDir, key, content);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
// ── Verifying ─────────────────────────────────────────────
|
|
167
|
+
let verified = false;
|
|
168
|
+
if (result.action) {
|
|
169
|
+
sectionHeader("Verifying fix", chalk.blue.bold);
|
|
170
|
+
const originalKinds = actionableIssues.map((i) => i.kind);
|
|
171
|
+
for (let attempt = 0; attempt < config.remediation.max_retries; attempt++) {
|
|
172
|
+
const verification = await verify(originalKinds, { context: clusterContext }, config.remediation.cooldown * 1000);
|
|
173
|
+
if (verification.passed) {
|
|
174
|
+
console.log(" " + chalk.green("✔ Fix verified successfully"));
|
|
175
|
+
for (const issue of actionableIssues) {
|
|
176
|
+
issueHistory.delete(issueKey(issue));
|
|
177
|
+
}
|
|
178
|
+
verified = true;
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
console.log(" " + chalk.yellow(`▲ Attempt ${attempt + 1} failed: ${verification.message}`));
|
|
182
|
+
}
|
|
183
|
+
if (!verified) {
|
|
184
|
+
console.log(" " + chalk.red("✖ Fix could not be verified — manual intervention may be needed"));
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
// Log incident
|
|
188
|
+
const incidentName = actionableIssues[0].kind;
|
|
189
|
+
const incidentContent = [
|
|
190
|
+
`# Incident: ${actionableIssues.map((i) => i.kind).join(", ")}`,
|
|
191
|
+
"",
|
|
192
|
+
`**Date:** ${new Date().toISOString()}`,
|
|
193
|
+
`**Severity:** ${actionableIssues[0].severity}`,
|
|
194
|
+
"",
|
|
195
|
+
"## Issues",
|
|
196
|
+
...actionableIssues.map((i) => `- ${i.message}`),
|
|
197
|
+
"",
|
|
198
|
+
"## Analysis",
|
|
199
|
+
result.analysis,
|
|
200
|
+
].join("\n");
|
|
201
|
+
writeIncident(kbDir, dateStr, incidentName, incidentContent);
|
|
202
|
+
// Report to SaaS if authenticated
|
|
203
|
+
const auth = loadAuth();
|
|
204
|
+
if (auth?.apiKey) {
|
|
205
|
+
try {
|
|
206
|
+
await reportIncident(auth, {
|
|
207
|
+
clusterContext: clusterContext ?? "default",
|
|
208
|
+
issues: JSON.stringify(actionableIssues.map((i) => ({
|
|
209
|
+
kind: i.kind,
|
|
210
|
+
severity: i.severity,
|
|
211
|
+
message: i.message,
|
|
212
|
+
}))),
|
|
213
|
+
analysis: result.analysis,
|
|
214
|
+
actionTaken: result.action?.name,
|
|
215
|
+
verified: result.action ? verified : undefined,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
catch {
|
|
219
|
+
// Silent — don't break local flow if server is unreachable
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { AuthState } from "./auth.js";
|
|
2
|
+
export declare function proxyRequest(auth: AuthState, body: Record<string, unknown>): Promise<Record<string, unknown>>;
|
|
3
|
+
export declare function fetchSlackWebhook(auth: AuthState): Promise<{
|
|
4
|
+
connected: boolean;
|
|
5
|
+
webhookUrl?: string;
|
|
6
|
+
channelName?: string;
|
|
7
|
+
teamName?: string;
|
|
8
|
+
}>;
|
|
9
|
+
export declare function reportIncident(auth: AuthState, incident: {
|
|
10
|
+
clusterContext: string;
|
|
11
|
+
issues: string;
|
|
12
|
+
analysis: string;
|
|
13
|
+
actionTaken?: string;
|
|
14
|
+
verified?: boolean;
|
|
15
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { dbg } from "./debug.js";
|
|
2
|
+
const MAX_RETRIES = 3;
|
|
3
|
+
export async function proxyRequest(auth, body) {
|
|
4
|
+
const apiKey = auth.apiKey ?? auth.token;
|
|
5
|
+
const url = `${auth.serverUrl}/v1/messages`;
|
|
6
|
+
const keyPreview = apiKey ? `${apiKey.slice(0, 12)}...` : "(none)";
|
|
7
|
+
dbg("proxy", `POST ${url}`, {
|
|
8
|
+
keyPrefix: keyPreview,
|
|
9
|
+
model: body.model,
|
|
10
|
+
maxTokens: body.max_tokens,
|
|
11
|
+
});
|
|
12
|
+
let res;
|
|
13
|
+
let lastErr;
|
|
14
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
15
|
+
if (attempt > 0) {
|
|
16
|
+
const delay = 2 ** (attempt - 1) * 1000; // 1s, 2s, 4s
|
|
17
|
+
dbg("proxy", `retry ${attempt}/${MAX_RETRIES} after ${delay}ms`);
|
|
18
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
19
|
+
}
|
|
20
|
+
res = await fetch(url, {
|
|
21
|
+
method: "POST",
|
|
22
|
+
headers: {
|
|
23
|
+
"Content-Type": "application/json",
|
|
24
|
+
Authorization: `ApiKey ${apiKey}`,
|
|
25
|
+
},
|
|
26
|
+
body: JSON.stringify(body),
|
|
27
|
+
});
|
|
28
|
+
dbg("proxy", `response ${res.status} from ${url}`);
|
|
29
|
+
if (res.status !== 429 && res.status !== 503)
|
|
30
|
+
break;
|
|
31
|
+
dbg("proxy", `transient error ${res.status}, will retry`);
|
|
32
|
+
lastErr = res;
|
|
33
|
+
}
|
|
34
|
+
res = res;
|
|
35
|
+
if (!res.ok) {
|
|
36
|
+
const errBody = await res.json().catch(() => ({}));
|
|
37
|
+
const message = errBody.error ?? `HTTP ${res.status}`;
|
|
38
|
+
// Distinguish backend 401 from other errors for easier debugging
|
|
39
|
+
const hint = res.status === 401
|
|
40
|
+
? " — your API key may be expired or invalid. Run: kubeagent login --server <url>"
|
|
41
|
+
: "";
|
|
42
|
+
throw Object.assign(new Error(`KubeAgent proxy: ${message}${hint}`), {
|
|
43
|
+
status: res.status,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
return (await res.json());
|
|
47
|
+
}
|
|
48
|
+
export async function fetchSlackWebhook(auth) {
|
|
49
|
+
const apiKey = auth.apiKey ?? auth.token;
|
|
50
|
+
try {
|
|
51
|
+
const res = await fetch(`${auth.serverUrl}/slack/api/webhook`, {
|
|
52
|
+
headers: { Authorization: `ApiKey ${apiKey}` },
|
|
53
|
+
});
|
|
54
|
+
if (!res.ok)
|
|
55
|
+
return { connected: false };
|
|
56
|
+
return (await res.json());
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return { connected: false };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
export async function reportIncident(auth, incident) {
|
|
63
|
+
const apiKey = auth.apiKey ?? auth.token;
|
|
64
|
+
await fetch(`${auth.serverUrl}/incidents`, {
|
|
65
|
+
method: "POST",
|
|
66
|
+
headers: {
|
|
67
|
+
"Content-Type": "application/json",
|
|
68
|
+
Authorization: `ApiKey ${apiKey}`,
|
|
69
|
+
},
|
|
70
|
+
body: JSON.stringify(incident),
|
|
71
|
+
});
|
|
72
|
+
}
|
package/dist/render.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare function renderMarkdown(text: string): string;
|
|
2
|
+
/** Print a labelled section divider: ── Label ──────────── */
|
|
3
|
+
export declare function sectionHeader(label: string, color?: import("chalk").ChalkInstance): void;
|
|
4
|
+
/** Print a two-line command approval box. */
|
|
5
|
+
export declare function commandBox(cmd: string): void;
|
package/dist/render.js
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
function renderInline(text) {
|
|
3
|
+
return text
|
|
4
|
+
.replace(/\*\*(.*?)\*\*/g, (_, t) => chalk.bold(t))
|
|
5
|
+
.replace(/__(.*?)__/g, (_, t) => chalk.bold(t))
|
|
6
|
+
.replace(/`([^`]+)`/g, (_, t) => chalk.cyan(t))
|
|
7
|
+
.replace(/\*(.*?)\*/g, (_, t) => chalk.italic ? chalk.italic(t) : t);
|
|
8
|
+
}
|
|
9
|
+
function renderTable(tableLines) {
|
|
10
|
+
const dataLines = tableLines.filter((l) => !/^\s*\|[\s\-:|]+\|\s*$/.test(l));
|
|
11
|
+
if (dataLines.length === 0)
|
|
12
|
+
return "";
|
|
13
|
+
const rows = dataLines.map((l) => l.trim().replace(/^\||\|$/g, "").split("|").map((c) => c.trim()));
|
|
14
|
+
const cols = Math.max(...rows.map((r) => r.length));
|
|
15
|
+
const widths = Array.from({ length: cols }, () => 0);
|
|
16
|
+
for (const row of rows) {
|
|
17
|
+
for (let c = 0; c < row.length; c++) {
|
|
18
|
+
widths[c] = Math.max(widths[c], row[c].length);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
const result = [];
|
|
22
|
+
let isHeader = true;
|
|
23
|
+
for (const row of rows) {
|
|
24
|
+
const cells = row.map((cell, c) => {
|
|
25
|
+
const rendered = renderInline(cell);
|
|
26
|
+
const pad = widths[c] - cell.length;
|
|
27
|
+
const padded = rendered + " ".repeat(Math.max(0, pad));
|
|
28
|
+
return isHeader ? chalk.bold(padded) : padded;
|
|
29
|
+
});
|
|
30
|
+
result.push(" " + cells.join(" │ "));
|
|
31
|
+
if (isHeader) {
|
|
32
|
+
result.push(" " + widths.map((w) => "─".repeat(w + 2)).join("┼"));
|
|
33
|
+
isHeader = false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return result.join("\n");
|
|
37
|
+
}
|
|
38
|
+
export function renderMarkdown(text) {
|
|
39
|
+
const lines = text.split("\n");
|
|
40
|
+
const output = [];
|
|
41
|
+
let i = 0;
|
|
42
|
+
let inCodeBlock = false;
|
|
43
|
+
const codeLines = [];
|
|
44
|
+
while (i < lines.length) {
|
|
45
|
+
const line = lines[i];
|
|
46
|
+
// Code blocks
|
|
47
|
+
if (line.trim().startsWith("```")) {
|
|
48
|
+
if (!inCodeBlock) {
|
|
49
|
+
inCodeBlock = true;
|
|
50
|
+
codeLines.length = 0;
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
inCodeBlock = false;
|
|
54
|
+
output.push(codeLines.map((l) => " " + chalk.dim(l)).join("\n"));
|
|
55
|
+
}
|
|
56
|
+
i++;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (inCodeBlock) {
|
|
60
|
+
codeLines.push(line);
|
|
61
|
+
i++;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
// Tables
|
|
65
|
+
if (line.trim().startsWith("|")) {
|
|
66
|
+
const tableLines = [];
|
|
67
|
+
while (i < lines.length && lines[i].trim().startsWith("|")) {
|
|
68
|
+
tableLines.push(lines[i]);
|
|
69
|
+
i++;
|
|
70
|
+
}
|
|
71
|
+
output.push(renderTable(tableLines));
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
// Headings
|
|
75
|
+
if (line.startsWith("### ")) {
|
|
76
|
+
output.push("\n" + chalk.bold(line.slice(4)));
|
|
77
|
+
}
|
|
78
|
+
else if (line.startsWith("## ")) {
|
|
79
|
+
output.push("\n" + chalk.bold.underline(line.slice(3)));
|
|
80
|
+
}
|
|
81
|
+
else if (line.startsWith("# ")) {
|
|
82
|
+
output.push("\n" + chalk.bold.underline(line.slice(2)));
|
|
83
|
+
// Horizontal rule
|
|
84
|
+
}
|
|
85
|
+
else if (/^[-*]{3,}$/.test(line.trim())) {
|
|
86
|
+
output.push(chalk.dim("─".repeat(60)));
|
|
87
|
+
// Blockquote
|
|
88
|
+
}
|
|
89
|
+
else if (line.startsWith("> ")) {
|
|
90
|
+
output.push(chalk.dim("│ ") + chalk.italic(renderInline(line.slice(2))));
|
|
91
|
+
// Checkboxes
|
|
92
|
+
}
|
|
93
|
+
else if (/^(\s*)- \[x\] /i.test(line)) {
|
|
94
|
+
const indent = line.match(/^(\s*)/)?.[1] ?? "";
|
|
95
|
+
const content = line.replace(/^\s*- \[x\] /i, "");
|
|
96
|
+
output.push(indent + chalk.green("✔ ") + chalk.dim(renderInline(content)));
|
|
97
|
+
}
|
|
98
|
+
else if (/^(\s*)- \[ \] /.test(line)) {
|
|
99
|
+
const indent = line.match(/^(\s*)/)?.[1] ?? "";
|
|
100
|
+
const content = line.replace(/^\s*- \[ \] /, "");
|
|
101
|
+
output.push(indent + chalk.dim("○ ") + renderInline(content));
|
|
102
|
+
// Bullet lists
|
|
103
|
+
}
|
|
104
|
+
else if (/^(\s*)[-*] /.test(line)) {
|
|
105
|
+
const indent = line.match(/^(\s*)/)?.[1] ?? "";
|
|
106
|
+
const depth = Math.floor(indent.length / 2);
|
|
107
|
+
const bullet = depth === 0 ? chalk.dim("•") : chalk.dim("◦");
|
|
108
|
+
const content = line.replace(/^\s*[-*] /, "");
|
|
109
|
+
output.push(" " + " ".repeat(depth) + bullet + " " + renderInline(content));
|
|
110
|
+
// Numbered lists
|
|
111
|
+
}
|
|
112
|
+
else if (/^(\s*)\d+\. /.test(line)) {
|
|
113
|
+
const indent = line.match(/^(\s*)/)?.[1] ?? "";
|
|
114
|
+
const depth = Math.floor(indent.length / 2);
|
|
115
|
+
const num = line.match(/^\s*(\d+)\./)?.[1] ?? "";
|
|
116
|
+
const content = line.replace(/^\s*\d+\. /, "");
|
|
117
|
+
output.push(" " + " ".repeat(depth) + chalk.dim(`${num}.`) + " " + renderInline(content));
|
|
118
|
+
// Everything else
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
output.push(renderInline(line));
|
|
122
|
+
}
|
|
123
|
+
i++;
|
|
124
|
+
}
|
|
125
|
+
return output.join("\n");
|
|
126
|
+
}
|
|
127
|
+
/** Print a labelled section divider: ── Label ──────────── */
|
|
128
|
+
export function sectionHeader(label, color = chalk.bold) {
|
|
129
|
+
const width = 56;
|
|
130
|
+
const inner = ` ${label} `;
|
|
131
|
+
const dashes = "─".repeat(Math.max(0, width - inner.length));
|
|
132
|
+
console.log("\n" + color("──" + inner + dashes));
|
|
133
|
+
}
|
|
134
|
+
/** Print a two-line command approval box. */
|
|
135
|
+
export function commandBox(cmd) {
|
|
136
|
+
const maxWidth = Math.max(cmd.length + 4, 40);
|
|
137
|
+
const top = " ╭" + "─".repeat(maxWidth) + "╮";
|
|
138
|
+
const middle = " │ " + chalk.cyan(cmd) + " ".repeat(Math.max(0, maxWidth - cmd.length - 3)) + "│";
|
|
139
|
+
const bottom = " ╰" + "─".repeat(maxWidth) + "╯";
|
|
140
|
+
console.log(chalk.dim(top));
|
|
141
|
+
console.log(middle);
|
|
142
|
+
console.log(chalk.dim(bottom));
|
|
143
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Issue, IssueKind } from "./monitor/types.js";
|
|
2
|
+
import type { KubectlOptions } from "./kubectl.js";
|
|
3
|
+
export interface VerificationResult {
|
|
4
|
+
passed: boolean;
|
|
5
|
+
remainingIssues: Issue[];
|
|
6
|
+
message: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function evaluateResult(currentIssues: Issue[], originalIssueKinds: IssueKind[]): VerificationResult;
|
|
9
|
+
export declare function verify(originalIssueKinds: IssueKind[], kubectlOptions: KubectlOptions, cooldownMs?: number): Promise<VerificationResult>;
|
package/dist/verifier.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { runChecks } from "./monitor/index.js";
|
|
2
|
+
export function evaluateResult(currentIssues, originalIssueKinds) {
|
|
3
|
+
const remaining = currentIssues.filter((issue) => originalIssueKinds.includes(issue.kind));
|
|
4
|
+
return {
|
|
5
|
+
passed: remaining.length === 0,
|
|
6
|
+
remainingIssues: remaining,
|
|
7
|
+
message: remaining.length === 0
|
|
8
|
+
? "All original issues resolved."
|
|
9
|
+
: `${remaining.length} issue(s) still present: ${remaining.map((i) => i.message).join("; ")}`,
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
export async function verify(originalIssueKinds, kubectlOptions, cooldownMs = 30_000) {
|
|
13
|
+
// Wait for cooldown before checking
|
|
14
|
+
await new Promise((resolve) => setTimeout(resolve, cooldownMs));
|
|
15
|
+
const currentIssues = await runChecks(kubectlOptions);
|
|
16
|
+
return evaluateResult(currentIssues, originalIssueKinds);
|
|
17
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "kubeagent",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "AI-powered Kubernetes management CLI",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"kubeagent": "./dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"README.md",
|
|
12
|
+
"LICENSE"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsc",
|
|
16
|
+
"dev": "tsx src/cli.ts",
|
|
17
|
+
"test": "vitest run",
|
|
18
|
+
"test:watch": "vitest"
|
|
19
|
+
},
|
|
20
|
+
"engines": {
|
|
21
|
+
"node": ">=20"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@anthropic-ai/sdk": "^0.80.0",
|
|
25
|
+
"chalk": "^5.4.0",
|
|
26
|
+
"commander": "^13.0.0",
|
|
27
|
+
"js-yaml": "^4.1.0",
|
|
28
|
+
"ora": "^8.0.0",
|
|
29
|
+
"zod": "^3.24.0",
|
|
30
|
+
"zod-to-json-schema": "^3.25.2"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/js-yaml": "^4.0.9",
|
|
34
|
+
"@types/node": "^22.0.0",
|
|
35
|
+
"tsx": "^4.19.0",
|
|
36
|
+
"typescript": "^5.7.0",
|
|
37
|
+
"vitest": "^3.0.0"
|
|
38
|
+
}
|
|
39
|
+
}
|