kubeagent 0.1.24 → 0.1.26

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/dist/cli.js CHANGED
@@ -56,8 +56,9 @@ program
56
56
  program
57
57
  .command("onboard")
58
58
  .description("Scan cluster + codebases, interview user, generate knowledge base")
59
- .action(async () => {
60
- await onboard();
59
+ .option("-t, --timeout <seconds>", "Max seconds to wait for cluster scan (default: 120)", "120")
60
+ .action(async (opts) => {
61
+ await onboard({ timeout: parseInt(opts.timeout, 10) });
61
62
  });
62
63
  program
63
64
  .command("watch")
@@ -457,8 +458,12 @@ program
457
458
  const resetDate = new Date(balance.resetsAt).toLocaleDateString(undefined, { month: "short", day: "numeric", year: "numeric" });
458
459
  console.log(` Resets: ${resetDate}`);
459
460
  }
460
- // Low-balance warning: < 20% of monthly allocation remaining
461
- if (balance.monthlyTotal > 0 && balance.monthlyRemaining < balance.monthlyTotal * 0.2) {
461
+ // Exhausted balance warning
462
+ if (balance.totalRemaining === 0) {
463
+ console.log("\n" + chalk.red(" Balance exhausted.") + " Upgrade or buy credits at " + chalk.cyan(upgradeUrl));
464
+ // Low-balance warning: < 20% of monthly allocation remaining
465
+ }
466
+ else if (balance.monthlyTotal > 0 && balance.monthlyRemaining < balance.monthlyTotal * 0.2) {
462
467
  const pct = Math.round((balance.monthlyRemaining / balance.monthlyTotal) * 100);
463
468
  console.log("\n" + chalk.yellow(` ⚠ Low balance: ${pct}% of monthly tokens remaining.`));
464
469
  console.log(chalk.dim(` Upgrade or buy extra credits: `) + chalk.cyan(upgradeUrl));
@@ -38,5 +38,5 @@ export interface ClusterInfo {
38
38
  services: ServiceInfo[];
39
39
  ingresses: IngressInfo[];
40
40
  }
41
- export declare function scanCluster(options: KubectlOptions): Promise<ClusterInfo>;
41
+ export declare function scanCluster(options: KubectlOptions, onProgress?: (step: string) => void): Promise<ClusterInfo>;
42
42
  export declare function formatClusterMarkdown(info: ClusterInfo): string;
@@ -1,7 +1,8 @@
1
1
  import { kubectlJson } from "../kubectl.js";
2
- export async function scanCluster(options) {
2
+ export async function scanCluster(options, onProgress) {
3
3
  const context = options.context ?? "default";
4
4
  // Nodes
5
+ onProgress?.("nodes");
5
6
  const nodesRaw = (await kubectlJson(["get", "nodes"], options));
6
7
  const nodes = nodesRaw.items.map((n) => ({
7
8
  name: n.metadata.name,
@@ -12,9 +13,11 @@ export async function scanCluster(options) {
12
13
  os: n.status.nodeInfo.osImage,
13
14
  }));
14
15
  // Namespaces
16
+ onProgress?.("namespaces");
15
17
  const nsRaw = (await kubectlJson(["get", "namespaces"], options));
16
18
  const namespaces = nsRaw.items.map((n) => n.metadata.name);
17
19
  // Deployments
20
+ onProgress?.("deployments");
18
21
  const deplRaw = (await kubectlJson(["get", "deployments", "--all-namespaces"], options));
19
22
  const deployments = deplRaw.items.map((d) => ({
20
23
  name: d.metadata.name,
@@ -23,6 +26,7 @@ export async function scanCluster(options) {
23
26
  image: d.spec.template.spec.containers[0]?.image ?? "unknown",
24
27
  }));
25
28
  // StatefulSets
29
+ onProgress?.("statefulsets");
26
30
  const ssRaw = (await kubectlJson(["get", "statefulsets", "--all-namespaces"], options));
27
31
  const statefulsets = ssRaw.items.map((s) => ({
28
32
  name: s.metadata.name,
@@ -31,6 +35,7 @@ export async function scanCluster(options) {
31
35
  image: s.spec.template.spec.containers[0]?.image ?? "unknown",
32
36
  }));
33
37
  // Services
38
+ onProgress?.("services");
34
39
  const svcRaw = (await kubectlJson(["get", "services", "--all-namespaces"], options));
35
40
  const services = svcRaw.items.map((s) => ({
36
41
  name: s.metadata.name,
@@ -39,6 +44,7 @@ export async function scanCluster(options) {
39
44
  ports: (s.spec.ports ?? []).map((p) => `${p.port}/${p.protocol}`).join(", "),
40
45
  }));
41
46
  // Ingresses
47
+ onProgress?.("ingresses");
42
48
  const ingRaw = (await kubectlJson(["get", "ingresses", "--all-namespaces"], options));
43
49
  const ingresses = ingRaw.items.map((i) => ({
44
50
  name: i.metadata.name,
@@ -1 +1,3 @@
1
- export declare function onboard(): Promise<void>;
1
+ export declare function onboard(opts?: {
2
+ timeout?: number;
3
+ }): Promise<void>;
@@ -56,8 +56,9 @@ async function configureSafeActions(current) {
56
56
  console.log(chalk.dim(`Safe actions set to: ${result.length > 0 ? result.join(", ") : "(none — all require approval)"}`));
57
57
  return result;
58
58
  }
59
- export async function onboard() {
59
+ export async function onboard(opts = {}) {
60
60
  console.log(chalk.bold("\nKubeAgent Onboarding\n"));
61
+ const timeoutMs = (opts.timeout ?? 120) * 1000;
61
62
  // Step 1: Pick cluster context from list
62
63
  const context = await pickContext();
63
64
  const kubectlOpts = context ? { context } : {};
@@ -66,8 +67,16 @@ export async function onboard() {
66
67
  const spinner = ora("Scanning cluster...").start();
67
68
  let clusterInfo;
68
69
  try {
69
- clusterInfo = await scanCluster(kubectlOpts);
70
- spinner.succeed(`Found ${clusterInfo.nodes.length} nodes, ${clusterInfo.deployments.length} deployments, ${clusterInfo.namespaces.length} namespaces`);
70
+ const scanPromise = scanCluster(kubectlOpts, (step) => {
71
+ spinner.text = `Scanning cluster ${step}...`;
72
+ });
73
+ let timeoutId;
74
+ const timeoutPromise = new Promise((_, reject) => {
75
+ timeoutId = setTimeout(() => reject(new Error(`Cluster scan timed out after ${opts.timeout ?? 120}s`)), timeoutMs);
76
+ });
77
+ clusterInfo = await Promise.race([scanPromise, timeoutPromise]);
78
+ clearTimeout(timeoutId);
79
+ spinner.succeed(`Found ${clusterInfo.nodes.length} nodes, ${clusterInfo.deployments.length} deployments across ${clusterInfo.namespaces.length} namespaces`);
71
80
  }
72
81
  catch (err) {
73
82
  spinner.fail(`Cluster scan failed: ${err.message}`);
@@ -96,8 +105,12 @@ export async function onboard() {
96
105
  const notes = await runInterview({ cluster: clusterInfo, projects });
97
106
  const kbDir = join(configDir(), "clusters", contextName);
98
107
  ensureKbDir(kbDir);
108
+ const totalItems = projects.length + 1;
109
+ const earlyKbSpinner = ora(`Building knowledge base [1/${totalItems}]: cluster...`).start();
99
110
  writeClusterKb(kbDir, formatClusterMarkdown(clusterInfo));
100
- for (const p of projects) {
111
+ for (let i = 0; i < projects.length; i++) {
112
+ const p = projects[i];
113
+ earlyKbSpinner.text = `Building knowledge base [${i + 2}/${totalItems}]: ${p.name}...`;
101
114
  const mapping = projectMappings.find((m) => m.name === p.name);
102
115
  const mappingNote = mapping ? `**Deployment:** ${mapping.deployment} (${mapping.namespace})\n**Kind:** ${mapping.kind}` : undefined;
103
116
  writeProjectKb(kbDir, p.name, formatProjectMarkdown(p.name, p.path, p.stack, mappingNote));
@@ -105,6 +118,7 @@ export async function onboard() {
105
118
  if (notes.size > 0) {
106
119
  writeProjectKb(kbDir, "_notes", `# Onboarding Notes\n\n${Array.from(notes.entries()).map(([q, a]) => `**${q}**\n${a}`).join("\n\n")}\n`);
107
120
  }
121
+ earlyKbSpinner.succeed(`Knowledge base written (${totalItems} file${totalItems !== 1 ? "s" : ""})`);
108
122
  const currentSafeActions = existingConfig.remediation?.safe_actions ?? DEFAULT_SAFE_ACTIONS;
109
123
  const safeActions = await configureSafeActions(currentSafeActions);
110
124
  const existingChannels = existingConfig.notifications?.channels ?? [];
@@ -258,9 +272,13 @@ export async function onboard() {
258
272
  // Step 6: Write knowledge base
259
273
  const kbDir = join(configDir(), "clusters", contextName);
260
274
  ensureKbDir(kbDir);
275
+ const totalKbItems = projects.length + 1; // +1 for cluster
276
+ const kbSpinner = ora(`Building knowledge base [1/${totalKbItems}]: cluster...`).start();
261
277
  const clusterMd = formatClusterMarkdown(clusterInfo);
262
278
  writeClusterKb(kbDir, clusterMd);
263
- for (const p of projects) {
279
+ for (let i = 0; i < projects.length; i++) {
280
+ const p = projects[i];
281
+ kbSpinner.text = `Building knowledge base [${i + 2}/${totalKbItems}]: ${p.name}...`;
264
282
  const interviewNotes = Array.from(notes.entries())
265
283
  .filter(([q]) => q.toLowerCase().includes(p.name.toLowerCase()))
266
284
  .map(([q, a]) => `**${q}**\n${a}`)
@@ -280,6 +298,7 @@ export async function onboard() {
280
298
  .join("\n\n");
281
299
  writeProjectKb(kbDir, "_notes", `# Onboarding Notes\n\n${generalNotes}\n`);
282
300
  }
301
+ kbSpinner.succeed(`Knowledge base written (${totalKbItems} file${totalKbItems !== 1 ? "s" : ""})`);
283
302
  // Step 6b: Configure safe actions
284
303
  const currentSafeActions = existingConfig.remediation?.safe_actions ?? DEFAULT_SAFE_ACTIONS;
285
304
  const safeActions = await configureSafeActions(currentSafeActions);
@@ -1,5 +1,6 @@
1
1
  import readline from "node:readline";
2
2
  import chalk from "chalk";
3
+ import ora from "ora";
3
4
  import { loadAuth } from "../auth.js";
4
5
  import { proxyRequest } from "../proxy-client.js";
5
6
  async function ask(question) {
@@ -29,6 +30,7 @@ export async function runInterview(context) {
29
30
  `Projects: ${context.projects.map((p) => `${p.name} (${p.stack.language}/${p.stack.framework})`).join(", ")}`,
30
31
  ].join("\n");
31
32
  let response;
33
+ const interviewSpinner = ora("Preparing interview questions...").start();
32
34
  try {
33
35
  response = await createMessage({
34
36
  model: "claude-haiku-4-5-20251001",
@@ -40,8 +42,10 @@ export async function runInterview(context) {
40
42
  },
41
43
  ],
42
44
  });
45
+ interviewSpinner.succeed("Interview questions ready");
43
46
  }
44
47
  catch (err) {
48
+ interviewSpinner.stop();
45
49
  const msg = err.message;
46
50
  if (msg === "no_auth") {
47
51
  console.log(chalk.yellow("Skipping AI interview — run 'kubeagent login' to enable."));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kubeagent",
3
- "version": "0.1.24",
3
+ "version": "0.1.26",
4
4
  "description": "AI-powered Kubernetes management CLI",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "type": "module",