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,8 @@
1
+ import type { Issue } from "../monitor/types.js";
2
+ import type { TelegramChannel } from "../config.js";
3
+ export declare function sendTelegramQuestion(channel: TelegramChannel, question: string, choices: string[] | undefined, clusterContext?: string): Promise<void>;
4
+ export declare function sendTelegram(issues: Issue[], channel: TelegramChannel, clusterContext?: string): Promise<void>;
5
+ export declare function testTelegramCredentials(botToken: string, chatId: string): Promise<{
6
+ ok: boolean;
7
+ error?: string;
8
+ }>;
@@ -0,0 +1,63 @@
1
+ const TELEGRAM_API = "https://api.telegram.org";
2
+ function formatTelegramMessage(issues, clusterContext) {
3
+ const ctx = clusterContext ? ` — \`${clusterContext}\`` : "";
4
+ const header = `⚠ *KubeAgent Alert*${ctx}`;
5
+ const lines = issues.map((i) => {
6
+ const icon = i.severity === "critical" ? "🔴" : "🟡";
7
+ const ns = i.namespace ? ` \`${i.namespace}\`` : "";
8
+ return `${icon} ${i.message}${ns}`;
9
+ });
10
+ const critical = issues.filter((i) => i.severity === "critical").length;
11
+ const warning = issues.filter((i) => i.severity === "warning").length;
12
+ const summary = [
13
+ critical ? `${critical} critical` : "",
14
+ warning ? `${warning} warning` : "",
15
+ ].filter(Boolean).join(", ");
16
+ return [header, summary, "", ...lines].join("\n");
17
+ }
18
+ async function postToTelegram(channel, text) {
19
+ const url = `${TELEGRAM_API}/bot${channel.bot_token}/sendMessage`;
20
+ try {
21
+ const res = await fetch(url, {
22
+ method: "POST",
23
+ headers: { "Content-Type": "application/json" },
24
+ body: JSON.stringify({ chat_id: channel.chat_id, text, parse_mode: "Markdown" }),
25
+ });
26
+ if (!res.ok) {
27
+ const body = await res.json().catch(() => ({}));
28
+ console.error(`Telegram error: ${body.description ?? res.statusText}`);
29
+ }
30
+ }
31
+ catch (err) {
32
+ console.error(`Telegram error: ${err.message}`);
33
+ }
34
+ }
35
+ export async function sendTelegramQuestion(channel, question, choices, clusterContext) {
36
+ const ctx = clusterContext ? ` — \`${clusterContext}\`` : "";
37
+ const choiceLines = choices?.map((c, i) => ` ${i + 1}. ${c}`) ?? [];
38
+ const body = choiceLines.length
39
+ ? `${question}\n\n${choiceLines.join("\n")}\n\n_Answer via terminal._`
40
+ : `${question}\n\n_Answer required via terminal._`;
41
+ await postToTelegram(channel, `❓ *KubeAgent needs input*${ctx}\n\n${body}`);
42
+ }
43
+ export async function sendTelegram(issues, channel, clusterContext) {
44
+ await postToTelegram(channel, formatTelegramMessage(issues, clusterContext));
45
+ }
46
+ export async function testTelegramCredentials(botToken, chatId) {
47
+ try {
48
+ const res = await fetch(`${TELEGRAM_API}/bot${botToken}/sendMessage`, {
49
+ method: "POST",
50
+ headers: { "Content-Type": "application/json" },
51
+ body: JSON.stringify({
52
+ chat_id: chatId,
53
+ text: "✅ *KubeAgent* connected successfully\\! You'll receive cluster alerts here\\.",
54
+ parse_mode: "MarkdownV2",
55
+ }),
56
+ });
57
+ const body = await res.json();
58
+ return body.ok ? { ok: true } : { ok: false, error: body.description };
59
+ }
60
+ catch (err) {
61
+ return { ok: false, error: err.message };
62
+ }
63
+ }
@@ -0,0 +1,3 @@
1
+ import type { Issue } from "../monitor/types.js";
2
+ import type { WebhookConfig } from "../config.js";
3
+ export declare function sendWebhook(issues: Issue[], config: WebhookConfig): Promise<void>;
@@ -0,0 +1,49 @@
1
+ function formatMessage(issues) {
2
+ const header = `**KubeAgent Alert** — ${issues.length} issue(s) detected\n`;
3
+ const body = issues
4
+ .map((i) => {
5
+ const icon = i.severity === "critical" ? "\u{1F534}" : "\u{1F7E1}";
6
+ return `${icon} **${i.kind}**: ${i.message}`;
7
+ })
8
+ .join("\n");
9
+ return header + body;
10
+ }
11
+ export async function sendWebhook(issues, config) {
12
+ const message = formatMessage(issues);
13
+ let payload;
14
+ switch (config.type) {
15
+ case "discord":
16
+ payload = { content: message };
17
+ break;
18
+ case "slack":
19
+ case "mattermost":
20
+ default:
21
+ payload = { text: message };
22
+ break;
23
+ }
24
+ // Validate URL to prevent SSRF
25
+ try {
26
+ const url = new URL(config.url);
27
+ if (!["https:", "http:"].includes(url.protocol)) {
28
+ console.error(`Webhook error: unsupported protocol ${url.protocol}`);
29
+ return;
30
+ }
31
+ }
32
+ catch {
33
+ console.error(`Webhook error: invalid URL ${config.url}`);
34
+ return;
35
+ }
36
+ try {
37
+ const response = await fetch(config.url, {
38
+ method: "POST",
39
+ headers: { "Content-Type": "application/json" },
40
+ body: JSON.stringify(payload),
41
+ });
42
+ if (!response.ok) {
43
+ console.error(`Webhook failed: ${response.status} ${response.statusText}`);
44
+ }
45
+ }
46
+ catch (err) {
47
+ console.error(`Webhook error: ${err.message}`);
48
+ }
49
+ }
@@ -0,0 +1,42 @@
1
+ import { type KubectlOptions } from "../kubectl.js";
2
+ export interface NodeInfo {
3
+ name: string;
4
+ roles: string[];
5
+ version: string;
6
+ os: string;
7
+ }
8
+ export interface DeploymentInfo {
9
+ name: string;
10
+ namespace: string;
11
+ replicas: number;
12
+ image: string;
13
+ }
14
+ export interface StatefulSetInfo {
15
+ name: string;
16
+ namespace: string;
17
+ replicas: number;
18
+ image: string;
19
+ }
20
+ export interface ServiceInfo {
21
+ name: string;
22
+ namespace: string;
23
+ type: string;
24
+ ports: string;
25
+ }
26
+ export interface IngressInfo {
27
+ name: string;
28
+ namespace: string;
29
+ hosts: string[];
30
+ tls: boolean;
31
+ }
32
+ export interface ClusterInfo {
33
+ context: string;
34
+ nodes: NodeInfo[];
35
+ namespaces: string[];
36
+ deployments: DeploymentInfo[];
37
+ statefulsets: StatefulSetInfo[];
38
+ services: ServiceInfo[];
39
+ ingresses: IngressInfo[];
40
+ }
41
+ export declare function scanCluster(options: KubectlOptions): Promise<ClusterInfo>;
42
+ export declare function formatClusterMarkdown(info: ClusterInfo): string;
@@ -0,0 +1,103 @@
1
+ import { kubectlJson } from "../kubectl.js";
2
+ export async function scanCluster(options) {
3
+ const context = options.context ?? "default";
4
+ // Nodes
5
+ const nodesRaw = (await kubectlJson(["get", "nodes"], options));
6
+ const nodes = nodesRaw.items.map((n) => ({
7
+ name: n.metadata.name,
8
+ roles: Object.keys(n.metadata.labels)
9
+ .filter((l) => l.startsWith("node-role.kubernetes.io/"))
10
+ .map((l) => l.replace("node-role.kubernetes.io/", "")),
11
+ version: n.status.nodeInfo.kubeletVersion,
12
+ os: n.status.nodeInfo.osImage,
13
+ }));
14
+ // Namespaces
15
+ const nsRaw = (await kubectlJson(["get", "namespaces"], options));
16
+ const namespaces = nsRaw.items.map((n) => n.metadata.name);
17
+ // Deployments
18
+ const deplRaw = (await kubectlJson(["get", "deployments", "--all-namespaces"], options));
19
+ const deployments = deplRaw.items.map((d) => ({
20
+ name: d.metadata.name,
21
+ namespace: d.metadata.namespace,
22
+ replicas: d.spec.replicas,
23
+ image: d.spec.template.spec.containers[0]?.image ?? "unknown",
24
+ }));
25
+ // StatefulSets
26
+ const ssRaw = (await kubectlJson(["get", "statefulsets", "--all-namespaces"], options));
27
+ const statefulsets = ssRaw.items.map((s) => ({
28
+ name: s.metadata.name,
29
+ namespace: s.metadata.namespace,
30
+ replicas: s.spec.replicas,
31
+ image: s.spec.template.spec.containers[0]?.image ?? "unknown",
32
+ }));
33
+ // Services
34
+ const svcRaw = (await kubectlJson(["get", "services", "--all-namespaces"], options));
35
+ const services = svcRaw.items.map((s) => ({
36
+ name: s.metadata.name,
37
+ namespace: s.metadata.namespace,
38
+ type: s.spec.type,
39
+ ports: (s.spec.ports ?? []).map((p) => `${p.port}/${p.protocol}`).join(", "),
40
+ }));
41
+ // Ingresses
42
+ const ingRaw = (await kubectlJson(["get", "ingresses", "--all-namespaces"], options));
43
+ const ingresses = ingRaw.items.map((i) => ({
44
+ name: i.metadata.name,
45
+ namespace: i.metadata.namespace,
46
+ hosts: (i.spec.rules ?? []).map((r) => r.host),
47
+ tls: (i.spec.tls?.length ?? 0) > 0,
48
+ }));
49
+ return { context, nodes, namespaces, deployments, statefulsets, services, ingresses };
50
+ }
51
+ export function formatClusterMarkdown(info) {
52
+ const lines = [];
53
+ lines.push(`# Cluster: ${info.context}`, "");
54
+ // Nodes
55
+ lines.push("## Nodes", "");
56
+ lines.push("| Name | Roles | Version | OS |");
57
+ lines.push("|------|-------|---------|-----|");
58
+ for (const n of info.nodes) {
59
+ lines.push(`| ${n.name} | ${n.roles.join(", ")} | ${n.version} | ${n.os} |`);
60
+ }
61
+ lines.push("");
62
+ // Namespaces
63
+ lines.push("## Namespaces", "");
64
+ lines.push(info.namespaces.map((n) => `- \`${n}\``).join("\n"));
65
+ lines.push("");
66
+ // Deployments
67
+ lines.push("## Deployments", "");
68
+ lines.push("| Name | Namespace | Replicas | Image |");
69
+ lines.push("|------|-----------|----------|-------|");
70
+ for (const d of info.deployments) {
71
+ lines.push(`| ${d.name} | ${d.namespace} | ${d.replicas} | \`${d.image}\` |`);
72
+ }
73
+ lines.push("");
74
+ // StatefulSets
75
+ if (info.statefulsets.length > 0) {
76
+ lines.push("## StatefulSets", "");
77
+ lines.push("| Name | Namespace | Replicas | Image |");
78
+ lines.push("|------|-----------|----------|-------|");
79
+ for (const s of info.statefulsets) {
80
+ lines.push(`| ${s.name} | ${s.namespace} | ${s.replicas} | \`${s.image}\` |`);
81
+ }
82
+ lines.push("");
83
+ }
84
+ // Services
85
+ lines.push("## Services", "");
86
+ lines.push("| Name | Namespace | Type | Ports |");
87
+ lines.push("|------|-----------|------|-------|");
88
+ for (const s of info.services) {
89
+ lines.push(`| ${s.name} | ${s.namespace} | ${s.type} | ${s.ports} |`);
90
+ }
91
+ lines.push("");
92
+ // Ingresses
93
+ if (info.ingresses.length > 0) {
94
+ lines.push("## Ingresses", "");
95
+ lines.push("| Name | Namespace | Hosts | TLS |");
96
+ lines.push("|------|-----------|-------|-----|");
97
+ for (const i of info.ingresses) {
98
+ lines.push(`| ${i.name} | ${i.namespace} | ${i.hosts.join(", ")} | ${i.tls ? "Yes" : "No"} |`);
99
+ }
100
+ lines.push("");
101
+ }
102
+ return lines.join("\n");
103
+ }
@@ -0,0 +1,9 @@
1
+ export interface TechStack {
2
+ language: string;
3
+ framework: string;
4
+ runtimeVersion: string;
5
+ dependencies: string[];
6
+ buildTool: string;
7
+ }
8
+ export declare function detectTechStack(codePath: string): TechStack;
9
+ export declare function formatProjectMarkdown(projectName: string, codePath: string, stack: TechStack, extraNotes?: string): string;
@@ -0,0 +1,114 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ export function detectTechStack(codePath) {
4
+ const stack = {
5
+ language: "Unknown",
6
+ framework: "Unknown",
7
+ runtimeVersion: "Unknown",
8
+ dependencies: [],
9
+ buildTool: "Unknown",
10
+ };
11
+ // Check composer.json (PHP)
12
+ const composerPath = join(codePath, "composer.json");
13
+ if (existsSync(composerPath)) {
14
+ try {
15
+ const composer = JSON.parse(readFileSync(composerPath, "utf-8"));
16
+ stack.language = "PHP";
17
+ stack.runtimeVersion = composer.require?.php ?? "Unknown";
18
+ const deps = { ...composer.require, ...composer["require-dev"] };
19
+ stack.dependencies = Object.keys(deps).slice(0, 20);
20
+ if (deps["laravel/framework"])
21
+ stack.framework = "Laravel";
22
+ else if (deps["symfony/framework-bundle"])
23
+ stack.framework = "Symfony";
24
+ else if (deps["yiisoft/yii"])
25
+ stack.framework = "Yii";
26
+ stack.buildTool = "Composer";
27
+ }
28
+ catch { /* ignore parse errors */ }
29
+ return stack;
30
+ }
31
+ // Check package.json (Node.js)
32
+ const packagePath = join(codePath, "package.json");
33
+ if (existsSync(packagePath)) {
34
+ try {
35
+ const pkg = JSON.parse(readFileSync(packagePath, "utf-8"));
36
+ stack.language = "TypeScript/JavaScript";
37
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
38
+ stack.dependencies = Object.keys(deps).slice(0, 20);
39
+ if (deps.next)
40
+ stack.framework = "Next.js";
41
+ else if (deps.nuxt)
42
+ stack.framework = "Nuxt";
43
+ else if (deps.react)
44
+ stack.framework = "React";
45
+ else if (deps.vue)
46
+ stack.framework = "Vue";
47
+ else if (deps.express)
48
+ stack.framework = "Express";
49
+ if (existsSync(join(codePath, "tsconfig.json"))) {
50
+ stack.language = "TypeScript";
51
+ }
52
+ stack.runtimeVersion = pkg.engines?.node ?? "Unknown";
53
+ stack.buildTool = deps.vite ? "Vite" : deps.webpack ? "Webpack" : "npm";
54
+ }
55
+ catch { /* ignore */ }
56
+ return stack;
57
+ }
58
+ // Check go.mod (Go)
59
+ const goModPath = join(codePath, "go.mod");
60
+ if (existsSync(goModPath)) {
61
+ const goMod = readFileSync(goModPath, "utf-8");
62
+ stack.language = "Go";
63
+ stack.buildTool = "go";
64
+ const goVersionMatch = goMod.match(/^go\s+(\S+)/m);
65
+ if (goVersionMatch)
66
+ stack.runtimeVersion = goVersionMatch[1];
67
+ const moduleMatch = goMod.match(/^module\s+(\S+)/m);
68
+ if (moduleMatch)
69
+ stack.dependencies.push(moduleMatch[1]);
70
+ const requireMatches = goMod.matchAll(/^\s+(\S+)\s+\S+/gm);
71
+ for (const m of requireMatches) {
72
+ if (m[1] && !m[1].startsWith("//")) {
73
+ stack.dependencies.push(m[1]);
74
+ }
75
+ }
76
+ stack.dependencies = stack.dependencies.slice(0, 20);
77
+ stack.framework = "stdlib";
78
+ return stack;
79
+ }
80
+ return stack;
81
+ }
82
+ export function formatProjectMarkdown(projectName, codePath, stack, extraNotes) {
83
+ const lines = [];
84
+ lines.push(`# ${projectName}`, "");
85
+ lines.push(`**Code path:** \`${codePath}\``);
86
+ lines.push(`**Language:** ${stack.language}`);
87
+ lines.push(`**Framework:** ${stack.framework}`);
88
+ lines.push(`**Runtime version:** ${stack.runtimeVersion}`);
89
+ lines.push(`**Build tool:** ${stack.buildTool}`);
90
+ lines.push("");
91
+ if (stack.dependencies.length > 0) {
92
+ lines.push("## Key Dependencies", "");
93
+ for (const dep of stack.dependencies) {
94
+ lines.push(`- \`${dep}\``);
95
+ }
96
+ lines.push("");
97
+ }
98
+ // Check for Dockerfile
99
+ if (existsSync(join(codePath, "Dockerfile"))) {
100
+ const dockerfile = readFileSync(join(codePath, "Dockerfile"), "utf-8");
101
+ const fromMatch = dockerfile.match(/^FROM\s+(\S+)/m);
102
+ if (fromMatch) {
103
+ lines.push(`## Docker`, "");
104
+ lines.push(`Base image: \`${fromMatch[1]}\``);
105
+ lines.push("");
106
+ }
107
+ }
108
+ if (extraNotes) {
109
+ lines.push("## Notes", "");
110
+ lines.push(extraNotes);
111
+ lines.push("");
112
+ }
113
+ return lines.join("\n");
114
+ }
@@ -0,0 +1 @@
1
+ export declare function onboard(): Promise<void>;