@kinetica/admin-agent 0.1.2 → 0.2.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.
@@ -31,6 +31,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
31
31
  // src/cli/index.ts
32
32
  var cli_exports = {};
33
33
  __export(cli_exports, {
34
+ chooseBundleSessionVersion: () => chooseBundleSessionVersion,
34
35
  getSession: () => getSession,
35
36
  main: () => main,
36
37
  verbose: () => verbose
@@ -84,11 +85,11 @@ function dimRgb(color, factor) {
84
85
  }
85
86
  var SHADOW_CHARS = /* @__PURE__ */ new Set(["\u2557", "\u2554", "\u2551", "\u255D", "\u255A", "\u2550"]);
86
87
  var DIM_FACTOR = 0.4;
87
- function gradientize(text) {
88
+ function gradientize(text2) {
88
89
  const PURPLE = [147, 51, 234];
89
90
  const HOT_PINK = [236, 72, 153];
90
91
  const RESET = "\x1B[0m";
91
- const lines = text.split("\n");
92
+ const lines = text2.split("\n");
92
93
  const maxIdx = Math.max(lines.length - 1, 1);
93
94
  return lines.map((line, i) => {
94
95
  const bright = lerpRgb(PURPLE, HOT_PINK, i / maxIdx);
@@ -111,17 +112,17 @@ function printBanner(model) {
111
112
  const version = getVersion();
112
113
  const subtitle = `admin-agent ${import_picocolors.default.dim(`v${version}`)}`;
113
114
  const header = model ? `${subtitle}
114
- ${import_picocolors.default.dim(`model: ${model}`)}` : subtitle;
115
+ ${import_picocolors.default.dim(`Model: ${model}`)}` : subtitle;
115
116
  process.stderr.write("\n\n" + gradientize(LOGO) + "\n\n" + header + "\n");
116
117
  return subtitle;
117
118
  }
118
119
 
119
120
  // src/cli/select-model.ts
120
- var import_prompts5 = require("@inquirer/prompts");
121
+ var import_prompts6 = require("@inquirer/prompts");
121
122
 
122
123
  // src/agent/run-agent.ts
123
- var import_claude_agent_sdk4 = require("@anthropic-ai/claude-agent-sdk");
124
- var import_prompts4 = require("@inquirer/prompts");
124
+ var import_claude_agent_sdk5 = require("@anthropic-ai/claude-agent-sdk");
125
+ var import_prompts5 = require("@inquirer/prompts");
125
126
  var import_picocolors8 = __toESM(require("picocolors"));
126
127
 
127
128
  // src/agent/diagnostic-sql.ts
@@ -556,7 +557,7 @@ function formatTableArray(rows) {
556
557
  const colWidths = headers.map(
557
558
  (h, i) => Math.max(h.length, ...cells.map((row) => row[i].length), 3)
558
559
  );
559
- const pad = (text, col) => text.padEnd(colWidths[col]);
560
+ const pad = (text2, col) => text2.padEnd(colWidths[col]);
560
561
  const headerRow = `| ${headers.map((h, i) => pad(h, i)).join(" | ")} |`;
561
562
  const separatorRow = `| ${colWidths.map((w) => "-".repeat(w)).join(" | ")} |`;
562
563
  const dataRows = cells.map((row) => `| ${row.map((cell, i) => pad(cell, i)).join(" | ")} |`);
@@ -585,13 +586,13 @@ var DEFAULT_TRUNCATION = {
585
586
  };
586
587
 
587
588
  // src/output/truncate.ts
588
- function truncateOutput(text, options = DEFAULT_TRUNCATION) {
589
- if (text === "") return "";
590
- const lines = text.split("\n");
589
+ function truncateOutput(text2, options = DEFAULT_TRUNCATION) {
590
+ if (text2 === "") return "";
591
+ const lines = text2.split("\n");
591
592
  const { headLines, tailLines } = options;
592
593
  const threshold = headLines + tailLines;
593
594
  if (lines.length <= threshold) {
594
- return text;
595
+ return text2;
595
596
  }
596
597
  const truncatedCount = lines.length - headLines - tailLines;
597
598
  const head = lines.slice(0, headLines);
@@ -1221,7 +1222,7 @@ var DEFAULT_SCRUB_PATTERNS = [
1221
1222
  ];
1222
1223
  function scrubCredentials(content, patterns = DEFAULT_SCRUB_PATTERNS) {
1223
1224
  const configRedacted = redactConfigSecrets(content);
1224
- return patterns.reduce((text, pattern) => text.replace(pattern, "[REDACTED]"), configRedacted);
1225
+ return patterns.reduce((text2, pattern) => text2.replace(pattern, "[REDACTED]"), configRedacted);
1225
1226
  }
1226
1227
 
1227
1228
  // src/tools/rest/show-configuration.ts
@@ -2714,7 +2715,7 @@ function fingerprint(value) {
2714
2715
  }
2715
2716
  function scrubCredentialPatterns(value) {
2716
2717
  return CREDENTIAL_PATTERNS.reduce(
2717
- (text, { regex, replacement }) => text.replace(regex, replacement),
2718
+ (text2, { regex, replacement }) => text2.replace(regex, replacement),
2718
2719
  value
2719
2720
  );
2720
2721
  }
@@ -2769,15 +2770,13 @@ var DIAGNOSTIC_TOOL_NAMES = [
2769
2770
  "kinetica_resource_objects",
2770
2771
  "kinetica_host_manager_status"
2771
2772
  ];
2772
- function createDiagnosticRegistry() {
2773
- return DIAGNOSTIC_TOOL_NAMES.reduce(
2774
- (registry, name) => registry.registerReadOnlyTool(name),
2775
- createRegistry()
2776
- );
2777
- }
2778
2773
  function applyOutputPipeline(result) {
2779
2774
  const payload = result.ok ? result.data : result;
2780
- return truncateOutput(formatOutput(payload));
2775
+ const body = formatOutput(payload);
2776
+ const withNote = result.ok && result.note ? `${result.note}
2777
+
2778
+ ${body}` : body;
2779
+ return truncateOutput(withNote);
2781
2780
  }
2782
2781
  function logMutationAudit(toolName, result, input5) {
2783
2782
  const statusLabel = result.ok ? import_picocolors4.default.bold(import_picocolors4.default.green("EXECUTED")) : import_picocolors4.default.bold(import_picocolors4.default.red("FAILED"));
@@ -3191,6 +3190,26 @@ function buildEvidenceChecklist() {
3191
3190
  ].join("\n");
3192
3191
  }
3193
3192
 
3193
+ // src/agent/prompt-sections.ts
3194
+ function buildFailurePatternsSection(playbooks) {
3195
+ if (!playbooks || playbooks.length === 0) return "";
3196
+ const entries = playbooks.map((p) => `**${p.title}:**
3197
+
3198
+ ${p.body}`).join("\n\n");
3199
+ return `### Common Failure Patterns
3200
+
3201
+ ${entries}`;
3202
+ }
3203
+ function buildReferenceSection(references) {
3204
+ if (!references || references.length === 0) return "";
3205
+ const entries = references.map((r) => `**${r.title}:**
3206
+
3207
+ ${r.body}`).join("\n\n");
3208
+ return `### Reference Knowledge
3209
+
3210
+ ${entries}`;
3211
+ }
3212
+
3194
3213
  // src/agent/report-template.ts
3195
3214
  var import_node_fs2 = require("fs");
3196
3215
  var import_node_path2 = require("path");
@@ -3301,25 +3320,7 @@ ${sqls.join("\n\n")}
3301
3320
  }
3302
3321
  return result.trimEnd();
3303
3322
  }
3304
- function buildFailurePatternsSection(playbooks) {
3305
- if (!playbooks || playbooks.length === 0) return "";
3306
- const entries = playbooks.map((p) => `**${p.title}:**
3307
-
3308
- ${p.body}`).join("\n\n");
3309
- return `### Common Failure Patterns
3310
-
3311
- ${entries}`;
3312
- }
3313
- function buildReferenceSection(references) {
3314
- if (!references || references.length === 0) return "";
3315
- const entries = references.map((r) => `**${r.title}:**
3316
-
3317
- ${r.body}`).join("\n\n");
3318
- return `### Reference Knowledge
3319
-
3320
- ${entries}`;
3321
- }
3322
- function buildSystemPrompt(kineticaVersion, catalogSchemas, playbooks, references, degraded) {
3323
+ function buildSystemPrompt(kineticaVersion, catalogSchemas, playbooks, references, degraded, bundleCapability, bundleReferences) {
3323
3324
  const versionSection = kineticaVersion ? `**Kinetica Version:** ${kineticaVersion} (provided at session start)` : "**Kinetica Version:** Unknown \u2014 detect via kinetica_health_check as the first action of every investigation.";
3324
3325
  const t = "`";
3325
3326
  const degradedSection = degraded ? `
@@ -3352,6 +3353,21 @@ function buildSystemPrompt(kineticaVersion, catalogSchemas, playbooks, reference
3352
3353
  - Do NOT attempt Round 4 (mutations) or Round 5 (verification) \u2014 the DB engine must be running first
3353
3354
 
3354
3355
  ` : "";
3356
+ const bundleSection = bundleCapability === void 0 ? "" : `
3357
+ ---
3358
+
3359
+ ## Support Bundle Capability
3360
+
3361
+ ${bundleCapability === "attached" ? `A Kinetica support bundle (offline ${t}gpudb_sysinfo${t} snapshot) IS attached to this session. You now have **two complementary evidence sources**:
3362
+ - **The bundle** (${t}kinetica_bundle_*${t} tools) \u2014 frozen point-in-time history: per-rank logs (the incident narrative the live endpoints can't show), gpudb.conf at capture time, and host diagnostics. Call ${t}kinetica_bundle_list_files${t} first, then ${t}kinetica_bundle_log_timeline${t}.
3363
+ - **The live system** (the live diagnostic tools) \u2014 current state, right now.` : `The operator can attach an offline Kinetica support bundle for analysis. If they ask to "analyze a support bundle" (or you need historical logs the live endpoints don't expose \u2014 Kinetica has no log endpoint), call ${t}kinetica_load_bundle${t} **with no path** \u2014 they will be shown an interactive directory picker to choose the bundle. Do NOT ask for the path in chat. The ${t}kinetica_bundle_*${t} tools then read its logs/config/host-diagnostics.
3364
+
3365
+ **Attaching a bundle is SETUP, not an investigation.** After ${t}kinetica_load_bundle${t} succeeds, do NOT start gathering evidence. Confirm what the operator wants to investigate first (briefly note the bundle is ready), then wait for their answer before calling any ${t}kinetica_bundle_*${t} tools. Do not waste turns investigating something they did not ask about.`}
3366
+
3367
+ **Correlate the two:** the bundle tells you what HAPPENED (e.g. a crash, an error spike, config at capture time); the live tools tell you what is TRUE NOW (did it recover? is the config still drifted? did the issue recur?). Use the bundle for the historical narrative and the live tools to verify current state. Note in the report which findings came from the bundle (and its capture time) versus the live system.
3368
+ ${bundleReferences && bundleReferences.length > 0 ? `
3369
+ ${buildReferenceSection(bundleReferences)}
3370
+ ` : ""}`;
3355
3371
  return `You are an expert Kinetica GPU database administrator and diagnostician with deep knowledge of Kinetica's internals, system tables, REST API, and common failure patterns. Your job is to autonomously investigate database issues reported by operators, gather diagnostic evidence, reason over that evidence to identify root causes, and produce a structured diagnostic report with actionable remediation steps.
3356
3372
 
3357
3373
  ${versionSection}
@@ -3479,7 +3495,7 @@ ${t}kinetica_show_table${t} (e.g., ${t}is_shard_key${t}, ${t}is_primary_key${t},
3479
3495
  ${buildFailurePatternsSection(playbooks)}
3480
3496
 
3481
3497
  ${buildReferenceSection(references)}
3482
-
3498
+ ${bundleSection}
3483
3499
  ---
3484
3500
 
3485
3501
  ## Analysis Instructions
@@ -3536,10 +3552,14 @@ Include specific, actionable remediation steps tied to your findings. Structure
3536
3552
 
3537
3553
  ## Post-Report Behavior
3538
3554
 
3539
- 1. Call the ${t}save_report${t} tool with the complete report markdown content to save it to disk.
3540
- 2. After the report is saved, ask: "Would you like to investigate another issue, or end the session?"
3541
- 3. If the operator wants another investigation, start fresh with the same 5-round protocol.
3542
- 4. On session end: summarize all issues investigated and list the saved report file paths, then exit.
3555
+ 1. Present the finished report in your response so the operator can read it.
3556
+ 2. **Ask BEFORE saving \u2014 never save unprompted.** After presenting the report, ask exactly: "Would you like me to save this report to disk? (yes/no)" and then STOP \u2014 end your turn and wait for the operator's answer. Do NOT call ${t}save_report${t} in the same turn as the question; the question must come first.
3557
+ - If the operator answers yes \u2192 call ${t}save_report${t} with the complete report markdown content.
3558
+ - If the operator answers no \u2192 do not save; acknowledge and continue.
3559
+ - **Only exception:** when checkpointing under budget pressure (the operator warned the budget guard is approaching, or you are preserving work with a ${t}partial: true${t} report before an early cutoff), save immediately WITHOUT asking \u2014 preserving findings outweighs the prompt.
3560
+ 3. After saving (or after the operator declines), ask: "Would you like to investigate another issue, or end the session?"
3561
+ 4. If the operator wants another investigation, start fresh with the same 5-round protocol.
3562
+ 5. On session end: summarize all issues investigated and list the saved report file paths, then exit.
3543
3563
 
3544
3564
  ---
3545
3565
 
@@ -3550,6 +3570,13 @@ Monitor your context window usage during long investigations:
3550
3570
  - If you detect that context is getting full (many rounds, many large tool responses), warn the operator: "The session context is getting long. Consider starting a fresh session after this report to maintain investigation quality. Your reports are saved to disk."
3551
3571
  - Do NOT continue investigating when context is too full \u2014 write the report with evidence gathered so far.
3552
3572
 
3573
+ ## Budget & Length Awareness
3574
+
3575
+ The session has a per-session budget guard that can end the run before you finish \u2014 and the operator may see a warning that you are "approaching the budget guard". To make sure a diagnostic always survives an early cutoff:
3576
+ - During a long or expensive investigation (many rounds or many large tool responses), proactively call \`save_report\` with \`partial: true\` to checkpoint your findings so far. A partial report is far better than none.
3577
+ - If the operator warns you that the budget guard is approaching, STOP gathering new evidence: immediately save a \`partial: true\` report with the evidence you have, state your best current hypothesis, and wind down the turn.
3578
+ - Treat the guard as a normal limit, not an error \u2014 never apologize for it; just preserve the work.
3579
+
3553
3580
  ---
3554
3581
 
3555
3582
  ## Output Formatting
@@ -3635,9 +3662,8 @@ async function discoverCatalogSchemas(session2) {
3635
3662
  var import_promises2 = require("fs/promises");
3636
3663
  var import_node_path3 = require("path");
3637
3664
  var import_node_fs3 = require("fs");
3638
- async function loadReferences(refsDir) {
3665
+ async function loadReferencesFrom(dir) {
3639
3666
  try {
3640
- const dir = refsDir ?? (0, import_node_path3.join)(findPackageRoot(__dirname), "knowledge", "references");
3641
3667
  if (!(0, import_node_fs3.existsSync)(dir)) return [];
3642
3668
  const files = await (0, import_promises2.readdir)(dir);
3643
3669
  const mdFiles = files.filter((f) => f.endsWith(".md")).sort();
@@ -3659,13 +3685,21 @@ async function loadReferences(refsDir) {
3659
3685
  return [];
3660
3686
  }
3661
3687
  }
3688
+ function loadReferences(refsDir) {
3689
+ return loadReferencesFrom(refsDir ?? (0, import_node_path3.join)(findPackageRoot(__dirname), "knowledge", "references"));
3690
+ }
3691
+ function loadBundleReferences(refsDir) {
3692
+ return loadReferencesFrom(
3693
+ refsDir ?? (0, import_node_path3.join)(findPackageRoot(__dirname), "knowledge", "references", "bundle")
3694
+ );
3695
+ }
3662
3696
 
3663
3697
  // src/agent/prompt-budget.ts
3664
3698
  var CHARS_PER_TOKEN = 4;
3665
3699
  var DEFAULT_PROMPT_BUDGET_TOKENS = 2e4;
3666
- function estimateTokens(text) {
3667
- if (!text) return 0;
3668
- return Math.ceil(text.length / CHARS_PER_TOKEN);
3700
+ function estimateTokens(text2) {
3701
+ if (!text2) return 0;
3702
+ return Math.ceil(text2.length / CHARS_PER_TOKEN);
3669
3703
  }
3670
3704
  function checkPromptBudget(prompt, opts) {
3671
3705
  const threshold = opts?.warnAtTokens ?? DEFAULT_PROMPT_BUDGET_TOKENS;
@@ -3678,6 +3712,69 @@ function checkPromptBudget(prompt, opts) {
3678
3712
  };
3679
3713
  }
3680
3714
 
3715
+ // src/agent/session-budget.ts
3716
+ var DEFAULT_MAX_BUDGET_USD = 5;
3717
+ var DEFAULT_WARN_FRACTION = 0.8;
3718
+ var BUDGET_ENV_VAR = "ADMIN_AGENT_MAX_BUDGET";
3719
+ var MODEL_PRICING = {
3720
+ sonnet: { inputPerMTok: 3, outputPerMTok: 15, cacheReadPerMTok: 0.3, cacheCreationPerMTok: 3.75 },
3721
+ haiku: { inputPerMTok: 1, outputPerMTok: 5, cacheReadPerMTok: 0.1, cacheCreationPerMTok: 1.25 },
3722
+ opus: { inputPerMTok: 15, outputPerMTok: 75, cacheReadPerMTok: 1.5, cacheCreationPerMTok: 18.75 }
3723
+ };
3724
+ function fromSdkUsage(raw) {
3725
+ const u = raw ?? {};
3726
+ return {
3727
+ inputTokens: u.input_tokens,
3728
+ outputTokens: u.output_tokens,
3729
+ cacheReadInputTokens: u.cache_read_input_tokens,
3730
+ cacheCreationInputTokens: u.cache_creation_input_tokens
3731
+ };
3732
+ }
3733
+ function safeCount(value) {
3734
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : 0;
3735
+ }
3736
+ function isValidBudget(value) {
3737
+ return typeof value === "number" && Number.isFinite(value) && value > 0;
3738
+ }
3739
+ function estimateTurnCostUsd(usage, model) {
3740
+ if (!usage) return 0;
3741
+ const price = MODEL_PRICING[model];
3742
+ const input5 = safeCount(usage.inputTokens);
3743
+ const output = safeCount(usage.outputTokens);
3744
+ const cacheRead = safeCount(usage.cacheReadInputTokens);
3745
+ const cacheCreation = safeCount(usage.cacheCreationInputTokens);
3746
+ return (input5 * price.inputPerMTok + output * price.outputPerMTok + cacheRead * price.cacheReadPerMTok + cacheCreation * price.cacheCreationPerMTok) / 1e6;
3747
+ }
3748
+ function resolveMaxBudgetUsd(flagValue2, env = process.env) {
3749
+ if (isValidBudget(flagValue2)) return flagValue2;
3750
+ const raw = env[BUDGET_ENV_VAR];
3751
+ if (raw !== void 0 && raw !== "") {
3752
+ const parsed = Number(raw);
3753
+ if (isValidBudget(parsed)) return parsed;
3754
+ }
3755
+ return DEFAULT_MAX_BUDGET_USD;
3756
+ }
3757
+ function createBudgetTracker(opts) {
3758
+ const warnFraction = opts.warnFraction ?? DEFAULT_WARN_FRACTION;
3759
+ const warnAt = opts.maxUsd * warnFraction;
3760
+ let spent = 0;
3761
+ let warned = false;
3762
+ return {
3763
+ add(usage, model) {
3764
+ spent += estimateTurnCostUsd(usage, model);
3765
+ },
3766
+ spentUsd() {
3767
+ return spent;
3768
+ },
3769
+ shouldWarn() {
3770
+ return !warned && spent > warnAt;
3771
+ },
3772
+ markWarned() {
3773
+ warned = true;
3774
+ }
3775
+ };
3776
+ }
3777
+
3681
3778
  // src/report/save-report.ts
3682
3779
  var import_promises3 = require("fs/promises");
3683
3780
  var import_node_path4 = require("path");
@@ -3696,7 +3793,7 @@ function formatTimestamp(date) {
3696
3793
  function makeSaveReportTool() {
3697
3794
  return (0, import_claude_agent_sdk3.tool)(
3698
3795
  "save_report",
3699
- "Save a diagnostic report to disk. Automatically scrubs credentials, creates a timestamped filename in reports/, and auto-creates the directory. Use at the end of each investigation or when interrupted.",
3796
+ "Save a diagnostic report to disk. Call this ONLY after the operator has agreed to save (you must ask 'save this report? (yes/no)' and get a yes first) \u2014 or when checkpointing a partial report under budget pressure. Automatically scrubs credentials, creates a timestamped filename in reports/, and auto-creates the directory.",
3700
3797
  {
3701
3798
  content: import_zod17.z.string().describe("The full markdown diagnostic report content"),
3702
3799
  partial: import_zod17.z.boolean().optional().describe(
@@ -3720,170 +3817,1386 @@ function makeSaveReportTool() {
3720
3817
  );
3721
3818
  }
3722
3819
 
3723
- // src/approval/gate.ts
3724
- var import_prompts3 = require("@inquirer/prompts");
3820
+ // src/tools/bundle/index.ts
3821
+ var import_claude_agent_sdk4 = require("@anthropic-ai/claude-agent-sdk");
3725
3822
 
3726
- // src/approval/display.ts
3727
- var import_picocolors5 = __toESM(require("picocolors"));
3728
- var IMPACT_FALLBACK = "Impact unknown \u2014 review parameters carefully";
3729
- var DIVIDER2 = import_picocolors5.default.dim("\u2500".repeat(50));
3730
- var LABEL_WIDTH = 8;
3731
- function formatLabel(label) {
3732
- return ` ${label.padEnd(LABEL_WIDTH)}: `;
3823
+ // src/tools/bundle/list-files.ts
3824
+ var import_zod18 = require("zod");
3825
+
3826
+ // src/bundle/known-files.ts
3827
+ var KNOWN_BUNDLE_FILES = {
3828
+ // Host resources
3829
+ "cpu.txt": "CPU topology, NUMA, and interrupts (lscpu, numactl, /proc/cpuinfo, /proc/interrupts)",
3830
+ "mem.txt": "Memory usage, /proc/meminfo, and transparent-hugepage setting (free -m -t)",
3831
+ "disk.txt": "Filesystems, mounts, block devices, and disk stats (df, mount, lsblk, fdisk, /etc/fstab, /proc/diskstats)",
3832
+ "gpu.txt": "NVIDIA GPU inventory and state (nvidia-smi -L/-q, modinfo nvidia)",
3833
+ "net.txt": "Network interfaces, sockets, and DNS (hostname, ifconfig, netstat, /etc/resolv.conf)",
3834
+ // Processes
3835
+ "ps.txt": "Full process list (ps -auxww, ps -ejHlfww)",
3836
+ "gpudb-exe.txt": "Running gpudb processes (ps auxfwww | grep gpudb)",
3837
+ // Hardware / firmware
3838
+ "dmidecode.txt": "BIOS / DMI hardware inventory (dmidecode)",
3839
+ "lshw.txt": "Hardware listing (lshw -short -numeric)",
3840
+ "pci.txt": "PCI devices and I/O resources (lspci, /proc/ioports, /proc/iomem)",
3841
+ // Kernel / OS
3842
+ "dmesg.txt": "Kernel ring buffer \u2014 boot and runtime kernel messages (dmesg -T)",
3843
+ "dmesg-timestamp.txt": "Kernel ring buffer with human-readable timestamps",
3844
+ "sysctl.txt": "Kernel tunables (sysctl -a)",
3845
+ "sys.txt": "OS identity, uptime, ulimits, kernel cmdline, clocksource, and loaded modules (uname, ulimit, /proc/cmdline, lsmod)",
3846
+ "lsof.txt": "Open files and network sockets (lsof -n -P)",
3847
+ "lslocks.txt": "Held file locks (lslocks)",
3848
+ // Packages / linker / accounts
3849
+ "deb.txt": "Installed Debian packages and verification (dpkg -l, dpkg -V)",
3850
+ "rpm.txt": "Installed RPM packages (rpm -qa)",
3851
+ "ld.so.conf.txt": "Dynamic-linker library search paths (/etc/ld.so.conf)",
3852
+ "user.txt": "Users, groups, and the gpudb service account (whoami, id, /etc/passwd, /etc/group)",
3853
+ "sudoers.txt": "Sudo configuration (/etc/sudoers)",
3854
+ "etc_profile.txt": "Login shell profile (/etc/profile)",
3855
+ "etc_bashrc.txt": "System bashrc (/etc/bashrc)",
3856
+ "etc_host.txt": "Static hostname resolution (/etc/hosts)",
3857
+ // Kinetica-specific
3858
+ "gpudb.txt": "GPUdb version/build, binary md5 + ldd, and the captured gpudb.conf / gpudb_logger.conf ($GPUDB_EXE -v)",
3859
+ "gpudb_core_etc_gpudb.conf": "The live gpudb.conf at capture time (the database's main config)",
3860
+ "gpudb_core_etc_gpudb_logger.conf": "The logging configuration (gpudb_logger.conf)",
3861
+ "loki-info.txt": "Loki log-index stats: labels, series, and per-class volume (logcli)",
3862
+ "sql-queries.txt": "SQL query log extracted from Loki (logcli)",
3863
+ "tables.txt": "Table schemas and column types (gadmin --schema), when collected",
3864
+ "logfiles.txt": "Manifest: the log directories/files the collector enumerated",
3865
+ "errors.txt": "Collection commands that FAILED during capture (Evidence Gaps)",
3866
+ "proc-logs-erros.txt": "Per-process log-collection failures during capture (Evidence Gaps)"
3867
+ };
3868
+ var KIND_DESCRIPTIONS = {
3869
+ "core-log": "Per-rank rolling Kinetica core log (the primary incident narrative)",
3870
+ "component-log": "Component service log (sql-engine, httpd, reveal, tomcat, stats, \u2026)",
3871
+ "loki-tail": "Last-2h Loki tail for a service (small; searched only when no core logs exist)",
3872
+ "process-info": "Per-rank process snapshot: command line, PID, and environment (/proc/<pid>/environ)",
3873
+ config: "Kinetica configuration file",
3874
+ "version-info": "GPUdb version/build information",
3875
+ "collection-errors": "Collection commands that FAILED during capture (Evidence Gaps)",
3876
+ manifest: "Manifest of log directories/files the collector enumerated"
3877
+ };
3878
+ function basename(relPath) {
3879
+ const parts = relPath.split("/");
3880
+ return parts[parts.length - 1] ?? relPath;
3733
3881
  }
3734
- function renderApprovalPanel(toolName, toolInput, impact, beforeAfter, reasoningSummary) {
3735
- const header = import_picocolors5.default.bold(import_picocolors5.default.yellow(" Mutation Approval Required"));
3736
- const action = `${formatLabel("Action")}${import_picocolors5.default.bold(formatToolName(toolName))}`;
3737
- const paramEntries = Object.entries(toolInput);
3738
- const paramSection = paramEntries.length === 0 ? " (no parameters)" : paramEntries.map(([key, value]) => {
3739
- const formatted = typeof value === "string" ? value : JSON.stringify(value, null, 2);
3740
- return ` ${import_picocolors5.default.dim(key)}: ${formatted}`;
3741
- }).join("\n");
3742
- const impactLine = `${formatLabel("Impact")}${impact ?? IMPACT_FALLBACK}`;
3743
- const prompt = import_picocolors5.default.dim(
3744
- `${formatLabel("Respond")}y (proceed) | n (abort) | explain (show reasoning)`
3745
- );
3746
- const hasBeforeAfter = beforeAfter !== void 0 && beforeAfter.length > 0;
3747
- const beforeAfterSection = hasBeforeAfter ? beforeAfter.map(
3748
- (entry) => ` ${import_picocolors5.default.dim(entry.key)}: ${entry.current} ${import_picocolors5.default.yellow("->")} ${entry.proposed}`
3749
- ).join("\n") : null;
3750
- const hasReasoning = reasoningSummary !== void 0 && reasoningSummary.length > 0;
3751
- const reasoningSection = hasReasoning ? `${formatLabel("Reason")}${reasoningSummary}` : null;
3752
- const sections = ["", DIVIDER2, header, "", action, paramSection, ""];
3753
- if (beforeAfterSection !== null) {
3754
- sections.push(beforeAfterSection, "");
3755
- }
3756
- if (reasoningSection !== null) {
3757
- sections.push(reasoningSection, "");
3758
- }
3759
- sections.push(impactLine, "", prompt, DIVIDER2, "");
3760
- return sections.join("\n");
3882
+ function describeBundleFile(entry) {
3883
+ return KNOWN_BUNDLE_FILES[basename(entry.relPath)] ?? KIND_DESCRIPTIONS[entry.kind] ?? "";
3761
3884
  }
3762
3885
 
3763
- // src/approval/gate.ts
3764
- var DENY_MESSAGE = "User denied this mutation. Skip and continue with the investigation.";
3765
- var REASONING_FALLBACK = "Reasoning not available. Review the action details above before proceeding.";
3766
- function createApprovalGate(isReadOnly) {
3767
- return async (toolName, toolInput, options) => {
3768
- if (isReadOnly(toolName)) {
3769
- return {
3770
- behavior: "allow",
3771
- updatedInput: toolInput,
3772
- toolUseID: options.toolUseID
3773
- };
3774
- }
3775
- const impact = options.decisionReason;
3776
- const panel = renderApprovalPanel(toolName, toolInput, impact);
3777
- console.error(panel);
3778
- while (true) {
3779
- try {
3780
- const raw = await (0, import_prompts3.input)({ message: "Proceed? (y/n/explain):" }, { signal: options.signal });
3781
- const normalized = raw.trim().toLowerCase();
3782
- if (normalized === "y") {
3783
- process.stderr.write("\n");
3784
- return {
3785
- behavior: "allow",
3786
- updatedInput: toolInput,
3787
- toolUseID: options.toolUseID
3788
- };
3789
- }
3790
- if (normalized === "n") {
3791
- process.stderr.write("\n");
3792
- return {
3793
- behavior: "deny",
3794
- message: DENY_MESSAGE,
3795
- toolUseID: options.toolUseID
3796
- };
3797
- }
3798
- if (normalized === "explain") {
3799
- const reasoning = options.decisionReason;
3800
- if (reasoning) {
3801
- console.error(`
3802
- Agent reasoning: ${reasoning}
3803
- `);
3804
- } else {
3805
- console.error(`
3806
- ${REASONING_FALLBACK}
3807
- `);
3808
- }
3809
- }
3810
- } catch {
3811
- return {
3812
- behavior: "deny",
3813
- message: DENY_MESSAGE,
3814
- toolUseID: options.toolUseID
3815
- };
3816
- }
3886
+ // src/tools/bundle/list-files.ts
3887
+ var BundleListFilesSchema = import_zod18.z.object({
3888
+ kind: import_zod18.z.string().optional()
3889
+ });
3890
+ async function bundleListFiles(source, args = {}) {
3891
+ const all = source.listFiles();
3892
+ const filtered = args.kind ? all.filter((e) => e.kind === args.kind) : all;
3893
+ const { totalFiles, totalBytes, byKind, ranks, services } = source.inventory();
3894
+ const version = await source.detectVersion();
3895
+ const errors = await source.collectionErrors();
3896
+ const files = filtered.map((e) => ({
3897
+ file: e.relPath,
3898
+ kind: e.kind,
3899
+ rank: e.rank ?? "",
3900
+ size_kb: Math.round(e.sizeBytes / 1024),
3901
+ // What the file contains — so the agent can pick the right one without reading it.
3902
+ description: describeBundleFile(e)
3903
+ }));
3904
+ return {
3905
+ ok: true,
3906
+ data: {
3907
+ detected_version: version ?? "unknown",
3908
+ ranks_present: ranks.join(", ") || "none",
3909
+ services_present: services.join(", ") || "none",
3910
+ total_files: totalFiles,
3911
+ total_size_mb: Number((totalBytes / 1e6).toFixed(1)),
3912
+ counts_by_kind: byKind,
3913
+ failed_collections: errors.length,
3914
+ files
3817
3915
  }
3818
3916
  };
3819
3917
  }
3820
3918
 
3821
- // src/agent/turn-gate.ts
3822
- function createTurnGate() {
3823
- let resolve2 = () => {
3919
+ // src/tools/bundle/log-timeline.ts
3920
+ var import_zod19 = require("zod");
3921
+ var BundleLogTimelineSchema = import_zod19.z.object({
3922
+ min_severity: import_zod19.z.enum(["INFO", "WARN", "UERR", "ERROR", "FATAL"]).optional(),
3923
+ granularity: import_zod19.z.enum(["day", "hour", "minute"]).optional(),
3924
+ rank: import_zod19.z.string().describe('Numeric rank only, e.g. "r0"/"r1". For the host manager use host_manager.').optional(),
3925
+ host_manager: import_zod19.z.boolean().describe("Bucket the host-manager (hm) log \u2014 a singleton service, not a rank.").optional(),
3926
+ component: import_zod19.z.string().optional(),
3927
+ include_components: import_zod19.z.boolean().optional()
3928
+ });
3929
+ async function bundleLogTimeline(source, args = {}) {
3930
+ const query3 = {
3931
+ ...args.min_severity !== void 0 ? { minSeverity: args.min_severity } : {},
3932
+ ...args.granularity !== void 0 ? { granularity: args.granularity } : {},
3933
+ ...args.rank !== void 0 ? { rank: args.rank } : {},
3934
+ ...args.host_manager !== void 0 ? { hostManager: args.host_manager } : {},
3935
+ ...args.component !== void 0 ? { component: args.component } : {},
3936
+ ...args.include_components !== void 0 ? { includeComponents: args.include_components } : {}
3824
3937
  };
3825
- let promise = new Promise((r) => {
3826
- resolve2 = r;
3938
+ const result = await source.logTimeline(query3);
3939
+ const severities = [...new Set(result.buckets.flatMap((b) => Object.keys(b.counts)))];
3940
+ const order = ["FATAL", "ERROR", "UERR", "WARN", "INFO"];
3941
+ severities.sort((a, b) => order.indexOf(a) - order.indexOf(b));
3942
+ const rows = result.buckets.map((b) => {
3943
+ const row = { time_bucket: b.bucket };
3944
+ for (const sev of severities) row[sev] = b.counts[sev] ?? 0;
3945
+ row.total = b.total;
3946
+ return row;
3827
3947
  });
3828
- return Object.freeze({
3829
- wait: () => promise,
3830
- open: () => {
3831
- resolve2();
3832
- },
3833
- close: () => {
3834
- promise = new Promise((r) => {
3835
- resolve2 = r;
3836
- });
3948
+ return {
3949
+ ok: true,
3950
+ note: result.totalCounted === 0 ? "No lines at or above the severity threshold \u2014 try a lower min_severity." : `${result.totalCounted} event(s) across ${result.buckets.length} bucket(s), ${result.filesScanned.length} file(s).`,
3951
+ data: {
3952
+ lines_scanned: result.linesScanned,
3953
+ files_scanned: result.filesScanned.join(", ") || "none",
3954
+ buckets: rows
3837
3955
  }
3838
- });
3956
+ };
3839
3957
  }
3840
3958
 
3841
- // src/output/render-markdown.ts
3842
- var import_picocolors6 = __toESM(require("picocolors"));
3843
- var BOLD_RE = /\*\*(.+?)\*\*/g;
3844
- var HEADING_RE = /^(#{1,6})\s+(.+)$/;
3845
- function renderMarkdownLine(line) {
3846
- const headingMatch = HEADING_RE.exec(line);
3847
- if (headingMatch) {
3848
- return import_picocolors6.default.bold(headingMatch[2]);
3959
+ // src/tools/bundle/search-logs.ts
3960
+ var import_zod20 = require("zod");
3961
+ var BundleSearchLogsSchema = import_zod20.z.object({
3962
+ regex: import_zod20.z.string().optional(),
3963
+ min_severity: import_zod20.z.enum(["INFO", "WARN", "UERR", "ERROR", "FATAL"]).optional(),
3964
+ from_ts: import_zod20.z.string().optional(),
3965
+ to_ts: import_zod20.z.string().optional(),
3966
+ rank: import_zod20.z.string().describe('Numeric rank only, e.g. "r0"/"r1". For the host manager use host_manager.').optional(),
3967
+ host_manager: import_zod20.z.boolean().describe("Search the host-manager (hm) log \u2014 a singleton service, not a rank.").optional(),
3968
+ component: import_zod20.z.string().optional(),
3969
+ include_components: import_zod20.z.boolean().optional(),
3970
+ max_matches: import_zod20.z.number().int().min(1).max(1e3).optional()
3971
+ });
3972
+ async function bundleSearchLogs(source, args = {}) {
3973
+ const query3 = {
3974
+ ...args.regex !== void 0 ? { regex: args.regex } : {},
3975
+ ...args.min_severity !== void 0 ? { minSeverity: args.min_severity } : {},
3976
+ ...args.from_ts !== void 0 ? { fromTs: args.from_ts } : {},
3977
+ ...args.to_ts !== void 0 ? { toTs: args.to_ts } : {},
3978
+ ...args.rank !== void 0 ? { rank: args.rank } : {},
3979
+ ...args.host_manager !== void 0 ? { hostManager: args.host_manager } : {},
3980
+ ...args.component !== void 0 ? { component: args.component } : {},
3981
+ ...args.include_components !== void 0 ? { includeComponents: args.include_components } : {},
3982
+ ...args.max_matches !== void 0 ? { maxMatches: args.max_matches } : {}
3983
+ };
3984
+ const result = await source.searchLogs(query3);
3985
+ const note = result.capped ? `Showing ${result.matches.length} of ${result.totalMatched} matches across ${result.filesScanned.length} file(s) (display capped). Narrow with a tighter regex, severity, or time window to surface the specific lines.` : `${result.totalMatched} match(es) across ${result.filesScanned.length} file(s).`;
3986
+ return {
3987
+ ok: true,
3988
+ note,
3989
+ data: {
3990
+ total_matched: result.totalMatched,
3991
+ lines_scanned: result.linesScanned,
3992
+ files_scanned: result.filesScanned.join(", ") || "none",
3993
+ capped: result.capped,
3994
+ matches: result.matches.map((m) => ({
3995
+ file: m.file,
3996
+ line: m.lineNumber,
3997
+ timestamp: m.timestamp ?? "",
3998
+ severity: m.severity ?? "",
3999
+ rank: m.rank ?? "",
4000
+ message: m.message
4001
+ }))
4002
+ }
4003
+ };
4004
+ }
4005
+
4006
+ // src/tools/bundle/read-config.ts
4007
+ var import_zod21 = require("zod");
4008
+ var BundleReadConfigSchema = import_zod21.z.object({
4009
+ section: import_zod21.z.string().optional(),
4010
+ key: import_zod21.z.string().optional()
4011
+ });
4012
+ async function bundleReadConfig(source, args = {}) {
4013
+ const result = await source.readConfig({
4014
+ ...args.section !== void 0 ? { section: args.section } : {},
4015
+ ...args.key !== void 0 ? { key: args.key } : {}
4016
+ });
4017
+ if ("error" in result) {
4018
+ return { ok: false, status: 0, error: result.error, raw: "" };
3849
4019
  }
3850
- if (line.includes("**")) {
3851
- return line.replace(BOLD_RE, (_, text) => import_picocolors6.default.bold(text));
4020
+ if (result.entries.length === 0 && args.section !== void 0) {
4021
+ const all = await source.readConfig(args.key !== void 0 ? { key: args.key } : {});
4022
+ const sections = "error" in all ? [] : [...new Set(all.entries.map((e) => e.section))].sort();
4023
+ const sectionList = sections.map((s) => s === "" ? "(flat/top-level)" : s).join(", ");
4024
+ return {
4025
+ ok: true,
4026
+ note: `No entries in section "${args.section}" of ${result.file}. gpudb.conf is largely flat \u2014 retry filtering by key only. Sections present: ${sectionList || "(none)"}.`,
4027
+ data: { section_not_found: args.section, available_sections: sections }
4028
+ };
3852
4029
  }
3853
- return line;
4030
+ return {
4031
+ ok: true,
4032
+ note: `${result.entries.length} entr(y/ies) from ${result.file}.`,
4033
+ data: result.entries.map((e) => ({ section: e.section, key: e.key, value: e.value }))
4034
+ };
3854
4035
  }
3855
4036
 
3856
- // src/output/reformat-tables.ts
3857
- var SEPARATOR_CELL_RE = /^:?-+:?$/;
3858
- var BOLD_MARKERS_RE = /\*\*(.+?)\*\*/g;
3859
- function visualWidth(text) {
3860
- return text.replace(BOLD_MARKERS_RE, "$1").length;
3861
- }
3862
- function isSeparatorCell(cell) {
3863
- return SEPARATOR_CELL_RE.test(cell);
3864
- }
3865
- function isSeparatorRow(cells) {
3866
- return cells.length > 0 && cells.every(isSeparatorCell);
4037
+ // src/tools/bundle/read-sysinfo.ts
4038
+ var import_zod22 = require("zod");
4039
+ var BundleReadSysinfoSchema = import_zod22.z.object({
4040
+ name: import_zod22.z.string().min(1)
4041
+ });
4042
+ async function bundleReadSysinfo(source, args) {
4043
+ const result = await source.readSysinfo(args.name);
4044
+ if ("error" in result) {
4045
+ return { ok: false, status: 0, error: result.error, raw: "" };
4046
+ }
4047
+ return {
4048
+ ok: true,
4049
+ data: {
4050
+ ...result.header !== void 0 ? { source_file: result.header } : {},
4051
+ blocks: result.blocks.map((b) => ({
4052
+ command: b.command,
4053
+ ...b.exitCode !== void 0 ? { exit_code: b.exitCode } : {},
4054
+ output: b.output
4055
+ }))
4056
+ }
4057
+ };
3867
4058
  }
3868
- function parseCells(line) {
3869
- return line.split("|").slice(1, -1).map((c) => c.trim());
4059
+
4060
+ // src/tools/bundle/load-bundle.ts
4061
+ var import_zod23 = require("zod");
4062
+
4063
+ // src/bundle/verify-bundle.ts
4064
+ var import_promises6 = require("fs/promises");
4065
+
4066
+ // src/bundle/BundleSource.ts
4067
+ var import_promises5 = require("fs/promises");
4068
+ var import_node_path6 = require("path");
4069
+
4070
+ // src/bundle/sysinfo-block.ts
4071
+ var SEPARATOR_RE = /^-{3,}$/;
4072
+ var EXEC_CMD_RE = /^EXEC_CMD:\s?(.*)$/;
4073
+ var EXEC_END_RE = /^EXEC_END with exit code (\d+)\s*:?\s*(.*)$/;
4074
+ var SHOWING_RE = /^### Showing whole log file\s*:/;
4075
+ function trimBlankEdges(lines) {
4076
+ let start = 0;
4077
+ let end = lines.length;
4078
+ while (start < end && lines[start].trim() === "") start++;
4079
+ while (end > start && lines[end - 1].trim() === "") end--;
4080
+ return lines.slice(start, end).join("\n");
4081
+ }
4082
+ function parseSysinfo(content) {
4083
+ const lines = content.split("\n");
4084
+ let header;
4085
+ const blocks = [];
4086
+ let current;
4087
+ let sawCommand = false;
4088
+ const closeBlock = (exitCode, exitMessage) => {
4089
+ if (!current) return;
4090
+ blocks.push({
4091
+ command: current.command,
4092
+ output: trimBlankEdges(current.output),
4093
+ ...exitCode !== void 0 ? { exitCode } : {},
4094
+ ...exitMessage !== void 0 && exitMessage !== "" ? { exitMessage } : {}
4095
+ });
4096
+ current = void 0;
4097
+ };
4098
+ for (const line of lines) {
4099
+ if (SEPARATOR_RE.test(line)) continue;
4100
+ const cmdMatch = EXEC_CMD_RE.exec(line);
4101
+ if (cmdMatch) {
4102
+ closeBlock();
4103
+ current = { command: cmdMatch[1].trim(), output: [] };
4104
+ sawCommand = true;
4105
+ continue;
4106
+ }
4107
+ const endMatch = EXEC_END_RE.exec(line);
4108
+ if (endMatch && current) {
4109
+ closeBlock(Number(endMatch[1]), endMatch[2].trim());
4110
+ continue;
4111
+ }
4112
+ if (current) {
4113
+ if (SHOWING_RE.test(line)) continue;
4114
+ current.output.push(line);
4115
+ continue;
4116
+ }
4117
+ if (header === void 0 && !sawCommand && line.trim() !== "") {
4118
+ header = line.trim();
4119
+ }
4120
+ }
4121
+ closeBlock();
4122
+ return { ...header !== void 0 ? { header } : {}, blocks };
3870
4123
  }
3871
- function reformatTableBlock(lines) {
3872
- const parsed = lines.map(parseCells);
3873
- const colCount = Math.max(...parsed.map((row) => row.length));
3874
- const normalised = parsed.map((row) => {
3875
- const padded = [...row];
3876
- while (padded.length < colCount) {
3877
- padded.push("");
4124
+
4125
+ // src/bundle/parse-ini.ts
4126
+ var SECTION_RE = /^\[(.+)\]$/;
4127
+ function parseIni(content) {
4128
+ const entries = [];
4129
+ let section = "";
4130
+ for (const rawLine of content.split("\n")) {
4131
+ const line = rawLine.trim();
4132
+ if (line === "" || line.startsWith("#") || line.startsWith(";")) continue;
4133
+ const sectionMatch = SECTION_RE.exec(line);
4134
+ if (sectionMatch) {
4135
+ section = sectionMatch[1].trim();
4136
+ continue;
3878
4137
  }
3879
- return padded;
4138
+ const eq = line.indexOf("=");
4139
+ if (eq === -1) continue;
4140
+ const key = line.slice(0, eq).trim();
4141
+ const value = line.slice(eq + 1).trim();
4142
+ if (key) entries.push({ section, key, value });
4143
+ }
4144
+ return entries;
4145
+ }
4146
+ function filterIni(entries, opts = {}) {
4147
+ const section = opts.section?.toLowerCase();
4148
+ const key = opts.key?.toLowerCase();
4149
+ return entries.filter((e) => {
4150
+ if (section !== void 0 && e.section.toLowerCase() !== section) return false;
4151
+ if (key !== void 0 && !e.key.toLowerCase().includes(key)) return false;
4152
+ return true;
3880
4153
  });
3881
- const colWidths = Array.from(
3882
- { length: colCount },
3883
- (_, col) => Math.max(
3884
- 3,
3885
- ...normalised.filter((row) => !isSeparatorRow(row)).map((row) => visualWidth(row[col]))
3886
- )
4154
+ }
4155
+
4156
+ // src/bundle/log-search.ts
4157
+ var import_node_fs4 = require("fs");
4158
+ var import_node_readline = require("readline");
4159
+
4160
+ // src/bundle/parse-log-line.ts
4161
+ var PREFIX_RE = /^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d+)\s+([A-Z]+)\s+\(([^)]*)\)\s*(.*)$/;
4162
+ var CORE_TAIL_RE = /^(\S+)\s+(\S+:\d+)\s+-\s+(.*)$/;
4163
+ var RANK_RE = /^(r\d+)\b/;
4164
+ var SEVERITY_RANK = {
4165
+ TRACE: 0,
4166
+ DEBUG: 1,
4167
+ INFO: 2,
4168
+ WARN: 3,
4169
+ UERR: 4,
4170
+ ERROR: 5,
4171
+ FATAL: 6
4172
+ };
4173
+ function severityRank(severity) {
4174
+ if (severity === void 0) return -1;
4175
+ return SEVERITY_RANK[severity] ?? -1;
4176
+ }
4177
+ function parseLogLine(line) {
4178
+ const match = PREFIX_RE.exec(line);
4179
+ if (!match) {
4180
+ return { message: line, raw: line };
4181
+ }
4182
+ const [, timestamp, severity, paren, rest] = match;
4183
+ const parts = paren.split(",");
4184
+ const pid = parts[0]?.trim() || void 0;
4185
+ const tid = parts[1]?.trim() || void 0;
4186
+ const context = parts.slice(2).join(",").trim() || void 0;
4187
+ const rank = context ? RANK_RE.exec(context)?.[1] ?? void 0 : void 0;
4188
+ const coreTail = CORE_TAIL_RE.exec(rest);
4189
+ if (coreTail) {
4190
+ const [, host, source, message] = coreTail;
4191
+ return { timestamp, severity, pid, tid, context, rank, host, source, message, raw: line };
4192
+ }
4193
+ return { timestamp, severity, pid, tid, context, rank, message: rest, raw: line };
4194
+ }
4195
+
4196
+ // src/bundle/log-search.ts
4197
+ var DEFAULT_MAX_MATCHES = 200;
4198
+ var REGEX_SCAN_MAX = 8192;
4199
+ var GRANULARITY_LEN = {
4200
+ day: 10,
4201
+ // "2026-06-11"
4202
+ hour: 13,
4203
+ // "2026-06-11 15"
4204
+ minute: 16
4205
+ // "2026-06-11 15:18"
4206
+ };
4207
+ function compileRegex(query3) {
4208
+ if (query3.regex === void 0) return void 0;
4209
+ return new RegExp(query3.regex, query3.caseSensitive ? void 0 : "i");
4210
+ }
4211
+ var TS_FLOOR = "0000-01-01 00:00:00.000";
4212
+ var TS_CEIL = "9999-12-31 23:59:59.999";
4213
+ var SAFE_PREFIX_LENS = [4, 7, 10, 13, 16, 19];
4214
+ function alignPrefixLen(len) {
4215
+ let aligned = 0;
4216
+ for (const n of SAFE_PREFIX_LENS) if (n <= len) aligned = n;
4217
+ return aligned;
4218
+ }
4219
+ function floorTimestamp(ts) {
4220
+ if (ts.length >= TS_FLOOR.length) return ts;
4221
+ const len = alignPrefixLen(ts.length);
4222
+ return ts.slice(0, len) + TS_FLOOR.slice(len);
4223
+ }
4224
+ function ceilTimestamp(ts) {
4225
+ if (ts.length >= TS_CEIL.length) return ts;
4226
+ const len = alignPrefixLen(ts.length);
4227
+ return ts.slice(0, len) + TS_CEIL.slice(len);
4228
+ }
4229
+ function matchesFilters(parsed, query3, regex, minRank) {
4230
+ if (regex && !regex.test(parsed.raw.slice(0, REGEX_SCAN_MAX))) return false;
4231
+ if (query3.minSeverity !== void 0 && severityRank(parsed.severity) < minRank) return false;
4232
+ if (query3.rank !== void 0 && parsed.rank !== query3.rank) return false;
4233
+ if (query3.fromTs !== void 0 && (parsed.timestamp === void 0 || parsed.timestamp < query3.fromTs))
4234
+ return false;
4235
+ if (query3.toTs !== void 0 && (parsed.timestamp === void 0 || parsed.timestamp > query3.toTs))
4236
+ return false;
4237
+ return true;
4238
+ }
4239
+ async function searchLogFile(filePath, query3) {
4240
+ const maxMatches = query3.maxMatches ?? DEFAULT_MAX_MATCHES;
4241
+ const minRank = query3.minSeverity !== void 0 ? severityRank(query3.minSeverity) : -Infinity;
4242
+ let regex;
4243
+ try {
4244
+ regex = compileRegex(query3);
4245
+ } catch (err) {
4246
+ const message = err instanceof Error ? err.message : String(err);
4247
+ return {
4248
+ matches: [],
4249
+ totalMatched: 0,
4250
+ linesScanned: 0,
4251
+ capped: false,
4252
+ error: `invalid regex: ${message}`
4253
+ };
4254
+ }
4255
+ const boundedQuery = {
4256
+ ...query3,
4257
+ ...query3.fromTs !== void 0 ? { fromTs: floorTimestamp(query3.fromTs) } : {},
4258
+ ...query3.toTs !== void 0 ? { toTs: ceilTimestamp(query3.toTs) } : {}
4259
+ };
4260
+ const matches = [];
4261
+ let totalMatched = 0;
4262
+ let linesScanned = 0;
4263
+ try {
4264
+ const rl = (0, import_node_readline.createInterface)({
4265
+ input: (0, import_node_fs4.createReadStream)(filePath, { encoding: "utf-8" }),
4266
+ crlfDelay: Infinity
4267
+ });
4268
+ for await (const line of rl) {
4269
+ linesScanned++;
4270
+ const parsed = parseLogLine(line);
4271
+ if (!matchesFilters(parsed, boundedQuery, regex, minRank)) continue;
4272
+ totalMatched++;
4273
+ if (matches.length < maxMatches) {
4274
+ matches.push({
4275
+ lineNumber: linesScanned,
4276
+ ...parsed.timestamp !== void 0 ? { timestamp: parsed.timestamp } : {},
4277
+ ...parsed.severity !== void 0 ? { severity: parsed.severity } : {},
4278
+ ...parsed.rank !== void 0 ? { rank: parsed.rank } : {},
4279
+ message: parsed.message,
4280
+ raw: parsed.raw
4281
+ });
4282
+ }
4283
+ }
4284
+ } catch (err) {
4285
+ const message = err instanceof Error ? err.message : String(err);
4286
+ return {
4287
+ matches,
4288
+ totalMatched,
4289
+ linesScanned,
4290
+ capped: totalMatched > matches.length,
4291
+ error: message
4292
+ };
4293
+ }
4294
+ return { matches, totalMatched, linesScanned, capped: totalMatched > matches.length };
4295
+ }
4296
+ async function aggregateTimeline(filePath, query3 = {}) {
4297
+ const granularity = query3.granularity ?? "hour";
4298
+ const prefixLen = GRANULARITY_LEN[granularity];
4299
+ const minRank = severityRank(query3.minSeverity ?? "WARN");
4300
+ const buckets = /* @__PURE__ */ new Map();
4301
+ let linesScanned = 0;
4302
+ let totalCounted = 0;
4303
+ try {
4304
+ const rl = (0, import_node_readline.createInterface)({
4305
+ input: (0, import_node_fs4.createReadStream)(filePath, { encoding: "utf-8" }),
4306
+ crlfDelay: Infinity
4307
+ });
4308
+ for await (const line of rl) {
4309
+ linesScanned++;
4310
+ const parsed = parseLogLine(line);
4311
+ if (parsed.timestamp === void 0 || parsed.severity === void 0) continue;
4312
+ if (severityRank(parsed.severity) < minRank) continue;
4313
+ if (query3.rank !== void 0 && parsed.rank !== query3.rank) continue;
4314
+ const key = parsed.timestamp.slice(0, prefixLen);
4315
+ const bucket = buckets.get(key) ?? {};
4316
+ bucket[parsed.severity] = (bucket[parsed.severity] ?? 0) + 1;
4317
+ buckets.set(key, bucket);
4318
+ totalCounted++;
4319
+ }
4320
+ } catch (err) {
4321
+ const message = err instanceof Error ? err.message : String(err);
4322
+ return { buckets: [], linesScanned, totalCounted, error: message };
4323
+ }
4324
+ const result = [];
4325
+ for (const [bucket, counts] of buckets) {
4326
+ const total = Object.values(counts).reduce((a, b) => a + b, 0);
4327
+ result.push({ bucket, counts, total });
4328
+ }
4329
+ return { buckets: result, linesScanned, totalCounted };
4330
+ }
4331
+
4332
+ // src/bundle/bundle-index.ts
4333
+ var import_promises4 = require("fs/promises");
4334
+ var import_node_path5 = require("path");
4335
+
4336
+ // src/bundle/classify-file.ts
4337
+ var ROLLING_ID_RE = /core-gpudb-rolling-(r\d+|hm)\.log$/;
4338
+ var EXE_ID_RE = /gpudb-exe-(r\d+|hm)-/;
4339
+ var HOST_RE = /\b(node\w+)\b/;
4340
+ function rankOrService(id) {
4341
+ return id === "hm" ? { service: "host-manager" } : { rank: id };
4342
+ }
4343
+ function basename2(relPath) {
4344
+ const parts = relPath.split("/");
4345
+ return parts[parts.length - 1] ?? relPath;
4346
+ }
4347
+ function dirOf(relPath) {
4348
+ const parts = relPath.split("/");
4349
+ return parts.length > 1 ? parts[parts.length - 2] : "";
4350
+ }
4351
+ function inferHost(relPath) {
4352
+ return HOST_RE.exec(relPath)?.[1] ?? void 0;
4353
+ }
4354
+ function componentName(base) {
4355
+ return base.replace(/(\.log)+$/, "").replace(/^core-gpudb-/, "").replace(/^gpudb-/, "").replace(/-node\w+$/, "");
4356
+ }
4357
+ function classifyFile(relPath) {
4358
+ const base = basename2(relPath);
4359
+ const dir = dirOf(relPath);
4360
+ const host = inferHost(relPath);
4361
+ if (base.endsWith(".conf")) {
4362
+ return { kind: "config", ...host ? { host } : {} };
4363
+ }
4364
+ if (base === "logfiles.txt") {
4365
+ return { kind: "manifest", ...host ? { host } : {} };
4366
+ }
4367
+ if (base === "errors.txt" || base.endsWith("erros.txt")) {
4368
+ return { kind: "collection-errors", ...host ? { host } : {} };
4369
+ }
4370
+ if (base === "gpudb.txt") {
4371
+ return { kind: "version-info", ...host ? { host } : {} };
4372
+ }
4373
+ const exeId = EXE_ID_RE.exec(base);
4374
+ if (exeId) {
4375
+ return { kind: "process-info", ...rankOrService(exeId[1]), ...host ? { host } : {} };
4376
+ }
4377
+ if (base.endsWith(".log")) {
4378
+ const rolling = ROLLING_ID_RE.exec(base);
4379
+ if (rolling) {
4380
+ return { kind: "core-log", ...rankOrService(rolling[1]), ...host ? { host } : {} };
4381
+ }
4382
+ if (dir === "logs") {
4383
+ return { kind: "loki-tail", component: componentName(base), ...host ? { host } : {} };
4384
+ }
4385
+ return { kind: "component-log", component: componentName(base), ...host ? { host } : {} };
4386
+ }
4387
+ if (base.endsWith(".txt")) {
4388
+ return { kind: "os-diag", ...host ? { host } : {} };
4389
+ }
4390
+ return { kind: "unknown", ...host ? { host } : {} };
4391
+ }
4392
+
4393
+ // src/bundle/bundle-index.ts
4394
+ async function buildIndex(rootDir) {
4395
+ let relPaths;
4396
+ try {
4397
+ relPaths = await (0, import_promises4.readdir)(rootDir, { recursive: true });
4398
+ } catch {
4399
+ return [];
4400
+ }
4401
+ const settled = await Promise.all(
4402
+ relPaths.map(async (rel) => {
4403
+ const relPath = rel.split("\\").join("/");
4404
+ const absPath = (0, import_node_path5.join)(rootDir, rel);
4405
+ try {
4406
+ const s = await (0, import_promises4.lstat)(absPath);
4407
+ if (s.isSymbolicLink() || !s.isFile()) return null;
4408
+ const c = classifyFile(relPath);
4409
+ return {
4410
+ relPath,
4411
+ absPath,
4412
+ kind: c.kind,
4413
+ ...c.rank !== void 0 ? { rank: c.rank } : {},
4414
+ ...c.service !== void 0 ? { service: c.service } : {},
4415
+ ...c.host !== void 0 ? { host: c.host } : {},
4416
+ ...c.component !== void 0 ? { component: c.component } : {},
4417
+ sizeBytes: s.size
4418
+ };
4419
+ } catch {
4420
+ return null;
4421
+ }
4422
+ })
4423
+ );
4424
+ return settled.filter((e) => e !== null).sort((a, b) => a.relPath.localeCompare(b.relPath));
4425
+ }
4426
+
4427
+ // src/bundle/BundleSource.ts
4428
+ var GPUDB_VERSION_RE = /GPUdb version\s*:\s*(\S+)/;
4429
+ function selectLogFiles(index, opts) {
4430
+ if (opts.component !== void 0) {
4431
+ return index.filter((e) => e.kind === "component-log" && e.component === opts.component);
4432
+ }
4433
+ if (opts.hostManager) {
4434
+ return index.filter((e) => e.kind === "core-log" && e.service === "host-manager");
4435
+ }
4436
+ let core = index.filter(
4437
+ (e) => e.kind === "core-log" && (opts.rank === void 0 || e.rank === opts.rank)
4438
+ );
4439
+ if (core.length === 0) {
4440
+ core = index.filter((e) => e.kind === "loki-tail");
4441
+ }
4442
+ if (opts.includeComponents) {
4443
+ return [...core, ...index.filter((e) => e.kind === "component-log")];
4444
+ }
4445
+ return core;
4446
+ }
4447
+ function toLineQuery(q) {
4448
+ return {
4449
+ ...q.regex !== void 0 ? { regex: q.regex } : {},
4450
+ ...q.caseSensitive !== void 0 ? { caseSensitive: q.caseSensitive } : {},
4451
+ ...q.minSeverity !== void 0 ? { minSeverity: q.minSeverity } : {},
4452
+ ...q.fromTs !== void 0 ? { fromTs: q.fromTs } : {},
4453
+ ...q.toTs !== void 0 ? { toTs: q.toTs } : {},
4454
+ ...q.maxMatches !== void 0 ? { maxMatches: q.maxMatches } : {}
4455
+ };
4456
+ }
4457
+ function toTimelineLineQuery(q) {
4458
+ return {
4459
+ ...q.minSeverity !== void 0 ? { minSeverity: q.minSeverity } : {},
4460
+ ...q.granularity !== void 0 ? { granularity: q.granularity } : {}
4461
+ };
4462
+ }
4463
+ async function createBundleSource(rootDir) {
4464
+ const root = (0, import_node_path6.resolve)(rootDir);
4465
+ const index = await buildIndex(root);
4466
+ const resolve3 = (relPath) => {
4467
+ const abs = (0, import_node_path6.resolve)(root, relPath);
4468
+ if (abs !== root && !abs.startsWith(root + import_node_path6.sep)) return void 0;
4469
+ return abs;
4470
+ };
4471
+ const findByKind = (kind) => index.find((e) => e.kind === kind);
4472
+ const inventoryValue = (() => {
4473
+ const byKind = {};
4474
+ const rankSet = /* @__PURE__ */ new Set();
4475
+ const serviceSet = /* @__PURE__ */ new Set();
4476
+ let totalBytes = 0;
4477
+ for (const e of index) {
4478
+ byKind[e.kind] = (byKind[e.kind] ?? 0) + 1;
4479
+ totalBytes += e.sizeBytes;
4480
+ if (e.rank) rankSet.add(e.rank);
4481
+ if (e.service) serviceSet.add(e.service);
4482
+ }
4483
+ return {
4484
+ totalFiles: index.length,
4485
+ totalBytes,
4486
+ byKind,
4487
+ ranks: [...rankSet].sort(),
4488
+ services: [...serviceSet].sort()
4489
+ };
4490
+ })();
4491
+ const detectVersion = async () => {
4492
+ const versionFile = findByKind("version-info");
4493
+ if (versionFile) {
4494
+ try {
4495
+ const parsed = parseSysinfo(await (0, import_promises5.readFile)(versionFile.absPath, "utf-8"));
4496
+ for (const block of parsed.blocks) {
4497
+ const m = GPUDB_VERSION_RE.exec(block.output);
4498
+ if (m) return m[1];
4499
+ }
4500
+ } catch {
4501
+ }
4502
+ }
4503
+ const configFile = findByKind("config");
4504
+ if (configFile) {
4505
+ try {
4506
+ const entries = parseIni(await (0, import_promises5.readFile)(configFile.absPath, "utf-8"));
4507
+ return entries.find((e) => e.key === "file_version")?.value;
4508
+ } catch {
4509
+ return void 0;
4510
+ }
4511
+ }
4512
+ return void 0;
4513
+ };
4514
+ const readConfig = async (opts = {}) => {
4515
+ const configFile = index.find((e) => e.kind === "config" && e.relPath.endsWith("gpudb.conf")) ?? findByKind("config");
4516
+ if (!configFile) return { error: "no gpudb.conf found in bundle" };
4517
+ try {
4518
+ const entries = parseIni(await (0, import_promises5.readFile)(configFile.absPath, "utf-8"));
4519
+ return { entries: filterIni(entries, opts), file: configFile.relPath };
4520
+ } catch (err) {
4521
+ return { error: err instanceof Error ? err.message : String(err) };
4522
+ }
4523
+ };
4524
+ const readSysinfo = async (name) => {
4525
+ const entry = index.find(
4526
+ (e) => e.relPath === name || e.relPath.endsWith("/" + name) || e.relPath.split("/").pop() === name
4527
+ );
4528
+ if (!entry) return { error: `no bundle file named "${name}"` };
4529
+ const abs = resolve3(entry.relPath);
4530
+ if (!abs) return { error: `path "${name}" escapes the bundle root` };
4531
+ try {
4532
+ return parseSysinfo(await (0, import_promises5.readFile)(abs, "utf-8"));
4533
+ } catch (err) {
4534
+ return { error: err instanceof Error ? err.message : String(err) };
4535
+ }
4536
+ };
4537
+ const searchLogs = async (query3) => {
4538
+ const files = selectLogFiles(index, query3);
4539
+ const lineQuery = toLineQuery(query3);
4540
+ const matches = [];
4541
+ const filesScanned = [];
4542
+ let totalMatched = 0;
4543
+ let linesScanned = 0;
4544
+ const maxMatches = query3.maxMatches ?? DEFAULT_MAX_MATCHES;
4545
+ for (const file of files) {
4546
+ const remaining = Math.max(0, maxMatches - matches.length);
4547
+ const r = await searchLogFile(file.absPath, { ...lineQuery, maxMatches: remaining });
4548
+ filesScanned.push(file.relPath);
4549
+ totalMatched += r.totalMatched;
4550
+ linesScanned += r.linesScanned;
4551
+ for (const m of r.matches) matches.push({ ...m, file: file.relPath });
4552
+ }
4553
+ return {
4554
+ matches,
4555
+ totalMatched,
4556
+ linesScanned,
4557
+ filesScanned,
4558
+ capped: totalMatched > matches.length
4559
+ };
4560
+ };
4561
+ const logTimeline = async (query3) => {
4562
+ const files = selectLogFiles(index, query3);
4563
+ const lineQuery = toTimelineLineQuery(query3);
4564
+ const merged = /* @__PURE__ */ new Map();
4565
+ const filesScanned = [];
4566
+ let linesScanned = 0;
4567
+ let totalCounted = 0;
4568
+ for (const file of files) {
4569
+ const r = await aggregateTimeline(file.absPath, lineQuery);
4570
+ filesScanned.push(file.relPath);
4571
+ linesScanned += r.linesScanned;
4572
+ totalCounted += r.totalCounted;
4573
+ for (const b of r.buckets) {
4574
+ const existing = merged.get(b.bucket) ?? {};
4575
+ for (const [sev, n] of Object.entries(b.counts)) existing[sev] = (existing[sev] ?? 0) + n;
4576
+ merged.set(b.bucket, existing);
4577
+ }
4578
+ }
4579
+ const buckets = [...merged.entries()].sort((a, b) => a[0].localeCompare(b[0])).map(([bucket, counts]) => ({
4580
+ bucket,
4581
+ counts,
4582
+ total: Object.values(counts).reduce((x, y) => x + y, 0)
4583
+ }));
4584
+ return { buckets, linesScanned, totalCounted, filesScanned };
4585
+ };
4586
+ const collectionErrors = async () => {
4587
+ const files = index.filter((e) => e.kind === "collection-errors");
4588
+ const lines = [];
4589
+ for (const file of files) {
4590
+ try {
4591
+ const content = await (0, import_promises5.readFile)(file.absPath, "utf-8");
4592
+ for (const line of content.split("\n")) {
4593
+ const trimmed = line.trim();
4594
+ if (trimmed !== "" && !/^-{3,}$/.test(trimmed)) lines.push(trimmed);
4595
+ }
4596
+ } catch {
4597
+ }
4598
+ }
4599
+ return lines;
4600
+ };
4601
+ return {
4602
+ root,
4603
+ listFiles: () => index,
4604
+ inventory: () => inventoryValue,
4605
+ resolve: resolve3,
4606
+ detectVersion,
4607
+ readConfig,
4608
+ readSysinfo,
4609
+ searchLogs,
4610
+ logTimeline,
4611
+ collectionErrors
4612
+ };
4613
+ }
4614
+
4615
+ // src/bundle/verify-bundle.ts
4616
+ var ARCHIVE_RE = /\.(tgz|tar\.gz|tar|gz|zip)$/i;
4617
+ var EXPECTED_KINDS = ["config", "core-log"];
4618
+ async function verifyBundle(bundlePath) {
4619
+ let info;
4620
+ try {
4621
+ info = await (0, import_promises6.stat)(bundlePath);
4622
+ } catch {
4623
+ return { ok: false, error: `bundle path does not exist: ${bundlePath}` };
4624
+ }
4625
+ if (!info.isDirectory()) {
4626
+ if (ARCHIVE_RE.test(bundlePath)) {
4627
+ return {
4628
+ ok: false,
4629
+ error: `bundle mode expects an extracted directory, not an archive. Run \`tar xzf ${bundlePath}\` and pass the resulting directory.`
4630
+ };
4631
+ }
4632
+ return { ok: false, error: `bundle path is not a directory: ${bundlePath}` };
4633
+ }
4634
+ const bundleSource = await createBundleSource(bundlePath);
4635
+ const inventory = bundleSource.inventory();
4636
+ if (inventory.totalFiles === 0) {
4637
+ return { ok: false, error: `no readable files found in bundle directory: ${bundlePath}` };
4638
+ }
4639
+ const missingExpected = EXPECTED_KINDS.filter((k) => (inventory.byKind[k] ?? 0) === 0);
4640
+ const kineticaVersion = await bundleSource.detectVersion();
4641
+ return {
4642
+ ok: true,
4643
+ bundleSource,
4644
+ ...kineticaVersion !== void 0 ? { kineticaVersion } : {},
4645
+ inventory,
4646
+ missingExpected
4647
+ };
4648
+ }
4649
+
4650
+ // src/tools/bundle/load-bundle.ts
4651
+ var BundleLoadSchema = import_zod23.z.object({
4652
+ path: import_zod23.z.string().min(1).optional()
4653
+ });
4654
+ async function bundleLoad(holder, args, promptForPath, confirmPath) {
4655
+ let path2;
4656
+ if (args.path !== void 0) {
4657
+ if (confirmPath && !await confirmPath(args.path)) {
4658
+ return {
4659
+ ok: false,
4660
+ status: 0,
4661
+ error: `Operator declined to load a bundle from "${args.path}".`,
4662
+ raw: args.path
4663
+ };
4664
+ }
4665
+ path2 = args.path;
4666
+ } else {
4667
+ path2 = promptForPath ? await promptForPath() : void 0;
4668
+ }
4669
+ if (!path2) {
4670
+ return {
4671
+ ok: false,
4672
+ status: 0,
4673
+ error: "No bundle path provided and no directory picker is available. Ask the operator for the extracted bundle directory path and pass it as `path`.",
4674
+ raw: ""
4675
+ };
4676
+ }
4677
+ const result = await verifyBundle(path2);
4678
+ if (!result.ok) {
4679
+ return { ok: false, status: 0, error: result.error, raw: path2 };
4680
+ }
4681
+ holder.set(result.bundleSource);
4682
+ const missingNote = result.missingExpected.length > 0 ? ` Missing expected artifact(s): ${result.missingExpected.join(", ")} (treat as Evidence Gaps).` : "";
4683
+ return {
4684
+ ok: true,
4685
+ // Loading a bundle is SETUP, not an investigation. Do not auto-proceed — the
4686
+ // operator hasn't said what they want yet. End the turn and ask.
4687
+ note: `Bundle attached. Do NOT start investigating yet \u2014 ask the operator what they want to investigate, then proceed.${missingNote}`,
4688
+ data: {
4689
+ loaded: true,
4690
+ path: path2,
4691
+ detected_version: result.kineticaVersion ?? "unknown",
4692
+ total_files: result.inventory.totalFiles,
4693
+ ranks_present: result.inventory.ranks.join(", ") || "none",
4694
+ counts_by_kind: result.inventory.byKind,
4695
+ missing_expected: result.missingExpected.join(", ") || "none"
4696
+ }
4697
+ };
4698
+ }
4699
+
4700
+ // src/tools/bundle/index.ts
4701
+ var BUNDLE_TOOL_NAMES = [
4702
+ "kinetica_load_bundle",
4703
+ "kinetica_bundle_list_files",
4704
+ "kinetica_bundle_log_timeline",
4705
+ "kinetica_bundle_search_logs",
4706
+ "kinetica_bundle_read_config",
4707
+ "kinetica_bundle_read_sysinfo"
4708
+ ];
4709
+ var text = (s) => ({ content: [{ type: "text", text: s }] });
4710
+ function notLoaded() {
4711
+ return {
4712
+ ok: false,
4713
+ status: 0,
4714
+ error: "No support bundle is loaded. Ask the operator for the extracted bundle directory path and call kinetica_load_bundle first.",
4715
+ raw: ""
4716
+ };
4717
+ }
4718
+ async function withSource(holder, fn) {
4719
+ const source = holder.get();
4720
+ if (!source) return applyOutputPipeline(notLoaded());
4721
+ return applyOutputPipeline(await fn(source));
4722
+ }
4723
+ function makeLoadBundleTool(holder, deps) {
4724
+ return (0, import_claude_agent_sdk4.tool)(
4725
+ "kinetica_load_bundle",
4726
+ "Attach an extracted Kinetica support bundle (gpudb_sysinfo directory) so the kinetica_bundle_* tools can read its logs/config/host-diagnostics. When the operator wants to analyze a support bundle, call this tool WITHOUT a path \u2014 they will be shown an interactive directory picker to choose it. Do NOT ask for the path in chat. (You may pass an explicit `path` if the operator already gave you one; it must be a directory, not a .tgz.)",
4727
+ BundleLoadSchema.shape,
4728
+ async (args) => text(
4729
+ applyOutputPipeline(await bundleLoad(holder, args, deps?.promptForPath, deps?.confirmPath))
4730
+ ),
4731
+ { annotations: { readOnly: true } }
4732
+ );
4733
+ }
4734
+ function makeListFilesTool(holder) {
4735
+ return (0, import_claude_agent_sdk4.tool)(
4736
+ "kinetica_bundle_list_files",
4737
+ "Inventory the attached support bundle: detected GPUdb version, ranks present (numeric ranks only), services present (e.g. host-manager \u2014 a singleton service, NOT a rank), file counts/sizes by kind, and how many collection commands failed. Each file row includes a `description` of what it contains (e.g. mem.txt \u2192 memory/THP, gpu.txt \u2192 nvidia-smi) so you can pick the right file without reading it. Call this FIRST after a bundle is attached. Optional `kind` filters the file list (e.g. core-log, component-log, config, os-diag).",
4738
+ BundleListFilesSchema.shape,
4739
+ async (args) => text(await withSource(holder, (s) => bundleListFiles(s, args))),
4740
+ { annotations: { readOnly: true } }
4741
+ );
4742
+ }
4743
+ function makeLogTimelineTool(holder) {
4744
+ return (0, import_claude_agent_sdk4.tool)(
4745
+ "kinetica_bundle_log_timeline",
4746
+ "Aggregate bundle log lines into per-time-bucket severity counts across ranks \u2014 the incident shape. Call this BEFORE search_logs to find WHEN errors spiked, then drill in with a tight time window. Defaults: min_severity=WARN, granularity=hour, core logs (all ranks AND the host manager). Narrow with rank=<r0|r1|\u2026> (numeric ranks only) or host_manager=true for the host-manager log (a service, not a rank). Set include_components=true or component=<name> to include component logs. Note severity order is WARN < UERR < ERROR < FATAL, so min_severity=ERROR EXCLUDES UERR (user-error) lines \u2014 use UERR or WARN to include them.",
4747
+ BundleLogTimelineSchema.shape,
4748
+ async (args) => text(await withSource(holder, (s) => bundleLogTimeline(s, args))),
4749
+ { annotations: { readOnly: true } }
4750
+ );
4751
+ }
4752
+ function makeSearchLogsTool(holder) {
4753
+ return (0, import_claude_agent_sdk4.tool)(
4754
+ "kinetica_bundle_search_logs",
4755
+ "Search bundle logs for matching lines by regex (case-insensitive), min_severity, time window (from_ts/to_ts as 'YYYY-MM-DD HH:MM:SS.mmm'; a partial prefix like a timeline bucket label '2026-06-11 15' also works \u2014 it is widened to cover that whole period), and rank/host_manager/component. Streamed and bounded \u2014 the default 200-match cap is shared across all files; when capped, narrow the query (the total may be a lower bound). Defaults to core logs across all ranks AND the host manager; narrow with rank=<r0|r1|\u2026> (numeric ranks only) or host_manager=true for the host-manager log (a service, not a rank); set component or include_components for component logs. Severity order is WARN < UERR < ERROR < FATAL, so min_severity=ERROR EXCLUDES UERR (user-error) lines.",
4756
+ BundleSearchLogsSchema.shape,
4757
+ async (args) => text(await withSource(holder, (s) => bundleSearchLogs(s, args))),
4758
+ { annotations: { readOnly: true } }
4759
+ );
4760
+ }
4761
+ function makeReadConfigTool(holder) {
4762
+ return (0, import_claude_agent_sdk4.tool)(
4763
+ "kinetica_bundle_read_config",
4764
+ "Read gpudb.conf from the attached bundle (the real on-disk config). Optionally filter by `section` (exact, case-insensitive) and/or `key` (substring, case-insensitive). Interpolation references like ${gaia.host0.address} are returned verbatim.",
4765
+ BundleReadConfigSchema.shape,
4766
+ async (args) => text(await withSource(holder, (s) => bundleReadConfig(s, args))),
4767
+ { annotations: { readOnly: true } }
4768
+ );
4769
+ }
4770
+ function makeReadSysinfoTool(holder) {
4771
+ return (0, import_claude_agent_sdk4.tool)(
4772
+ "kinetica_bundle_read_sysinfo",
4773
+ "Read an OS-diagnostic / process / version file's command blocks by name (e.g. mem.txt, cpu.txt, disk.txt, gpu.txt, net.txt, ps.txt, gpudb.txt, gpudb-exe-r0-*.txt). Returns each wrapped shell command and its output \u2014 host-level facts (memory, GPU, disk, THP, process args) the live endpoints never expose.",
4774
+ BundleReadSysinfoSchema.shape,
4775
+ async (args) => text(await withSource(holder, (s) => bundleReadSysinfo(s, args))),
4776
+ { annotations: { readOnly: true } }
4777
+ );
4778
+ }
4779
+ function makeBundleTools(holder, deps) {
4780
+ return [
4781
+ makeLoadBundleTool(holder, deps),
4782
+ makeListFilesTool(holder),
4783
+ makeLogTimelineTool(holder),
4784
+ makeSearchLogsTool(holder),
4785
+ makeReadConfigTool(holder),
4786
+ makeReadSysinfoTool(holder)
4787
+ ];
4788
+ }
4789
+ function createBundleRegistry() {
4790
+ return BUNDLE_TOOL_NAMES.reduce(
4791
+ (registry, name) => registry.registerReadOnlyTool(name),
4792
+ createRegistry()
4793
+ );
4794
+ }
4795
+
4796
+ // src/tools/bundle/catalog.ts
4797
+ var BUNDLE_TOOL_CATALOG = {
4798
+ kinetica_load_bundle: {
4799
+ reveals: "Attaches an extracted support bundle (directory path) for offline analysis",
4800
+ whenToUse: "When the operator wants to analyze a support bundle \u2014 ask for the path, then load"
4801
+ },
4802
+ kinetica_bundle_list_files: {
4803
+ reveals: "Bundle inventory: detected version, ranks, file kinds/sizes, failed collections",
4804
+ whenToUse: "First action of every bundle investigation (orientation)"
4805
+ },
4806
+ kinetica_bundle_log_timeline: {
4807
+ reveals: "WARN+ log lines bucketed by time + severity across ranks (incident shape)",
4808
+ whenToUse: "Right after list_files \u2014 find WHEN errors spiked before drilling in"
4809
+ },
4810
+ kinetica_bundle_search_logs: {
4811
+ reveals: "Matching log lines by regex/severity/time-window/rank (bounded, streamed)",
4812
+ whenToUse: "Drill into a time window or error pattern surfaced by the timeline"
4813
+ },
4814
+ kinetica_bundle_read_config: {
4815
+ reveals: "gpudb.conf entries (the real on-disk config), filterable by section/key",
4816
+ whenToUse: "Config drift, misconfiguration, parameter verification"
4817
+ },
4818
+ kinetica_bundle_read_sysinfo: {
4819
+ reveals: "OS-diag / process / version command blocks (mem, cpu, disk, gpu, ps, gpudb.txt)",
4820
+ whenToUse: "Host-level facts: memory pressure, GPU presence, disk, THP, process args"
4821
+ }
4822
+ };
4823
+ function buildBundleEvidenceChecklist() {
4824
+ const rows = BUNDLE_TOOL_NAMES.map((name) => {
4825
+ const entry = BUNDLE_TOOL_CATALOG[name];
4826
+ return `| ${name} | ${entry.reveals} | ${entry.whenToUse} |`;
4827
+ });
4828
+ return [
4829
+ "| Tool | What it reveals | When to use |",
4830
+ "|------|----------------|-------------|",
4831
+ ...rows
4832
+ ].join("\n");
4833
+ }
4834
+
4835
+ // src/agent/bundle-system-prompt.ts
4836
+ function buildBundleSystemPrompt(kineticaVersion, playbooks, references, bundleReferences) {
4837
+ const t = "`";
4838
+ const versionSection = kineticaVersion ? `**Kinetica Version:** ${kineticaVersion} (detected from the bundle's gpudb.txt / gpudb.conf)` : "**Kinetica Version:** Unknown \u2014 check gpudb.txt via kinetica_bundle_read_sysinfo, or file_version via kinetica_bundle_read_config.";
4839
+ return `You are an expert Kinetica GPU database administrator and diagnostician. You are operating in OFFLINE BUNDLE MODE: instead of a live database, you are investigating an extracted support bundle (gpudb_sysinfo) \u2014 a snapshot of logs, configuration, and host diagnostics captured from a node at a point in time.
4840
+
4841
+ ${versionSection}
4842
+
4843
+ ---
4844
+
4845
+ ## OFFLINE BUNDLE MODE \u2014 What This Means
4846
+
4847
+ **You are reading frozen, point-in-time evidence \u2014 not a live system.**
4848
+
4849
+ - You CANNOT run SQL, query system tables, re-probe the cluster, or apply fixes. There are no mutation tools. Your job is to diagnose from the captured files and RECOMMEND remediation for the operator to apply against the live system later.
4850
+ - The single highest-value evidence here is the **logs** \u2014 the live system exposes no log endpoint, so this is the one place the incident's narrative (the lead-up, the error cascade, the crash) is visible. Lean on them.
4851
+ - A bundle covers **one node**. It may contain multiple ranks (e.g. r0, r1) plus the host manager. The host manager is a singleton service (port 9300), **not a rank** \u2014 to search/timeline its log, pass ${t}host_manager: true${t} (NOT ${t}rank: "hm"${t}); ${t}kinetica_bundle_list_files${t} lists it under ${t}services_present${t}. Cross-node correlation is not possible from a single bundle.
4852
+ - **Do not assume cluster-wide clock synchronization.** Correlate events by message content as well as timestamp; note when timing is ambiguous.
4853
+ - Some collection commands may have FAILED (e.g. nvidia-smi on a CPU-only host). ${t}kinetica_bundle_list_files${t} reports how many \u2014 treat absent artifacts as Evidence Gaps, not as healthy.
4854
+
4855
+ ---
4856
+
4857
+ ## Investigation Protocol (read-only)
4858
+
4859
+ ### Pre-Investigation: Announce Your Plan
4860
+
4861
+ Before gathering evidence, announce a brief 2-3 line plan: restate the issue, list the first tools you'll use, then begin immediately.
4862
+
4863
+ ### Round 1 \u2014 Orient
4864
+
4865
+ - ${t}kinetica_bundle_list_files${t} \u2014 **ALWAYS FIRST.** Learn the detected version, which ranks are present, what file kinds exist, and how many collections failed.
4866
+ - ${t}kinetica_bundle_log_timeline${t} (min_severity: WARN) \u2014 get the incident shape: when did WARN/ERROR/FATAL spike, and on which rank?
4867
+
4868
+ ### Round 2 \u2014 Drill Down
4869
+
4870
+ Based on the timeline, narrow in:
4871
+ - ${t}kinetica_bundle_search_logs${t} \u2014 search the spike window by regex/severity/rank. You can pass the timeline's hot bucket label straight into from_ts/to_ts (e.g. from_ts/to_ts = ${t}2026-06-11 15${t} searches that whole hour). Look for FATAL/ERROR clusters, stack traces, OOM, segfaults, failed allocations, stale-rank/heartbeat loss, rebalance failures. Remember UERR (user errors) rank below ERROR \u2014 use min_severity=WARN or UERR to include them.
4872
+ - ${t}kinetica_bundle_read_sysinfo${t} \u2014 corroborate with host facts: mem.txt (memory pressure, swap, THP), gpu.txt (GPU presence/OOM), disk.txt (disk full), cpu.txt, ps.txt, gpudb-exe-*.txt (process args/limits).
4873
+
4874
+ ### Round 3 \u2014 Confirm
4875
+
4876
+ - ${t}kinetica_bundle_read_config${t} \u2014 check gpudb.conf for misconfiguration / config-drift relevant to your hypothesis (tier limits, thread pools, ports, HA).
4877
+ - Re-search logs to confirm the root-cause sequence.
4878
+
4879
+ After Round 3 you MUST write the report \u2014 even if uncertainty remains. There are no mutation or verification rounds in bundle mode: you recommend, you do not apply.
4880
+
4881
+ ### Parallel Tool Calls
4882
+
4883
+ Issue independent reads together where possible (e.g. timeline + list_files, or a log search alongside a sysinfo read).
4884
+
4885
+ ---
4886
+
4887
+ ## Evidence Checklist \u2014 Bundle Tools
4888
+
4889
+ ${buildBundleEvidenceChecklist()}
4890
+
4891
+ ---
4892
+
4893
+ ${buildFailurePatternsSection(playbooks)}
4894
+
4895
+ ${buildReferenceSection([...bundleReferences ?? [], ...references ?? []])}
4896
+
4897
+ ---
4898
+
4899
+ ## Analysis Instructions
4900
+
4901
+ ### Commit to the Best Hypothesis
4902
+
4903
+ After gathering evidence, name specific root causes \u2014 no generic hedging.
4904
+
4905
+ **DO:**
4906
+ - "Root cause: rank 0 crashed with a segmentation fault (signal 11) at 15:18:52 (core-gpudb-rolling-r0.log:Job.cpp:9), preceded by 57 ERROR lines in the 15:00 hour."
4907
+ - "If uncertain, rank top 2-3 hypotheses by likelihood: Primary (70%): X; Secondary (25%): Y."
4908
+
4909
+ **DO NOT:**
4910
+ - "There could be various reasons..." / "Further investigation may be needed..."
4911
+
4912
+ ### Tie Evidence to Conclusions
4913
+
4914
+ Every conclusion must cite specific evidence \u2014 a file, a timestamp, a log line, a config key. Example: "GPU OOM is unlikely: gpu.txt shows nvidia-smi FAILED (exit 127) and gpudb-exe shows no --gpu rank args \u2014 this is a CPU-only host."
4915
+
4916
+ ### Evidence Gap Handling
4917
+
4918
+ Note gaps and continue \u2014 never halt on a missing artifact:
4919
+ - "Host memory at crash: unavailable (mem.txt is a point-in-time snapshot taken during collection, not at crash time)."
4920
+ - "GPU metrics: unavailable (nvidia-smi collection FAILED \u2014 CPU-only host)."
4921
+
4922
+ ---
4923
+
4924
+ ## Fix Instructions
4925
+
4926
+ Provide specific, actionable remediation tied to your findings, as a numbered list. Because you cannot act on the bundle, frame everything as recommendations for the operator to apply to the live system:
4927
+
4928
+ 1. Immediate manual actions the operator should take on the live system
4929
+ 2. Configuration changes to prevent recurrence (cite the gpudb.conf key + value)
4930
+ 3. Monitoring/alerting improvements
4931
+ 4. What to capture or verify on the live system to close remaining Evidence Gaps
4932
+
4933
+ ---
4934
+
4935
+ ## Post-Report Behavior
4936
+
4937
+ 1. Present the finished report in your response so the operator can read it.
4938
+ 2. **Ask BEFORE saving \u2014 never save unprompted.** After presenting the report, ask exactly: "Would you like me to save this report to disk? (yes/no)" and then STOP \u2014 end your turn and wait for the operator's answer. Do NOT call ${t}save_report${t} in the same turn as the question; the question must come first. Save only if they answer yes. (Exception: if checkpointing under budget pressure with a ${t}partial: true${t} report, save immediately without asking \u2014 preserving findings beats the prompt.)
4939
+ 3. After saving (or after the operator declines), ask: "Would you like to investigate another issue in this bundle, or end the session?"
4940
+ 4. On session end: summarize issues investigated and list saved report paths, then exit.
4941
+
4942
+ ---
4943
+
4944
+ ## Budget & Length Awareness
4945
+
4946
+ The session has a per-session budget guard that can end the run early. If the operator warns that the guard is approaching, STOP gathering evidence, call ${t}save_report${t} with ${t}partial: true${t}, state your best current hypothesis, and wind down. A partial report beats none. Treat the guard as a normal limit, never an error.
4947
+
4948
+ ---
4949
+
4950
+ ## Output Formatting
4951
+
4952
+ Synthesize findings into clean markdown tables (3-6 columns, **bold** key identifiers, consistent ${t}OK${t}/${t}WARN${t}/${t}ERROR${t} indicators). Do NOT dump raw log output \u2014 extract the most relevant lines.
4953
+
4954
+ ---
4955
+
4956
+ ## REPORT TEMPLATE
4957
+
4958
+ At the end of each investigation, generate a structured markdown report using this EXACT template and section order:
4959
+
4960
+ \`\`\`markdown
4961
+ ` + REPORT_TEMPLATE + `\`\`\`
4962
+
4963
+ **CRITICAL:** Use this exact section order. The metadata table comes first. Summary before Remediation. Evidence Collected before Evidence Gaps.
4964
+
4965
+ **Bundle-mode report notes:**
4966
+ - In the metadata, make clear this diagnosis is from an offline support bundle (note the node and detected version).
4967
+ - "Mutations Applied" will always be "None (offline bundle \u2014 read-only)". "Post-Remediation Verification" should state that verification requires re-running diagnostics against the live system.
4968
+ - "Evidence Collected" \u2014 cite specific files, timestamps, and log lines (no raw dumps; the 3-10 most relevant findings).
4969
+ `;
4970
+ }
4971
+
4972
+ // src/bundle/bundle-holder.ts
4973
+ function createBundleHolder(initial) {
4974
+ let current = initial;
4975
+ return {
4976
+ get: () => current,
4977
+ set: (source) => {
4978
+ current = source;
4979
+ },
4980
+ isLoaded: () => current !== void 0
4981
+ };
4982
+ }
4983
+
4984
+ // src/cli/pick-bundle-path.ts
4985
+ var import_prompts3 = require("@inquirer/prompts");
4986
+ var import_promises7 = require("fs/promises");
4987
+ var import_node_path7 = require("path");
4988
+ function isPermissionError(err) {
4989
+ if (typeof err !== "object" || err === null || !("code" in err)) return false;
4990
+ const code = err.code;
4991
+ return code === "EACCES" || code === "EPERM";
4992
+ }
4993
+ async function listDirectoryCandidates(term) {
4994
+ const input5 = term.trim() === "" ? "." : term;
4995
+ const endsWithSep = input5.endsWith("/");
4996
+ const baseDir = endsWithSep ? input5 : (0, import_node_path7.dirname)(input5) || ".";
4997
+ const prefix = endsWithSep ? "" : (0, import_node_path7.basename)(input5);
4998
+ const resolved = (0, import_node_path7.resolve)(baseDir);
4999
+ let entries;
5000
+ try {
5001
+ entries = await (0, import_promises7.readdir)(resolved, { withFileTypes: true });
5002
+ } catch (err) {
5003
+ if (isPermissionError(err)) return { kind: "denied", dir: resolved };
5004
+ return { kind: "ok", candidates: [] };
5005
+ }
5006
+ const candidates = entries.filter((e) => e.isDirectory() && e.name.startsWith(prefix)).map((e) => {
5007
+ const value = (0, import_node_path7.join)(baseDir, e.name);
5008
+ return { name: `${value}/`, value };
5009
+ }).sort((a, b) => a.value.localeCompare(b.value));
5010
+ return { kind: "ok", candidates };
5011
+ }
5012
+ function listingToChoices(listing) {
5013
+ if (listing.kind === "denied") {
5014
+ return [
5015
+ {
5016
+ name: `Permission denied reading ${listing.dir} \u2014 grant your terminal access in System Settings \u203A Privacy & Security \u203A Files & Folders (or Full Disk Access), then retry`,
5017
+ value: "",
5018
+ disabled: true
5019
+ }
5020
+ ];
5021
+ }
5022
+ return listing.candidates.map((c) => ({ name: c.name, value: c.value }));
5023
+ }
5024
+ async function promptBundleDirectory() {
5025
+ if (!process.stdin.isTTY) return void 0;
5026
+ try {
5027
+ return await (0, import_prompts3.search)({
5028
+ message: "Select the support bundle directory (type to filter):",
5029
+ source: async (term) => listingToChoices(await listDirectoryCandidates(term ?? ""))
5030
+ });
5031
+ } catch {
5032
+ return void 0;
5033
+ }
5034
+ }
5035
+
5036
+ // src/approval/gate.ts
5037
+ var import_prompts4 = require("@inquirer/prompts");
5038
+
5039
+ // src/approval/display.ts
5040
+ var import_picocolors5 = __toESM(require("picocolors"));
5041
+ var IMPACT_FALLBACK = "Impact unknown \u2014 review parameters carefully";
5042
+ var DIVIDER2 = import_picocolors5.default.dim("\u2500".repeat(50));
5043
+ var LABEL_WIDTH = 8;
5044
+ function formatLabel(label) {
5045
+ return ` ${label.padEnd(LABEL_WIDTH)}: `;
5046
+ }
5047
+ function renderApprovalPanel(toolName, toolInput, impact, beforeAfter, reasoningSummary) {
5048
+ const header = import_picocolors5.default.bold(import_picocolors5.default.yellow(" Mutation Approval Required"));
5049
+ const action = `${formatLabel("Action")}${import_picocolors5.default.bold(formatToolName(toolName))}`;
5050
+ const paramEntries = Object.entries(toolInput);
5051
+ const paramSection = paramEntries.length === 0 ? " (no parameters)" : paramEntries.map(([key, value]) => {
5052
+ const formatted = typeof value === "string" ? value : JSON.stringify(value, null, 2);
5053
+ return ` ${import_picocolors5.default.dim(key)}: ${formatted}`;
5054
+ }).join("\n");
5055
+ const impactLine = `${formatLabel("Impact")}${impact ?? IMPACT_FALLBACK}`;
5056
+ const prompt = import_picocolors5.default.dim(
5057
+ `${formatLabel("Respond")}y (proceed) | n (abort) | explain (show reasoning)`
5058
+ );
5059
+ const hasBeforeAfter = beforeAfter !== void 0 && beforeAfter.length > 0;
5060
+ const beforeAfterSection = hasBeforeAfter ? beforeAfter.map(
5061
+ (entry) => ` ${import_picocolors5.default.dim(entry.key)}: ${entry.current} ${import_picocolors5.default.yellow("->")} ${entry.proposed}`
5062
+ ).join("\n") : null;
5063
+ const hasReasoning = reasoningSummary !== void 0 && reasoningSummary.length > 0;
5064
+ const reasoningSection = hasReasoning ? `${formatLabel("Reason")}${reasoningSummary}` : null;
5065
+ const sections = ["", DIVIDER2, header, "", action, paramSection, ""];
5066
+ if (beforeAfterSection !== null) {
5067
+ sections.push(beforeAfterSection, "");
5068
+ }
5069
+ if (reasoningSection !== null) {
5070
+ sections.push(reasoningSection, "");
5071
+ }
5072
+ sections.push(impactLine, "", prompt, DIVIDER2, "");
5073
+ return sections.join("\n");
5074
+ }
5075
+
5076
+ // src/approval/gate.ts
5077
+ var DENY_MESSAGE = "User denied this mutation. Skip and continue with the investigation.";
5078
+ var REASONING_FALLBACK = "Reasoning not available. Review the action details above before proceeding.";
5079
+ function createApprovalGate(isReadOnly) {
5080
+ return async (toolName, toolInput, options) => {
5081
+ if (isReadOnly(toolName)) {
5082
+ return {
5083
+ behavior: "allow",
5084
+ updatedInput: toolInput,
5085
+ toolUseID: options.toolUseID
5086
+ };
5087
+ }
5088
+ const impact = options.decisionReason;
5089
+ const panel = renderApprovalPanel(toolName, toolInput, impact);
5090
+ console.error(panel);
5091
+ while (true) {
5092
+ try {
5093
+ const raw = await (0, import_prompts4.input)({ message: "Proceed? (y/n/explain):" }, { signal: options.signal });
5094
+ const normalized = raw.trim().toLowerCase();
5095
+ if (normalized === "y") {
5096
+ process.stderr.write("\n");
5097
+ return {
5098
+ behavior: "allow",
5099
+ updatedInput: toolInput,
5100
+ toolUseID: options.toolUseID
5101
+ };
5102
+ }
5103
+ if (normalized === "n") {
5104
+ process.stderr.write("\n");
5105
+ return {
5106
+ behavior: "deny",
5107
+ message: DENY_MESSAGE,
5108
+ toolUseID: options.toolUseID
5109
+ };
5110
+ }
5111
+ if (normalized === "explain") {
5112
+ const reasoning = options.decisionReason;
5113
+ if (reasoning) {
5114
+ console.error(`
5115
+ Agent reasoning: ${reasoning}
5116
+ `);
5117
+ } else {
5118
+ console.error(`
5119
+ ${REASONING_FALLBACK}
5120
+ `);
5121
+ }
5122
+ }
5123
+ } catch {
5124
+ return {
5125
+ behavior: "deny",
5126
+ message: DENY_MESSAGE,
5127
+ toolUseID: options.toolUseID
5128
+ };
5129
+ }
5130
+ }
5131
+ };
5132
+ }
5133
+
5134
+ // src/agent/turn-gate.ts
5135
+ function createTurnGate() {
5136
+ let resolve3 = () => {
5137
+ };
5138
+ let promise = new Promise((r) => {
5139
+ resolve3 = r;
5140
+ });
5141
+ return Object.freeze({
5142
+ wait: () => promise,
5143
+ open: () => {
5144
+ resolve3();
5145
+ },
5146
+ close: () => {
5147
+ promise = new Promise((r) => {
5148
+ resolve3 = r;
5149
+ });
5150
+ }
5151
+ });
5152
+ }
5153
+
5154
+ // src/output/render-markdown.ts
5155
+ var import_picocolors6 = __toESM(require("picocolors"));
5156
+ var BOLD_RE = /\*\*(.+?)\*\*/g;
5157
+ var HEADING_RE = /^(#{1,6})\s+(.+)$/;
5158
+ function renderMarkdownLine(line) {
5159
+ const headingMatch = HEADING_RE.exec(line);
5160
+ if (headingMatch) {
5161
+ return import_picocolors6.default.bold(headingMatch[2]);
5162
+ }
5163
+ if (line.includes("**")) {
5164
+ return line.replace(BOLD_RE, (_, text2) => import_picocolors6.default.bold(text2));
5165
+ }
5166
+ return line;
5167
+ }
5168
+
5169
+ // src/output/reformat-tables.ts
5170
+ var SEPARATOR_CELL_RE = /^:?-+:?$/;
5171
+ var BOLD_MARKERS_RE = /\*\*(.+?)\*\*/g;
5172
+ function visualWidth(text2) {
5173
+ return text2.replace(BOLD_MARKERS_RE, "$1").length;
5174
+ }
5175
+ function isSeparatorCell(cell) {
5176
+ return SEPARATOR_CELL_RE.test(cell);
5177
+ }
5178
+ function isSeparatorRow(cells) {
5179
+ return cells.length > 0 && cells.every(isSeparatorCell);
5180
+ }
5181
+ function parseCells(line) {
5182
+ return line.split("|").slice(1, -1).map((c) => c.trim());
5183
+ }
5184
+ function reformatTableBlock(lines) {
5185
+ const parsed = lines.map(parseCells);
5186
+ const colCount = Math.max(...parsed.map((row) => row.length));
5187
+ const normalised = parsed.map((row) => {
5188
+ const padded = [...row];
5189
+ while (padded.length < colCount) {
5190
+ padded.push("");
5191
+ }
5192
+ return padded;
5193
+ });
5194
+ const colWidths = Array.from(
5195
+ { length: colCount },
5196
+ (_, col) => Math.max(
5197
+ 3,
5198
+ ...normalised.filter((row) => !isSeparatorRow(row)).map((row) => visualWidth(row[col]))
5199
+ )
3887
5200
  );
3888
5201
  const borderRow = `+${colWidths.map((w) => "-".repeat(w + 2)).join("+")}+`;
3889
5202
  const bodyRows = normalised.map((row) => {
@@ -3911,9 +5224,9 @@ function createStreamingTableAligner() {
3911
5224
  tableLines = [];
3912
5225
  return aligned.join("\n") + "\n";
3913
5226
  }
3914
- function push(text) {
3915
- if (!text) return "";
3916
- const combined = lineBuffer + text;
5227
+ function push(text2) {
5228
+ if (!text2) return "";
5229
+ const combined = lineBuffer + text2;
3917
5230
  const segments = combined.split("\n");
3918
5231
  lineBuffer = segments[segments.length - 1];
3919
5232
  const completeLines = segments.slice(0, -1);
@@ -3970,15 +5283,37 @@ function createSpinner() {
3970
5283
 
3971
5284
  // src/agent/run-agent.ts
3972
5285
  var MCP_SERVER_NAME = "kinetica-diagnostics";
5286
+ var SAVE_REPORT_TOOL_NAME = `mcp__${MCP_SERVER_NAME}__save_report`;
5287
+ function contentCallsSaveReport(content) {
5288
+ if (!Array.isArray(content)) return false;
5289
+ return content.some((block) => {
5290
+ if (typeof block !== "object" || block === null) return false;
5291
+ const { type, name } = block;
5292
+ return type === "tool_use" && name === SAVE_REPORT_TOOL_NAME;
5293
+ });
5294
+ }
5295
+ function formatCostSuffix(costUsd) {
5296
+ return costUsd !== void 0 && costUsd > 0 ? ` Cost: $${costUsd.toFixed(4)}.` : "";
5297
+ }
5298
+ function formatMetricsLine(turns, durationMs, durationApiMs, costUsd) {
5299
+ const durationSec = Math.round(durationMs / 1e3);
5300
+ const apiPct = durationMs > 0 ? Math.round(durationApiMs / durationMs * 100) : 0;
5301
+ return `Turns: ${turns}. Duration: ${durationSec}s (${apiPct}% API).${formatCostSuffix(costUsd)}`;
5302
+ }
3973
5303
  var EXIT_COMMANDS = /* @__PURE__ */ new Set(["exit", "quit", "end", "q"]);
3974
5304
  var SUPPORTED_MODELS = ["sonnet", "haiku", "opus"];
3975
5305
  var DEFAULT_AGENT_MODEL = "sonnet";
3976
- var DEFAULT_MAX_BUDGET_USD = 5;
5306
+ var LIVE_MAX_TURNS = 100;
5307
+ var BUNDLE_MAX_TURNS = 40;
3977
5308
  var ALLOWED_TOOL_NAMES = [
3978
5309
  ...DIAGNOSTIC_TOOL_NAMES.map((name) => `mcp__${MCP_SERVER_NAME}__${name}`),
3979
- `mcp__${MCP_SERVER_NAME}__save_report`,
5310
+ SAVE_REPORT_TOOL_NAME,
3980
5311
  `mcp__${MCP_SERVER_NAME}__${ALTER_TABLE_COLUMNS_TOOL_NAME}`
3981
5312
  ];
5313
+ var BUNDLE_ALLOWED_TOOL_NAMES = [
5314
+ ...BUNDLE_TOOL_NAMES.map((name) => `mcp__${MCP_SERVER_NAME}__${name}`),
5315
+ SAVE_REPORT_TOOL_NAME
5316
+ ];
3982
5317
  var DISALLOWED_TOOLS = ["Bash", "Edit", "Write", "MultiEdit"];
3983
5318
  var ERROR_LABELS = {
3984
5319
  authentication_failed: "Authentication failed \u2014 check your API key or re-run with --login",
@@ -3989,8 +5324,8 @@ var ERROR_LABELS = {
3989
5324
  max_output_tokens: "Response exceeded maximum output length",
3990
5325
  unknown: "Unknown API error"
3991
5326
  };
3992
- function isExitCommand(text) {
3993
- return EXIT_COMMANDS.has(text.trim().toLowerCase());
5327
+ function isExitCommand(text2) {
5328
+ return EXIT_COMMANDS.has(text2.trim().toLowerCase());
3994
5329
  }
3995
5330
  function makeUserMessage(content) {
3996
5331
  return {
@@ -4004,7 +5339,7 @@ async function* makeInteractivePrompt(abortController, turnGate, spinner) {
4004
5339
  while (!abortController.signal.aborted) {
4005
5340
  try {
4006
5341
  process.stderr.write("\n");
4007
- const issue = await (0, import_prompts4.input)({ message: "Describe the issue to investigate:" });
5342
+ const issue = await (0, import_prompts5.input)({ message: "Describe the issue to investigate:" });
4008
5343
  process.stderr.write("\n");
4009
5344
  const trimmed = issue.trim();
4010
5345
  if (!trimmed) continue;
@@ -4021,7 +5356,7 @@ async function* makeInteractivePrompt(abortController, turnGate, spinner) {
4021
5356
  await turnGate.wait();
4022
5357
  if (abortController.signal.aborted) break;
4023
5358
  process.stderr.write("\n");
4024
- const response = await (0, import_prompts4.input)({ message: "You:" });
5359
+ const response = await (0, import_prompts5.input)({ message: "You:" });
4025
5360
  process.stderr.write("\n");
4026
5361
  const trimmed = response.trim();
4027
5362
  if (!trimmed) continue;
@@ -4070,23 +5405,31 @@ async function displayDegradedStatus(session2) {
4070
5405
  }
4071
5406
  process.stderr.write("\n");
4072
5407
  }
4073
- async function runAgent(session2, kineticaVersion, degraded, model) {
4074
- const [catalogSchemas, playbooks, references] = await Promise.all([
4075
- degraded ? Promise.resolve(void 0) : discoverCatalogSchemas(session2),
5408
+ async function runAgent(session2, kineticaVersion, degraded, model, runOptions) {
5409
+ const bundleSource = runOptions?.bundleSource;
5410
+ const authMethod = runOptions?.authMethod ?? "api_key";
5411
+ const dollarCapped = authMethod === "api_key";
5412
+ const resolvedBudgetUsd = runOptions?.maxBudgetUsd ?? DEFAULT_MAX_BUDGET_USD;
5413
+ const [catalogSchemas, playbooks, references, bundleReferences] = await Promise.all([
5414
+ degraded || !session2 ? Promise.resolve(void 0) : discoverCatalogSchemas(session2),
4076
5415
  loadPlaybooks(),
4077
- loadReferences()
5416
+ loadReferences(),
5417
+ loadBundleReferences()
4078
5418
  ]);
4079
- const systemPrompt = buildSystemPrompt(
5419
+ const bundleHolder = createBundleHolder(bundleSource);
5420
+ const systemPrompt = session2 ? buildSystemPrompt(
4080
5421
  kineticaVersion,
4081
5422
  catalogSchemas,
4082
5423
  playbooks,
4083
5424
  references,
4084
- degraded
4085
- );
5425
+ degraded,
5426
+ bundleHolder.isLoaded() ? "attached" : "available",
5427
+ bundleReferences
5428
+ ) : buildBundleSystemPrompt(kineticaVersion, playbooks, references, bundleReferences);
4086
5429
  const budget = checkPromptBudget(systemPrompt);
4087
5430
  if (process.env.DEBUG) {
4088
5431
  process.stderr.write(
4089
- import_picocolors8.default.dim(`system prompt: ~${budget.tokens} tokens (${budget.chars} chars)
5432
+ import_picocolors8.default.dim(`System prompt: ~${budget.tokens} tokens (${budget.chars} chars)
4090
5433
  `)
4091
5434
  );
4092
5435
  }
@@ -4098,17 +5441,49 @@ async function runAgent(session2, kineticaVersion, degraded, model) {
4098
5441
  )
4099
5442
  );
4100
5443
  }
4101
- const diagnosticTools = makeDiagnosticTools(session2, catalogSchemas);
4102
- const mutationTools = makeMutationTools(session2);
4103
5444
  const saveReportTool = makeSaveReportTool();
4104
- const alterTableColumnsTool = makeAlterTableColumnsToolWithDeps(session2);
4105
- const server = (0, import_claude_agent_sdk4.createSdkMcpServer)({
5445
+ if (!session2 && !bundleSource) {
5446
+ throw new Error("runAgent requires a Kinetica session, a bundleSource, or both.");
5447
+ }
5448
+ const spinner = createSpinner();
5449
+ const promptForPath = async () => {
5450
+ spinner.stop();
5451
+ return promptBundleDirectory();
5452
+ };
5453
+ const confirmPath = async (path2) => {
5454
+ spinner.stop();
5455
+ try {
5456
+ const answer = await (0, import_prompts5.input)({
5457
+ message: `Load support bundle from "${path2}"? The agent will be able to read files under that directory. (y/n):`
5458
+ });
5459
+ return answer.trim().toLowerCase() === "y";
5460
+ } catch {
5461
+ return false;
5462
+ }
5463
+ };
5464
+ const bundleTools = makeBundleTools(bundleHolder, { promptForPath, confirmPath });
5465
+ const liveTools = session2 ? [
5466
+ ...makeDiagnosticTools(session2, catalogSchemas),
5467
+ ...makeMutationTools(session2),
5468
+ makeAlterTableColumnsToolWithDeps(session2)
5469
+ ] : [];
5470
+ const serverTools = [...liveTools, ...bundleTools, saveReportTool];
5471
+ const allowedTools = [
5472
+ .../* @__PURE__ */ new Set([...session2 ? ALLOWED_TOOL_NAMES : [], ...BUNDLE_ALLOWED_TOOL_NAMES])
5473
+ ];
5474
+ let registry = createBundleRegistry();
5475
+ if (session2) {
5476
+ registry = DIAGNOSTIC_TOOL_NAMES.reduce(
5477
+ (reg, name) => reg.registerReadOnlyTool(name),
5478
+ registry
5479
+ );
5480
+ }
5481
+ const maxTurns = session2 ? LIVE_MAX_TURNS : BUNDLE_MAX_TURNS;
5482
+ const server = (0, import_claude_agent_sdk5.createSdkMcpServer)({
4106
5483
  name: MCP_SERVER_NAME,
4107
5484
  version: "1.0.0",
4108
- tools: [...diagnosticTools, ...mutationTools, saveReportTool, alterTableColumnsTool]
5485
+ tools: serverTools
4109
5486
  });
4110
- const spinner = createSpinner();
4111
- const registry = createDiagnosticRegistry();
4112
5487
  const approvalGate = createApprovalGate(registry.isReadOnlyTool);
4113
5488
  const canUseTool = async (toolName, toolInput, options2) => {
4114
5489
  spinner.stop();
@@ -4118,33 +5493,64 @@ async function runAgent(session2, kineticaVersion, degraded, model) {
4118
5493
  const effectiveModel = model ?? DEFAULT_AGENT_MODEL;
4119
5494
  const options = {
4120
5495
  mcpServers: { [MCP_SERVER_NAME]: server },
4121
- allowedTools: ALLOWED_TOOL_NAMES,
5496
+ allowedTools,
4122
5497
  disallowedTools: [...DISALLOWED_TOOLS],
4123
5498
  canUseTool,
4124
5499
  systemPrompt,
4125
5500
  model: effectiveModel,
4126
5501
  fallbackModel: "haiku",
4127
5502
  thinking: { type: "adaptive" },
4128
- maxTurns: 100,
4129
- maxBudgetUsd: DEFAULT_MAX_BUDGET_USD,
5503
+ maxTurns,
5504
+ // Only impose a dollar cap for per-token billing. For OAuth subscription users
5505
+ // the SDK would otherwise cut them off at a notional dollar figure they never pay;
5506
+ // omitting it leaves the turn limit (maxTurns) as their guard.
5507
+ ...dollarCapped ? { maxBudgetUsd: resolvedBudgetUsd } : {},
4130
5508
  persistSession: false,
4131
5509
  includePartialMessages: true,
4132
5510
  abortController,
4133
5511
  env: { ...process.env, CLAUDE_AGENT_SDK_CLIENT_APP: "admin-agent" }
4134
5512
  };
4135
- if (degraded) {
4136
- process.stderr.write("\nKinetica Diagnostic Session Ready (DEGRADED MODE)\n");
5513
+ const guardLine = dollarCapped ? import_picocolors8.default.dim(`Budget guard: $${resolvedBudgetUsd.toFixed(2)} (raise with --max-budget)
5514
+ `) : import_picocolors8.default.dim("Budget guard: subscription (Pro/Max) \u2014 turn-limited\n");
5515
+ const bundleSummary = () => {
5516
+ const src = bundleHolder.get();
5517
+ if (!src) return "";
5518
+ const { totalFiles, ranks } = src.inventory();
5519
+ const versionStr = kineticaVersion ? ` (version ${kineticaVersion})` : "";
5520
+ return `${totalFiles} files, ranks: ${ranks.join(", ") || "none"}${versionStr}`;
5521
+ };
5522
+ if (session2) {
5523
+ if (degraded) {
5524
+ process.stderr.write("\nKinetica Diagnostic Session Ready (DEGRADED MODE)\n");
5525
+ process.stderr.write(
5526
+ "DB engine (port 9191) is unreachable. Only host manager tools are available.\n\n"
5527
+ );
5528
+ await displayDegradedStatus(session2);
5529
+ } else {
5530
+ process.stderr.write("\nKinetica Diagnostic Session Ready\n");
5531
+ }
5532
+ if (bundleHolder.isLoaded()) {
5533
+ process.stderr.write(
5534
+ `Support bundle attached: ${bundleSummary()}. Live + bundle tools available.
5535
+ `
5536
+ );
5537
+ } else {
5538
+ process.stderr.write(
5539
+ import_picocolors8.default.dim("Tip: ask me to analyze a support bundle to add offline log/config analysis.\n")
5540
+ );
5541
+ }
5542
+ } else {
5543
+ process.stderr.write("\nKinetica Diagnostic Session Ready (OFFLINE BUNDLE MODE)\n");
5544
+ process.stderr.write(`Analyzing extracted support bundle: ${bundleSummary()}.
5545
+ `);
4137
5546
  process.stderr.write(
4138
- "DB engine (port 9191) is unreachable. Only host manager tools are available.\n\n"
5547
+ "Read-only \u2014 diagnoses from captured logs/config; live verification unavailable (no DB connection).\n"
4139
5548
  );
4140
- await displayDegradedStatus(session2);
4141
- process.stderr.write("Type 'exit' to end the session.\n\n");
4142
- } else {
4143
- process.stderr.write("\nKinetica Diagnostic Session Ready\n");
4144
- process.stderr.write("Type 'exit' to end the session.\n\n");
4145
5549
  }
5550
+ process.stderr.write(guardLine);
5551
+ process.stderr.write("Type 'exit' to end the session.\n\n");
4146
5552
  const turnGate = createTurnGate();
4147
- const agentQuery = (0, import_claude_agent_sdk4.query)({
5553
+ const agentQuery = (0, import_claude_agent_sdk5.query)({
4148
5554
  prompt: makeInteractivePrompt(abortController, turnGate, spinner),
4149
5555
  options
4150
5556
  });
@@ -4163,15 +5569,18 @@ async function runAgent(session2, kineticaVersion, degraded, model) {
4163
5569
  let lastStreamCharWasNewline = true;
4164
5570
  const tableAligner = createStreamingTableAligner();
4165
5571
  let hadNonAbortError = false;
5572
+ let reportSavedThisRun = false;
5573
+ let invBase = { turns: 0, duration: 0, api: 0, cost: 0 };
5574
+ const budgetTracker = dollarCapped ? createBudgetTracker({ maxUsd: resolvedBudgetUsd }) : void 0;
4166
5575
  try {
4167
5576
  for await (const message of agentQuery) {
4168
5577
  if (message.type === "stream_event") {
4169
5578
  const { event: evt } = message;
4170
5579
  if (evt.type === "content_block_delta" && evt.delta.type === "text_delta") {
4171
- const text = evt.delta.text ?? "";
4172
- if (text) {
5580
+ const text2 = evt.delta.text ?? "";
5581
+ if (text2) {
4173
5582
  spinner.stop();
4174
- const output = tableAligner.push(text);
5583
+ const output = tableAligner.push(text2);
4175
5584
  if (output) {
4176
5585
  process.stderr.write(output);
4177
5586
  lastStreamCharWasNewline = output.endsWith("\n");
@@ -4189,6 +5598,23 @@ async function runAgent(session2, kineticaVersion, degraded, model) {
4189
5598
  process.stderr.write("\n");
4190
5599
  lastStreamCharWasNewline = true;
4191
5600
  }
5601
+ if (budgetTracker) {
5602
+ budgetTracker.add(fromSdkUsage(assistantMsg.message.usage), effectiveModel);
5603
+ if (budgetTracker.shouldWarn()) {
5604
+ spinner.stop();
5605
+ process.stderr.write(
5606
+ import_picocolors8.default.yellow(
5607
+ `
5608
+ \u26A0 Approaching budget guard (~$${budgetTracker.spentUsd().toFixed(2)} / $${resolvedBudgetUsd.toFixed(2)}) \u2014 wrapping up soon. Save a partial report now if you want to preserve findings.
5609
+ `
5610
+ )
5611
+ );
5612
+ budgetTracker.markWarned();
5613
+ }
5614
+ }
5615
+ if (contentCallsSaveReport(assistantMsg.message.content)) {
5616
+ reportSavedThisRun = true;
5617
+ }
4192
5618
  if (assistantMsg.message.stop_reason === "end_turn") {
4193
5619
  spinner.stop();
4194
5620
  turnGate.open();
@@ -4217,10 +5643,21 @@ API error: ${label}
4217
5643
  cacheCreationTokens = usages.reduce((sum, u) => sum + (u.cacheCreationInputTokens ?? 0), 0);
4218
5644
  if (resultMsg.subtype === "error_max_turns") {
4219
5645
  process.stderr.write(
4220
- "\nInvestigation hit turn limit. Partial report may be available.\n"
5646
+ import_picocolors8.default.yellow(
5647
+ `
5648
+ Reached the turn limit (${numTurns} turns) \u2014 a safety guard, not an error. Any report the agent saved is in reports/. Start a fresh session to continue.
5649
+ `
5650
+ )
4221
5651
  );
4222
5652
  } else if (resultMsg.subtype === "error_max_budget_usd") {
4223
- process.stderr.write("\nBudget limit reached.\n");
5653
+ const spentStr = totalCostUsd > 0 ? ` ($${totalCostUsd.toFixed(2)} spent)` : "";
5654
+ process.stderr.write(
5655
+ import_picocolors8.default.yellow(
5656
+ `
5657
+ Reached the $${resolvedBudgetUsd.toFixed(2)} budget guard${spentStr} \u2014 a safety limit, not an error. Re-run with --max-budget=<amount> (or set ADMIN_AGENT_MAX_BUDGET) for more headroom. Any report the agent saved is in reports/.
5658
+ `
5659
+ )
5660
+ );
4224
5661
  } else if (resultMsg.subtype === "error_during_execution") {
4225
5662
  process.stderr.write(
4226
5663
  "\nExecution error \u2014 the agent encountered an unrecoverable failure.\n"
@@ -4236,6 +5673,24 @@ Agent session ended with error: ${resultMsg.subtype}
4236
5673
  Permission denials: ${denied}
4237
5674
  `);
4238
5675
  }
5676
+ if (reportSavedThisRun) {
5677
+ const line = formatMetricsLine(
5678
+ numTurns - invBase.turns,
5679
+ durationMs - invBase.duration,
5680
+ durationApiMs - invBase.api,
5681
+ dollarCapped ? totalCostUsd - invBase.cost : void 0
5682
+ );
5683
+ process.stderr.write(`
5684
+ Investigation complete \u2014 ${line}
5685
+ `);
5686
+ invBase = {
5687
+ turns: numTurns,
5688
+ duration: durationMs,
5689
+ api: durationApiMs,
5690
+ cost: totalCostUsd
5691
+ };
5692
+ reportSavedThisRun = false;
5693
+ }
4239
5694
  turnGate.open();
4240
5695
  } else if (message.type === "system") {
4241
5696
  const sysMsg = message;
@@ -4291,7 +5746,7 @@ Rate limited \u2014 requests rejected.${resetStr}
4291
5746
  }
4292
5747
  } catch (error) {
4293
5748
  spinner.stop();
4294
- if (error instanceof import_claude_agent_sdk4.AbortError) {
5749
+ if (error instanceof import_claude_agent_sdk5.AbortError) {
4295
5750
  hadNonAbortError = false;
4296
5751
  } else {
4297
5752
  hadNonAbortError = true;
@@ -4307,27 +5762,26 @@ Agent error: ${message}
4307
5762
  process.stderr.write(remaining);
4308
5763
  }
4309
5764
  turnGate.open();
4310
- const durationSec = Math.round(durationMs / 1e3);
4311
- const apiPct = durationMs > 0 ? Math.round(durationApiMs / durationMs * 100) : 0;
4312
- const costStr = totalCostUsd > 0 ? ` Cost: $${totalCostUsd.toFixed(4)}.` : "";
5765
+ const sessionCost = dollarCapped ? totalCostUsd : void 0;
4313
5766
  if (process.env.DEBUG && (cacheReadTokens > 0 || cacheCreationTokens > 0)) {
4314
5767
  process.stderr.write(
4315
5768
  import_picocolors8.default.dim(
4316
- `cache: ${cacheReadTokens} read / ${cacheCreationTokens} created input tokens (read > 0 confirms the system prompt is served from cache)
5769
+ `Cache: ${cacheReadTokens} read / ${cacheCreationTokens} created input tokens (read > 0 confirms the system prompt is served from cache)
4317
5770
  `
4318
5771
  )
4319
5772
  );
4320
5773
  }
4321
5774
  if (hadNonAbortError) {
4322
- process.stderr.write(`
4323
- Session ended due to error. Turns: ${numTurns}.${costStr}
4324
- `);
4325
- } else {
4326
5775
  process.stderr.write(
4327
5776
  `
4328
- Session ended. Turns: ${numTurns}. Duration: ${durationSec}s (${apiPct}% API).${costStr}
5777
+ Session ended due to error. Turns: ${numTurns}.${formatCostSuffix(sessionCost)}
4329
5778
  `
4330
5779
  );
5780
+ } else {
5781
+ const line = formatMetricsLine(numTurns, durationMs, durationApiMs, sessionCost);
5782
+ process.stderr.write(`
5783
+ Session ended. ${line}
5784
+ `);
4331
5785
  }
4332
5786
  }
4333
5787
  }
@@ -4339,7 +5793,7 @@ var MODEL_LABELS = {
4339
5793
  opus: "Opus \u2014 deepest reasoning, slower & pricier"
4340
5794
  };
4341
5795
  async function selectModel() {
4342
- return (0, import_prompts5.select)({
5796
+ return (0, import_prompts6.select)({
4343
5797
  message: "Select model for this session:",
4344
5798
  default: DEFAULT_AGENT_MODEL,
4345
5799
  choices: SUPPORTED_MODELS.map((value) => ({ value, name: MODEL_LABELS[value] }))
@@ -4347,7 +5801,7 @@ async function selectModel() {
4347
5801
  }
4348
5802
 
4349
5803
  // src/auth/preflight.ts
4350
- var import_claude_agent_sdk5 = require("@anthropic-ai/claude-agent-sdk");
5804
+ var import_claude_agent_sdk6 = require("@anthropic-ai/claude-agent-sdk");
4351
5805
 
4352
5806
  // src/auth/oauth-flow.ts
4353
5807
  var import_picocolors9 = __toESM(require("picocolors"));
@@ -4435,7 +5889,7 @@ async function authenticateAnthropic(options) {
4435
5889
  await new Promise(() => {
4436
5890
  });
4437
5891
  }
4438
- const authQuery = (0, import_claude_agent_sdk5.query)({
5892
+ const authQuery = (0, import_claude_agent_sdk6.query)({
4439
5893
  prompt: hangingPrompt(),
4440
5894
  options: {
4441
5895
  persistSession: false,
@@ -4467,12 +5921,12 @@ async function authenticateAnthropic(options) {
4467
5921
  // src/auth/logout.ts
4468
5922
  var import_child_process2 = require("child_process");
4469
5923
  var import_node_module = require("module");
4470
- var import_node_path5 = __toESM(require("path"));
5924
+ var import_node_path8 = __toESM(require("path"));
4471
5925
  var import_util = require("util");
4472
5926
  var execFileAsync = (0, import_util.promisify)(import_child_process2.execFile);
4473
5927
  function resolveSdkCliPath() {
4474
5928
  const require_ = (0, import_node_module.createRequire)(__filename);
4475
- return import_node_path5.default.join(import_node_path5.default.dirname(require_.resolve("@anthropic-ai/claude-agent-sdk")), "cli.js");
5929
+ return import_node_path8.default.join(import_node_path8.default.dirname(require_.resolve("@anthropic-ai/claude-agent-sdk")), "cli.js");
4476
5930
  }
4477
5931
  async function logout() {
4478
5932
  try {
@@ -4492,9 +5946,9 @@ async function logout() {
4492
5946
 
4493
5947
  // src/session/env-file.ts
4494
5948
  var import_fs2 = require("fs");
4495
- var import_promises4 = require("fs/promises");
5949
+ var import_promises8 = require("fs/promises");
4496
5950
  var import_path2 = require("path");
4497
- var import_prompts6 = require("@inquirer/prompts");
5951
+ var import_prompts7 = require("@inquirer/prompts");
4498
5952
  var import_picocolors10 = __toESM(require("picocolors"));
4499
5953
  function parseEnvContent(content) {
4500
5954
  const entries = [];
@@ -4577,7 +6031,7 @@ function loadEnvFile(dir, env = process.env) {
4577
6031
  async function offerSaveCredentials(url, user, dir) {
4578
6032
  if (!process.stdin.isTTY) return;
4579
6033
  try {
4580
- const shouldSave = await (0, import_prompts6.confirm)({
6034
+ const shouldSave = await (0, import_prompts7.confirm)({
4581
6035
  message: "Save KINETICA_URL and KINETICA_USER to .env? (password is never saved)",
4582
6036
  default: true
4583
6037
  });
@@ -4585,11 +6039,11 @@ async function offerSaveCredentials(url, user, dir) {
4585
6039
  const filePath = (0, import_path2.join)(dir ?? process.cwd(), ".env");
4586
6040
  let existing;
4587
6041
  try {
4588
- existing = await (0, import_promises4.readFile)(filePath, "utf8");
6042
+ existing = await (0, import_promises8.readFile)(filePath, "utf8");
4589
6043
  } catch {
4590
6044
  }
4591
6045
  const content = buildEnvContent(url, user, existing);
4592
- await (0, import_promises4.writeFile)(filePath, content, "utf8");
6046
+ await (0, import_promises8.writeFile)(filePath, content, "utf8");
4593
6047
  console.error(import_picocolors10.default.dim("Saved to .env"));
4594
6048
  } catch (err) {
4595
6049
  const message = err instanceof Error ? err.message : String(err);
@@ -4599,10 +6053,10 @@ async function offerSaveCredentials(url, user, dir) {
4599
6053
 
4600
6054
  // src/session/verify.ts
4601
6055
  var import_picocolors13 = __toESM(require("picocolors"));
4602
- var import_prompts9 = require("@inquirer/prompts");
6056
+ var import_prompts10 = require("@inquirer/prompts");
4603
6057
 
4604
6058
  // src/session/collect.ts
4605
- var import_prompts7 = require("@inquirer/prompts");
6059
+ var import_prompts8 = require("@inquirer/prompts");
4606
6060
  var import_picocolors11 = __toESM(require("picocolors"));
4607
6061
  async function collectCredentials() {
4608
6062
  const prompted = /* @__PURE__ */ new Set();
@@ -4610,27 +6064,27 @@ async function collectCredentials() {
4610
6064
  const envUser = process.env.KINETICA_USER;
4611
6065
  if (envUrl && envUser && process.stdin.isTTY) {
4612
6066
  console.error(import_picocolors11.default.dim(`Saved connection: ${envUrl} (${envUser})`));
4613
- const useSaved = await (0, import_prompts7.confirm)({
6067
+ const useSaved = await (0, import_prompts8.confirm)({
4614
6068
  message: "Use saved connection?",
4615
6069
  default: true
4616
6070
  });
4617
6071
  if (!useSaved) {
4618
6072
  prompted.add("url");
4619
6073
  prompted.add("user");
4620
- const url2 = await (0, import_prompts7.input)({ message: "Kinetica endpoint URL:" });
4621
- const user2 = await (0, import_prompts7.input)({ message: "Admin username:" });
4622
- const pass2 = await (0, import_prompts7.password)({ message: "Admin password:", mask: "*" });
6074
+ const url2 = await (0, import_prompts8.input)({ message: "Kinetica endpoint URL:" });
6075
+ const user2 = await (0, import_prompts8.input)({ message: "Admin username:" });
6076
+ const pass2 = await (0, import_prompts8.password)({ message: "Admin password:", mask: "*" });
4623
6077
  return { credentials: { url: url2, user: user2, pass: pass2 }, prompted };
4624
6078
  }
4625
6079
  }
4626
- const url = envUrl ?? (prompted.add("url"), await (0, import_prompts7.input)({ message: "Kinetica endpoint URL:" }));
4627
- const user = envUser ?? (prompted.add("user"), await (0, import_prompts7.input)({ message: "Admin username:" }));
4628
- const pass = process.env.KINETICA_PASS ?? await (0, import_prompts7.password)({ message: "Admin password:", mask: "*" });
6080
+ const url = envUrl ?? (prompted.add("url"), await (0, import_prompts8.input)({ message: "Kinetica endpoint URL:" }));
6081
+ const user = envUser ?? (prompted.add("user"), await (0, import_prompts8.input)({ message: "Admin username:" }));
6082
+ const pass = process.env.KINETICA_PASS ?? await (0, import_prompts8.password)({ message: "Admin password:", mask: "*" });
4629
6083
  return { credentials: { url, user, pass }, prompted };
4630
6084
  }
4631
6085
  async function repromptCredentials() {
4632
- const user = await (0, import_prompts7.input)({ message: "Admin username:" });
4633
- const pass = await (0, import_prompts7.password)({ message: "Admin password:", mask: "*" });
6086
+ const user = await (0, import_prompts8.input)({ message: "Admin username:" });
6087
+ const pass = await (0, import_prompts8.password)({ message: "Admin password:", mask: "*" });
4634
6088
  return { user, pass };
4635
6089
  }
4636
6090
 
@@ -4641,8 +6095,9 @@ function replacePort(baseUrl, port) {
4641
6095
  parsed.port = String(port);
4642
6096
  return parsed.origin;
4643
6097
  }
4644
- function createSession(url, user, pass) {
6098
+ function createSession(url, user, pass, options) {
4645
6099
  const authHeader = "Basic " + Buffer.from(`${user}:${pass}`).toString("base64");
6100
+ const timeoutMs = options?.timeoutMs ?? REQUEST_TIMEOUT_MS;
4646
6101
  const doFetch = async (fullUrl, body) => {
4647
6102
  if (process.env.DEBUG) {
4648
6103
  console.error(`[DEBUG] POST ${fullUrl}`);
@@ -4654,7 +6109,7 @@ function createSession(url, user, pass) {
4654
6109
  "Content-Type": "application/json"
4655
6110
  },
4656
6111
  body: body !== void 0 ? JSON.stringify(body) : void 0,
4657
- signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
6112
+ signal: AbortSignal.timeout(timeoutMs)
4658
6113
  });
4659
6114
  };
4660
6115
  return {
@@ -4666,7 +6121,7 @@ function createSession(url, user, pass) {
4666
6121
 
4667
6122
  // src/session/resolve-url.ts
4668
6123
  var import_picocolors12 = __toESM(require("picocolors"));
4669
- var import_prompts8 = require("@inquirer/prompts");
6124
+ var import_prompts9 = require("@inquirer/prompts");
4670
6125
  var PROBE_TIMEOUT_MS2 = 3e3;
4671
6126
  var HTTP_PROTOCOL_RE = /^https?:\/\//i;
4672
6127
  var ANY_PROTOCOL_RE = /^[a-z][a-z0-9+.-]*:\/\//i;
@@ -4708,7 +6163,7 @@ async function confirmHttpFallback(host) {
4708
6163
  )
4709
6164
  );
4710
6165
  try {
4711
- return await (0, import_prompts8.confirm)({
6166
+ return await (0, import_prompts9.confirm)({
4712
6167
  message: "Continue over plaintext HTTP?",
4713
6168
  default: false
4714
6169
  });
@@ -4716,7 +6171,7 @@ async function confirmHttpFallback(host) {
4716
6171
  return false;
4717
6172
  }
4718
6173
  }
4719
- async function resolveUrl(input5) {
6174
+ async function resolveUrl(input5, options = {}) {
4720
6175
  const trimmed = input5.trim();
4721
6176
  if (trimmed === "") {
4722
6177
  return { ok: false, error: "URL is empty" };
@@ -4750,7 +6205,7 @@ async function resolveUrl(input5) {
4750
6205
  error: `Could not connect to ${normalized} via https:// or http://`
4751
6206
  };
4752
6207
  }
4753
- if (!isInteractive()) {
6208
+ if (options.nonInteractive || !isInteractive()) {
4754
6209
  return {
4755
6210
  ok: false,
4756
6211
  error: `HTTPS unavailable at ${normalized} and terminal is non-interactive. Pass an explicit http:// prefix to allow plaintext HTTP, or point the URL at an HTTPS endpoint.`
@@ -4770,6 +6225,7 @@ async function resolveUrl(input5) {
4770
6225
  var MAX_RETRIES = 3;
4771
6226
  var MAX_REPROMPTS = 2;
4772
6227
  var DEFAULT_HM_PORT2 = 9300;
6228
+ var BEST_EFFORT_PROBE_TIMEOUT_MS = 5e3;
4773
6229
  function extractVersion(responseBody) {
4774
6230
  try {
4775
6231
  const outer = JSON.parse(responseBody);
@@ -4816,6 +6272,24 @@ async function verifyConnectivity(session2) {
4816
6272
  function isCredentialError(errorMessage) {
4817
6273
  return errorMessage.startsWith("HTTP 401") || errorMessage.startsWith("HTTP 403");
4818
6274
  }
6275
+ async function connectBestEffort() {
6276
+ const url = process.env.KINETICA_URL;
6277
+ const user = process.env.KINETICA_USER;
6278
+ const pass = process.env.KINETICA_PASS;
6279
+ if (!url || !user || !pass) return void 0;
6280
+ try {
6281
+ const resolved = await resolveUrl(url, { nonInteractive: true });
6282
+ if (!resolved.ok) return void 0;
6283
+ const probe = createSession(resolved.url, user, pass, {
6284
+ timeoutMs: BEST_EFFORT_PROBE_TIMEOUT_MS
6285
+ });
6286
+ const kineticaVersion = await verifyConnectivity(probe);
6287
+ const session2 = createSession(resolved.url, user, pass);
6288
+ return { session: session2, kineticaVersion, degraded: false };
6289
+ } catch {
6290
+ return void 0;
6291
+ }
6292
+ }
4819
6293
  async function connectWithRetry() {
4820
6294
  const { credentials, prompted } = await collectCredentials();
4821
6295
  const resolved = await resolveUrl(credentials.url);
@@ -4842,7 +6316,7 @@ async function connectWithRetry() {
4842
6316
  console.error(import_picocolors13.default.red(`Connection failed (attempt ${attempt}/${MAX_RETRIES}): ${msg}`));
4843
6317
  if (isCredentialError(msg)) {
4844
6318
  if (process.stdin.isTTY && repromptCount < MAX_REPROMPTS) {
4845
- const shouldRetry = await (0, import_prompts9.confirm)({
6319
+ const shouldRetry = await (0, import_prompts10.confirm)({
4846
6320
  message: "Credentials may be incorrect. Re-enter?",
4847
6321
  default: true
4848
6322
  });
@@ -4885,6 +6359,14 @@ async function connectWithRetry() {
4885
6359
  // src/cli/index.ts
4886
6360
  var verbose = false;
4887
6361
  var session;
6362
+ function flagValue(args, name) {
6363
+ const prefix = `${name}=`;
6364
+ const arg = args.find((a) => a.startsWith(prefix));
6365
+ return arg === void 0 ? void 0 : arg.slice(prefix.length);
6366
+ }
6367
+ function chooseBundleSessionVersion(bundleVersion, liveVersion) {
6368
+ return bundleVersion ?? liveVersion;
6369
+ }
4888
6370
  function printHelp() {
4889
6371
  const lines = [
4890
6372
  "",
@@ -4904,12 +6386,15 @@ function printHelp() {
4904
6386
  " --login-org=UUID Target organization UUID for OAuth",
4905
6387
  " --logout Log out from Anthropic account and exit",
4906
6388
  ` --model=NAME Override agent model (${SUPPORTED_MODELS.join(" | ")}); default: sonnet`,
6389
+ " --max-budget=USD Per-session budget cap in USD (API-key billing only); default: 5.00",
6390
+ " --bundle=PATH Offline mode: diagnose from an extracted support bundle directory",
4907
6391
  "",
4908
6392
  " Environment variables:",
4909
- " ANTHROPIC_API_KEY Anthropic API key (if not set, OAuth login via browser is used)",
4910
- " KINETICA_URL Kinetica endpoint URL",
4911
- " KINETICA_USER Admin username",
4912
- " KINETICA_PASS Admin password",
6393
+ " ANTHROPIC_API_KEY Anthropic API key (if not set, OAuth login via browser is used)",
6394
+ " ADMIN_AGENT_MAX_BUDGET Per-session budget cap in USD (overridden by --max-budget)",
6395
+ " KINETICA_URL Kinetica endpoint URL",
6396
+ " KINETICA_USER Admin username",
6397
+ " KINETICA_PASS Admin password",
4913
6398
  ""
4914
6399
  ];
4915
6400
  process.stdout.write(lines.join("\n") + "\n");
@@ -4934,12 +6419,9 @@ async function main() {
4934
6419
  return;
4935
6420
  }
4936
6421
  const forceLogin = args.includes("--login");
4937
- const loginMethodArg = args.find((a) => a.startsWith("--login-method="));
4938
- const loginMethod = loginMethodArg?.split("=")[1];
4939
- const loginOrgArg = args.find((a) => a.startsWith("--login-org="));
4940
- const loginOrgUUID = loginOrgArg?.split("=")[1];
4941
- const modelArg = args.find((a) => a.startsWith("--model="));
4942
- const modelValue = modelArg?.split("=")[1];
6422
+ const loginMethod = flagValue(args, "--login-method");
6423
+ const loginOrgUUID = flagValue(args, "--login-org");
6424
+ const modelValue = flagValue(args, "--model");
4943
6425
  let model;
4944
6426
  if (modelValue !== void 0) {
4945
6427
  if (SUPPORTED_MODELS.includes(modelValue)) {
@@ -4954,13 +6436,39 @@ async function main() {
4954
6436
  return;
4955
6437
  }
4956
6438
  }
6439
+ const budgetValue = flagValue(args, "--max-budget");
6440
+ let maxBudgetFlag;
6441
+ if (budgetValue !== void 0) {
6442
+ const parsed = Number(budgetValue);
6443
+ if (!isValidBudget(parsed)) {
6444
+ process.stderr.write(
6445
+ import_picocolors14.default.red(
6446
+ `Error: invalid --max-budget value "${budgetValue}". Use a positive number, e.g. --max-budget=10
6447
+ `
6448
+ )
6449
+ );
6450
+ process.exitCode = 1;
6451
+ return;
6452
+ }
6453
+ maxBudgetFlag = parsed;
6454
+ }
6455
+ const bundlePath = flagValue(args, "--bundle");
6456
+ if (bundlePath?.trim() === "") {
6457
+ process.stderr.write(
6458
+ import_picocolors14.default.red(
6459
+ "Error: --bundle requires a directory path, e.g. --bundle=/path/to/extracted-bundle\n"
6460
+ )
6461
+ );
6462
+ process.exitCode = 1;
6463
+ return;
6464
+ }
4957
6465
  loadEnvFile();
4958
6466
  printBanner();
4959
6467
  if (model === void 0 && process.stdin.isTTY) {
4960
6468
  model = await selectModel();
4961
6469
  }
4962
6470
  const effectiveModel = model ?? DEFAULT_AGENT_MODEL;
4963
- process.stderr.write(import_picocolors14.default.dim(`model: ${effectiveModel}
6471
+ process.stderr.write(import_picocolors14.default.dim(`Model: ${effectiveModel}
4964
6472
  `));
4965
6473
  const authResult = await authenticateAnthropic({ forceLogin, loginMethod, loginOrgUUID });
4966
6474
  if (authResult.method === "oauth") {
@@ -4970,9 +6478,54 @@ async function main() {
4970
6478
  } else {
4971
6479
  process.stderr.write(import_picocolors14.default.dim("Authenticated via API key\n"));
4972
6480
  }
6481
+ const maxBudgetUsd = resolveMaxBudgetUsd(maxBudgetFlag);
6482
+ if (bundlePath !== void 0) {
6483
+ const result = await verifyBundle(bundlePath);
6484
+ if (!result.ok) {
6485
+ process.stderr.write(import_picocolors14.default.red(`Error: ${result.error}
6486
+ `));
6487
+ process.exitCode = 1;
6488
+ return;
6489
+ }
6490
+ if (result.missingExpected.length > 0) {
6491
+ process.stderr.write(
6492
+ import_picocolors14.default.yellow(
6493
+ `Warning: bundle is missing expected artifact(s): ${result.missingExpected.join(", ")}. Diagnosing with what is present.
6494
+ `
6495
+ )
6496
+ );
6497
+ }
6498
+ const live = await connectBestEffort();
6499
+ process.stderr.write(
6500
+ live ? import_picocolors14.default.dim("Live connection available \u2014 bundle + live verification enabled.\n") : import_picocolors14.default.dim("No reachable live connection \u2014 offline bundle analysis only.\n")
6501
+ );
6502
+ if (live?.kineticaVersion && result.kineticaVersion && live.kineticaVersion !== result.kineticaVersion) {
6503
+ process.stderr.write(
6504
+ import_picocolors14.default.yellow(
6505
+ `Warning: live cluster version (${live.kineticaVersion}) differs from the bundle's captured version (${result.kineticaVersion}). Reasoning over the bundle uses the captured version; the live cluster may have been upgraded since capture.
6506
+ `
6507
+ )
6508
+ );
6509
+ }
6510
+ await runAgent(
6511
+ live?.session,
6512
+ chooseBundleSessionVersion(result.kineticaVersion, live?.kineticaVersion),
6513
+ false,
6514
+ model,
6515
+ {
6516
+ authMethod: authResult.method,
6517
+ maxBudgetUsd,
6518
+ bundleSource: result.bundleSource
6519
+ }
6520
+ );
6521
+ return;
6522
+ }
4973
6523
  const { session: connectedSession, kineticaVersion, degraded } = await connectWithRetry();
4974
6524
  session = connectedSession;
4975
- await runAgent(session, kineticaVersion, degraded, model);
6525
+ await runAgent(session, kineticaVersion, degraded, model, {
6526
+ authMethod: authResult.method,
6527
+ maxBudgetUsd
6528
+ });
4976
6529
  }
4977
6530
  function getSession() {
4978
6531
  return session;
@@ -4990,6 +6543,7 @@ if (process.env.NODE_ENV !== "test") {
4990
6543
  }
4991
6544
  // Annotate the CommonJS export names for ESM import in node:
4992
6545
  0 && (module.exports = {
6546
+ chooseBundleSessionVersion,
4993
6547
  getSession,
4994
6548
  main,
4995
6549
  verbose