@kinetica/admin-agent 0.1.3 → 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);
@@ -117,11 +118,11 @@ ${import_picocolors.default.dim(`Model: ${model}`)}` : 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
 
@@ -3642,9 +3662,8 @@ async function discoverCatalogSchemas(session2) {
3642
3662
  var import_promises2 = require("fs/promises");
3643
3663
  var import_node_path3 = require("path");
3644
3664
  var import_node_fs3 = require("fs");
3645
- async function loadReferences(refsDir) {
3665
+ async function loadReferencesFrom(dir) {
3646
3666
  try {
3647
- const dir = refsDir ?? (0, import_node_path3.join)(findPackageRoot(__dirname), "knowledge", "references");
3648
3667
  if (!(0, import_node_fs3.existsSync)(dir)) return [];
3649
3668
  const files = await (0, import_promises2.readdir)(dir);
3650
3669
  const mdFiles = files.filter((f) => f.endsWith(".md")).sort();
@@ -3666,13 +3685,21 @@ async function loadReferences(refsDir) {
3666
3685
  return [];
3667
3686
  }
3668
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
+ }
3669
3696
 
3670
3697
  // src/agent/prompt-budget.ts
3671
3698
  var CHARS_PER_TOKEN = 4;
3672
3699
  var DEFAULT_PROMPT_BUDGET_TOKENS = 2e4;
3673
- function estimateTokens(text) {
3674
- if (!text) return 0;
3675
- 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);
3676
3703
  }
3677
3704
  function checkPromptBudget(prompt, opts) {
3678
3705
  const threshold = opts?.warnAtTokens ?? DEFAULT_PROMPT_BUDGET_TOKENS;
@@ -3718,8 +3745,8 @@ function estimateTurnCostUsd(usage, model) {
3718
3745
  const cacheCreation = safeCount(usage.cacheCreationInputTokens);
3719
3746
  return (input5 * price.inputPerMTok + output * price.outputPerMTok + cacheRead * price.cacheReadPerMTok + cacheCreation * price.cacheCreationPerMTok) / 1e6;
3720
3747
  }
3721
- function resolveMaxBudgetUsd(flagValue, env = process.env) {
3722
- if (isValidBudget(flagValue)) return flagValue;
3748
+ function resolveMaxBudgetUsd(flagValue2, env = process.env) {
3749
+ if (isValidBudget(flagValue2)) return flagValue2;
3723
3750
  const raw = env[BUDGET_ENV_VAR];
3724
3751
  if (raw !== void 0 && raw !== "") {
3725
3752
  const parsed = Number(raw);
@@ -3766,7 +3793,7 @@ function formatTimestamp(date) {
3766
3793
  function makeSaveReportTool() {
3767
3794
  return (0, import_claude_agent_sdk3.tool)(
3768
3795
  "save_report",
3769
- "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.",
3770
3797
  {
3771
3798
  content: import_zod17.z.string().describe("The full markdown diagnostic report content"),
3772
3799
  partial: import_zod17.z.boolean().optional().describe(
@@ -3790,8 +3817,1224 @@ function makeSaveReportTool() {
3790
3817
  );
3791
3818
  }
3792
3819
 
3793
- // src/approval/gate.ts
3820
+ // src/tools/bundle/index.ts
3821
+ var import_claude_agent_sdk4 = require("@anthropic-ai/claude-agent-sdk");
3822
+
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;
3881
+ }
3882
+ function describeBundleFile(entry) {
3883
+ return KNOWN_BUNDLE_FILES[basename(entry.relPath)] ?? KIND_DESCRIPTIONS[entry.kind] ?? "";
3884
+ }
3885
+
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
3915
+ }
3916
+ };
3917
+ }
3918
+
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 } : {}
3937
+ };
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;
3947
+ });
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
3955
+ }
3956
+ };
3957
+ }
3958
+
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: "" };
4019
+ }
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
+ };
4029
+ }
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
+ };
4035
+ }
4036
+
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
+ };
4058
+ }
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 };
4123
+ }
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;
4137
+ }
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;
4153
+ });
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
3794
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");
3795
5038
 
3796
5039
  // src/approval/display.ts
3797
5040
  var import_picocolors5 = __toESM(require("picocolors"));
@@ -3847,7 +5090,7 @@ function createApprovalGate(isReadOnly) {
3847
5090
  console.error(panel);
3848
5091
  while (true) {
3849
5092
  try {
3850
- const raw = await (0, import_prompts3.input)({ message: "Proceed? (y/n/explain):" }, { signal: options.signal });
5093
+ const raw = await (0, import_prompts4.input)({ message: "Proceed? (y/n/explain):" }, { signal: options.signal });
3851
5094
  const normalized = raw.trim().toLowerCase();
3852
5095
  if (normalized === "y") {
3853
5096
  process.stderr.write("\n");
@@ -3890,19 +5133,19 @@ ${REASONING_FALLBACK}
3890
5133
 
3891
5134
  // src/agent/turn-gate.ts
3892
5135
  function createTurnGate() {
3893
- let resolve2 = () => {
5136
+ let resolve3 = () => {
3894
5137
  };
3895
5138
  let promise = new Promise((r) => {
3896
- resolve2 = r;
5139
+ resolve3 = r;
3897
5140
  });
3898
5141
  return Object.freeze({
3899
5142
  wait: () => promise,
3900
5143
  open: () => {
3901
- resolve2();
5144
+ resolve3();
3902
5145
  },
3903
5146
  close: () => {
3904
5147
  promise = new Promise((r) => {
3905
- resolve2 = r;
5148
+ resolve3 = r;
3906
5149
  });
3907
5150
  }
3908
5151
  });
@@ -3918,7 +5161,7 @@ function renderMarkdownLine(line) {
3918
5161
  return import_picocolors6.default.bold(headingMatch[2]);
3919
5162
  }
3920
5163
  if (line.includes("**")) {
3921
- return line.replace(BOLD_RE, (_, text) => import_picocolors6.default.bold(text));
5164
+ return line.replace(BOLD_RE, (_, text2) => import_picocolors6.default.bold(text2));
3922
5165
  }
3923
5166
  return line;
3924
5167
  }
@@ -3926,8 +5169,8 @@ function renderMarkdownLine(line) {
3926
5169
  // src/output/reformat-tables.ts
3927
5170
  var SEPARATOR_CELL_RE = /^:?-+:?$/;
3928
5171
  var BOLD_MARKERS_RE = /\*\*(.+?)\*\*/g;
3929
- function visualWidth(text) {
3930
- return text.replace(BOLD_MARKERS_RE, "$1").length;
5172
+ function visualWidth(text2) {
5173
+ return text2.replace(BOLD_MARKERS_RE, "$1").length;
3931
5174
  }
3932
5175
  function isSeparatorCell(cell) {
3933
5176
  return SEPARATOR_CELL_RE.test(cell);
@@ -3981,9 +5224,9 @@ function createStreamingTableAligner() {
3981
5224
  tableLines = [];
3982
5225
  return aligned.join("\n") + "\n";
3983
5226
  }
3984
- function push(text) {
3985
- if (!text) return "";
3986
- const combined = lineBuffer + text;
5227
+ function push(text2) {
5228
+ if (!text2) return "";
5229
+ const combined = lineBuffer + text2;
3987
5230
  const segments = combined.split("\n");
3988
5231
  lineBuffer = segments[segments.length - 1];
3989
5232
  const completeLines = segments.slice(0, -1);
@@ -4060,11 +5303,17 @@ function formatMetricsLine(turns, durationMs, durationApiMs, costUsd) {
4060
5303
  var EXIT_COMMANDS = /* @__PURE__ */ new Set(["exit", "quit", "end", "q"]);
4061
5304
  var SUPPORTED_MODELS = ["sonnet", "haiku", "opus"];
4062
5305
  var DEFAULT_AGENT_MODEL = "sonnet";
5306
+ var LIVE_MAX_TURNS = 100;
5307
+ var BUNDLE_MAX_TURNS = 40;
4063
5308
  var ALLOWED_TOOL_NAMES = [
4064
5309
  ...DIAGNOSTIC_TOOL_NAMES.map((name) => `mcp__${MCP_SERVER_NAME}__${name}`),
4065
5310
  SAVE_REPORT_TOOL_NAME,
4066
5311
  `mcp__${MCP_SERVER_NAME}__${ALTER_TABLE_COLUMNS_TOOL_NAME}`
4067
5312
  ];
5313
+ var BUNDLE_ALLOWED_TOOL_NAMES = [
5314
+ ...BUNDLE_TOOL_NAMES.map((name) => `mcp__${MCP_SERVER_NAME}__${name}`),
5315
+ SAVE_REPORT_TOOL_NAME
5316
+ ];
4068
5317
  var DISALLOWED_TOOLS = ["Bash", "Edit", "Write", "MultiEdit"];
4069
5318
  var ERROR_LABELS = {
4070
5319
  authentication_failed: "Authentication failed \u2014 check your API key or re-run with --login",
@@ -4075,8 +5324,8 @@ var ERROR_LABELS = {
4075
5324
  max_output_tokens: "Response exceeded maximum output length",
4076
5325
  unknown: "Unknown API error"
4077
5326
  };
4078
- function isExitCommand(text) {
4079
- return EXIT_COMMANDS.has(text.trim().toLowerCase());
5327
+ function isExitCommand(text2) {
5328
+ return EXIT_COMMANDS.has(text2.trim().toLowerCase());
4080
5329
  }
4081
5330
  function makeUserMessage(content) {
4082
5331
  return {
@@ -4090,7 +5339,7 @@ async function* makeInteractivePrompt(abortController, turnGate, spinner) {
4090
5339
  while (!abortController.signal.aborted) {
4091
5340
  try {
4092
5341
  process.stderr.write("\n");
4093
- 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:" });
4094
5343
  process.stderr.write("\n");
4095
5344
  const trimmed = issue.trim();
4096
5345
  if (!trimmed) continue;
@@ -4107,7 +5356,7 @@ async function* makeInteractivePrompt(abortController, turnGate, spinner) {
4107
5356
  await turnGate.wait();
4108
5357
  if (abortController.signal.aborted) break;
4109
5358
  process.stderr.write("\n");
4110
- const response = await (0, import_prompts4.input)({ message: "You:" });
5359
+ const response = await (0, import_prompts5.input)({ message: "You:" });
4111
5360
  process.stderr.write("\n");
4112
5361
  const trimmed = response.trim();
4113
5362
  if (!trimmed) continue;
@@ -4157,21 +5406,26 @@ async function displayDegradedStatus(session2) {
4157
5406
  process.stderr.write("\n");
4158
5407
  }
4159
5408
  async function runAgent(session2, kineticaVersion, degraded, model, runOptions) {
5409
+ const bundleSource = runOptions?.bundleSource;
4160
5410
  const authMethod = runOptions?.authMethod ?? "api_key";
4161
5411
  const dollarCapped = authMethod === "api_key";
4162
5412
  const resolvedBudgetUsd = runOptions?.maxBudgetUsd ?? DEFAULT_MAX_BUDGET_USD;
4163
- const [catalogSchemas, playbooks, references] = await Promise.all([
4164
- degraded ? Promise.resolve(void 0) : discoverCatalogSchemas(session2),
5413
+ const [catalogSchemas, playbooks, references, bundleReferences] = await Promise.all([
5414
+ degraded || !session2 ? Promise.resolve(void 0) : discoverCatalogSchemas(session2),
4165
5415
  loadPlaybooks(),
4166
- loadReferences()
5416
+ loadReferences(),
5417
+ loadBundleReferences()
4167
5418
  ]);
4168
- const systemPrompt = buildSystemPrompt(
5419
+ const bundleHolder = createBundleHolder(bundleSource);
5420
+ const systemPrompt = session2 ? buildSystemPrompt(
4169
5421
  kineticaVersion,
4170
5422
  catalogSchemas,
4171
5423
  playbooks,
4172
5424
  references,
4173
- degraded
4174
- );
5425
+ degraded,
5426
+ bundleHolder.isLoaded() ? "attached" : "available",
5427
+ bundleReferences
5428
+ ) : buildBundleSystemPrompt(kineticaVersion, playbooks, references, bundleReferences);
4175
5429
  const budget = checkPromptBudget(systemPrompt);
4176
5430
  if (process.env.DEBUG) {
4177
5431
  process.stderr.write(
@@ -4187,17 +5441,49 @@ async function runAgent(session2, kineticaVersion, degraded, model, runOptions)
4187
5441
  )
4188
5442
  );
4189
5443
  }
4190
- const diagnosticTools = makeDiagnosticTools(session2, catalogSchemas);
4191
- const mutationTools = makeMutationTools(session2);
4192
5444
  const saveReportTool = makeSaveReportTool();
4193
- const alterTableColumnsTool = makeAlterTableColumnsToolWithDeps(session2);
4194
- 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)({
4195
5483
  name: MCP_SERVER_NAME,
4196
5484
  version: "1.0.0",
4197
- tools: [...diagnosticTools, ...mutationTools, saveReportTool, alterTableColumnsTool]
5485
+ tools: serverTools
4198
5486
  });
4199
- const spinner = createSpinner();
4200
- const registry = createDiagnosticRegistry();
4201
5487
  const approvalGate = createApprovalGate(registry.isReadOnlyTool);
4202
5488
  const canUseTool = async (toolName, toolInput, options2) => {
4203
5489
  spinner.stop();
@@ -4207,14 +5493,14 @@ async function runAgent(session2, kineticaVersion, degraded, model, runOptions)
4207
5493
  const effectiveModel = model ?? DEFAULT_AGENT_MODEL;
4208
5494
  const options = {
4209
5495
  mcpServers: { [MCP_SERVER_NAME]: server },
4210
- allowedTools: ALLOWED_TOOL_NAMES,
5496
+ allowedTools,
4211
5497
  disallowedTools: [...DISALLOWED_TOOLS],
4212
5498
  canUseTool,
4213
5499
  systemPrompt,
4214
5500
  model: effectiveModel,
4215
5501
  fallbackModel: "haiku",
4216
5502
  thinking: { type: "adaptive" },
4217
- maxTurns: 100,
5503
+ maxTurns,
4218
5504
  // Only impose a dollar cap for per-token billing. For OAuth subscription users
4219
5505
  // the SDK would otherwise cut them off at a notional dollar figure they never pay;
4220
5506
  // omitting it leaves the turn limit (maxTurns) as their guard.
@@ -4226,19 +5512,45 @@ async function runAgent(session2, kineticaVersion, degraded, model, runOptions)
4226
5512
  };
4227
5513
  const guardLine = dollarCapped ? import_picocolors8.default.dim(`Budget guard: $${resolvedBudgetUsd.toFixed(2)} (raise with --max-budget)
4228
5514
  `) : import_picocolors8.default.dim("Budget guard: subscription (Pro/Max) \u2014 turn-limited\n");
4229
- if (degraded) {
4230
- process.stderr.write("\nKinetica Diagnostic Session Ready (DEGRADED MODE)\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
+ `);
4231
5546
  process.stderr.write(
4232
- "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"
4233
5548
  );
4234
- await displayDegradedStatus(session2);
4235
- } else {
4236
- process.stderr.write("\nKinetica Diagnostic Session Ready\n");
4237
5549
  }
4238
5550
  process.stderr.write(guardLine);
4239
5551
  process.stderr.write("Type 'exit' to end the session.\n\n");
4240
5552
  const turnGate = createTurnGate();
4241
- const agentQuery = (0, import_claude_agent_sdk4.query)({
5553
+ const agentQuery = (0, import_claude_agent_sdk5.query)({
4242
5554
  prompt: makeInteractivePrompt(abortController, turnGate, spinner),
4243
5555
  options
4244
5556
  });
@@ -4265,10 +5577,10 @@ async function runAgent(session2, kineticaVersion, degraded, model, runOptions)
4265
5577
  if (message.type === "stream_event") {
4266
5578
  const { event: evt } = message;
4267
5579
  if (evt.type === "content_block_delta" && evt.delta.type === "text_delta") {
4268
- const text = evt.delta.text ?? "";
4269
- if (text) {
5580
+ const text2 = evt.delta.text ?? "";
5581
+ if (text2) {
4270
5582
  spinner.stop();
4271
- const output = tableAligner.push(text);
5583
+ const output = tableAligner.push(text2);
4272
5584
  if (output) {
4273
5585
  process.stderr.write(output);
4274
5586
  lastStreamCharWasNewline = output.endsWith("\n");
@@ -4434,7 +5746,7 @@ Rate limited \u2014 requests rejected.${resetStr}
4434
5746
  }
4435
5747
  } catch (error) {
4436
5748
  spinner.stop();
4437
- if (error instanceof import_claude_agent_sdk4.AbortError) {
5749
+ if (error instanceof import_claude_agent_sdk5.AbortError) {
4438
5750
  hadNonAbortError = false;
4439
5751
  } else {
4440
5752
  hadNonAbortError = true;
@@ -4481,7 +5793,7 @@ var MODEL_LABELS = {
4481
5793
  opus: "Opus \u2014 deepest reasoning, slower & pricier"
4482
5794
  };
4483
5795
  async function selectModel() {
4484
- return (0, import_prompts5.select)({
5796
+ return (0, import_prompts6.select)({
4485
5797
  message: "Select model for this session:",
4486
5798
  default: DEFAULT_AGENT_MODEL,
4487
5799
  choices: SUPPORTED_MODELS.map((value) => ({ value, name: MODEL_LABELS[value] }))
@@ -4489,7 +5801,7 @@ async function selectModel() {
4489
5801
  }
4490
5802
 
4491
5803
  // src/auth/preflight.ts
4492
- var import_claude_agent_sdk5 = require("@anthropic-ai/claude-agent-sdk");
5804
+ var import_claude_agent_sdk6 = require("@anthropic-ai/claude-agent-sdk");
4493
5805
 
4494
5806
  // src/auth/oauth-flow.ts
4495
5807
  var import_picocolors9 = __toESM(require("picocolors"));
@@ -4577,7 +5889,7 @@ async function authenticateAnthropic(options) {
4577
5889
  await new Promise(() => {
4578
5890
  });
4579
5891
  }
4580
- const authQuery = (0, import_claude_agent_sdk5.query)({
5892
+ const authQuery = (0, import_claude_agent_sdk6.query)({
4581
5893
  prompt: hangingPrompt(),
4582
5894
  options: {
4583
5895
  persistSession: false,
@@ -4609,12 +5921,12 @@ async function authenticateAnthropic(options) {
4609
5921
  // src/auth/logout.ts
4610
5922
  var import_child_process2 = require("child_process");
4611
5923
  var import_node_module = require("module");
4612
- var import_node_path5 = __toESM(require("path"));
5924
+ var import_node_path8 = __toESM(require("path"));
4613
5925
  var import_util = require("util");
4614
5926
  var execFileAsync = (0, import_util.promisify)(import_child_process2.execFile);
4615
5927
  function resolveSdkCliPath() {
4616
5928
  const require_ = (0, import_node_module.createRequire)(__filename);
4617
- 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");
4618
5930
  }
4619
5931
  async function logout() {
4620
5932
  try {
@@ -4634,9 +5946,9 @@ async function logout() {
4634
5946
 
4635
5947
  // src/session/env-file.ts
4636
5948
  var import_fs2 = require("fs");
4637
- var import_promises4 = require("fs/promises");
5949
+ var import_promises8 = require("fs/promises");
4638
5950
  var import_path2 = require("path");
4639
- var import_prompts6 = require("@inquirer/prompts");
5951
+ var import_prompts7 = require("@inquirer/prompts");
4640
5952
  var import_picocolors10 = __toESM(require("picocolors"));
4641
5953
  function parseEnvContent(content) {
4642
5954
  const entries = [];
@@ -4719,7 +6031,7 @@ function loadEnvFile(dir, env = process.env) {
4719
6031
  async function offerSaveCredentials(url, user, dir) {
4720
6032
  if (!process.stdin.isTTY) return;
4721
6033
  try {
4722
- const shouldSave = await (0, import_prompts6.confirm)({
6034
+ const shouldSave = await (0, import_prompts7.confirm)({
4723
6035
  message: "Save KINETICA_URL and KINETICA_USER to .env? (password is never saved)",
4724
6036
  default: true
4725
6037
  });
@@ -4727,11 +6039,11 @@ async function offerSaveCredentials(url, user, dir) {
4727
6039
  const filePath = (0, import_path2.join)(dir ?? process.cwd(), ".env");
4728
6040
  let existing;
4729
6041
  try {
4730
- existing = await (0, import_promises4.readFile)(filePath, "utf8");
6042
+ existing = await (0, import_promises8.readFile)(filePath, "utf8");
4731
6043
  } catch {
4732
6044
  }
4733
6045
  const content = buildEnvContent(url, user, existing);
4734
- await (0, import_promises4.writeFile)(filePath, content, "utf8");
6046
+ await (0, import_promises8.writeFile)(filePath, content, "utf8");
4735
6047
  console.error(import_picocolors10.default.dim("Saved to .env"));
4736
6048
  } catch (err) {
4737
6049
  const message = err instanceof Error ? err.message : String(err);
@@ -4741,10 +6053,10 @@ async function offerSaveCredentials(url, user, dir) {
4741
6053
 
4742
6054
  // src/session/verify.ts
4743
6055
  var import_picocolors13 = __toESM(require("picocolors"));
4744
- var import_prompts9 = require("@inquirer/prompts");
6056
+ var import_prompts10 = require("@inquirer/prompts");
4745
6057
 
4746
6058
  // src/session/collect.ts
4747
- var import_prompts7 = require("@inquirer/prompts");
6059
+ var import_prompts8 = require("@inquirer/prompts");
4748
6060
  var import_picocolors11 = __toESM(require("picocolors"));
4749
6061
  async function collectCredentials() {
4750
6062
  const prompted = /* @__PURE__ */ new Set();
@@ -4752,27 +6064,27 @@ async function collectCredentials() {
4752
6064
  const envUser = process.env.KINETICA_USER;
4753
6065
  if (envUrl && envUser && process.stdin.isTTY) {
4754
6066
  console.error(import_picocolors11.default.dim(`Saved connection: ${envUrl} (${envUser})`));
4755
- const useSaved = await (0, import_prompts7.confirm)({
6067
+ const useSaved = await (0, import_prompts8.confirm)({
4756
6068
  message: "Use saved connection?",
4757
6069
  default: true
4758
6070
  });
4759
6071
  if (!useSaved) {
4760
6072
  prompted.add("url");
4761
6073
  prompted.add("user");
4762
- const url2 = await (0, import_prompts7.input)({ message: "Kinetica endpoint URL:" });
4763
- const user2 = await (0, import_prompts7.input)({ message: "Admin username:" });
4764
- 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: "*" });
4765
6077
  return { credentials: { url: url2, user: user2, pass: pass2 }, prompted };
4766
6078
  }
4767
6079
  }
4768
- const url = envUrl ?? (prompted.add("url"), await (0, import_prompts7.input)({ message: "Kinetica endpoint URL:" }));
4769
- const user = envUser ?? (prompted.add("user"), await (0, import_prompts7.input)({ message: "Admin username:" }));
4770
- 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: "*" });
4771
6083
  return { credentials: { url, user, pass }, prompted };
4772
6084
  }
4773
6085
  async function repromptCredentials() {
4774
- const user = await (0, import_prompts7.input)({ message: "Admin username:" });
4775
- 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: "*" });
4776
6088
  return { user, pass };
4777
6089
  }
4778
6090
 
@@ -4783,8 +6095,9 @@ function replacePort(baseUrl, port) {
4783
6095
  parsed.port = String(port);
4784
6096
  return parsed.origin;
4785
6097
  }
4786
- function createSession(url, user, pass) {
6098
+ function createSession(url, user, pass, options) {
4787
6099
  const authHeader = "Basic " + Buffer.from(`${user}:${pass}`).toString("base64");
6100
+ const timeoutMs = options?.timeoutMs ?? REQUEST_TIMEOUT_MS;
4788
6101
  const doFetch = async (fullUrl, body) => {
4789
6102
  if (process.env.DEBUG) {
4790
6103
  console.error(`[DEBUG] POST ${fullUrl}`);
@@ -4796,7 +6109,7 @@ function createSession(url, user, pass) {
4796
6109
  "Content-Type": "application/json"
4797
6110
  },
4798
6111
  body: body !== void 0 ? JSON.stringify(body) : void 0,
4799
- signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
6112
+ signal: AbortSignal.timeout(timeoutMs)
4800
6113
  });
4801
6114
  };
4802
6115
  return {
@@ -4808,7 +6121,7 @@ function createSession(url, user, pass) {
4808
6121
 
4809
6122
  // src/session/resolve-url.ts
4810
6123
  var import_picocolors12 = __toESM(require("picocolors"));
4811
- var import_prompts8 = require("@inquirer/prompts");
6124
+ var import_prompts9 = require("@inquirer/prompts");
4812
6125
  var PROBE_TIMEOUT_MS2 = 3e3;
4813
6126
  var HTTP_PROTOCOL_RE = /^https?:\/\//i;
4814
6127
  var ANY_PROTOCOL_RE = /^[a-z][a-z0-9+.-]*:\/\//i;
@@ -4850,7 +6163,7 @@ async function confirmHttpFallback(host) {
4850
6163
  )
4851
6164
  );
4852
6165
  try {
4853
- return await (0, import_prompts8.confirm)({
6166
+ return await (0, import_prompts9.confirm)({
4854
6167
  message: "Continue over plaintext HTTP?",
4855
6168
  default: false
4856
6169
  });
@@ -4858,7 +6171,7 @@ async function confirmHttpFallback(host) {
4858
6171
  return false;
4859
6172
  }
4860
6173
  }
4861
- async function resolveUrl(input5) {
6174
+ async function resolveUrl(input5, options = {}) {
4862
6175
  const trimmed = input5.trim();
4863
6176
  if (trimmed === "") {
4864
6177
  return { ok: false, error: "URL is empty" };
@@ -4892,7 +6205,7 @@ async function resolveUrl(input5) {
4892
6205
  error: `Could not connect to ${normalized} via https:// or http://`
4893
6206
  };
4894
6207
  }
4895
- if (!isInteractive()) {
6208
+ if (options.nonInteractive || !isInteractive()) {
4896
6209
  return {
4897
6210
  ok: false,
4898
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.`
@@ -4912,6 +6225,7 @@ async function resolveUrl(input5) {
4912
6225
  var MAX_RETRIES = 3;
4913
6226
  var MAX_REPROMPTS = 2;
4914
6227
  var DEFAULT_HM_PORT2 = 9300;
6228
+ var BEST_EFFORT_PROBE_TIMEOUT_MS = 5e3;
4915
6229
  function extractVersion(responseBody) {
4916
6230
  try {
4917
6231
  const outer = JSON.parse(responseBody);
@@ -4958,6 +6272,24 @@ async function verifyConnectivity(session2) {
4958
6272
  function isCredentialError(errorMessage) {
4959
6273
  return errorMessage.startsWith("HTTP 401") || errorMessage.startsWith("HTTP 403");
4960
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
+ }
4961
6293
  async function connectWithRetry() {
4962
6294
  const { credentials, prompted } = await collectCredentials();
4963
6295
  const resolved = await resolveUrl(credentials.url);
@@ -4984,7 +6316,7 @@ async function connectWithRetry() {
4984
6316
  console.error(import_picocolors13.default.red(`Connection failed (attempt ${attempt}/${MAX_RETRIES}): ${msg}`));
4985
6317
  if (isCredentialError(msg)) {
4986
6318
  if (process.stdin.isTTY && repromptCount < MAX_REPROMPTS) {
4987
- const shouldRetry = await (0, import_prompts9.confirm)({
6319
+ const shouldRetry = await (0, import_prompts10.confirm)({
4988
6320
  message: "Credentials may be incorrect. Re-enter?",
4989
6321
  default: true
4990
6322
  });
@@ -5027,6 +6359,14 @@ async function connectWithRetry() {
5027
6359
  // src/cli/index.ts
5028
6360
  var verbose = false;
5029
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
+ }
5030
6370
  function printHelp() {
5031
6371
  const lines = [
5032
6372
  "",
@@ -5047,6 +6387,7 @@ function printHelp() {
5047
6387
  " --logout Log out from Anthropic account and exit",
5048
6388
  ` --model=NAME Override agent model (${SUPPORTED_MODELS.join(" | ")}); default: sonnet`,
5049
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",
5050
6391
  "",
5051
6392
  " Environment variables:",
5052
6393
  " ANTHROPIC_API_KEY Anthropic API key (if not set, OAuth login via browser is used)",
@@ -5078,12 +6419,9 @@ async function main() {
5078
6419
  return;
5079
6420
  }
5080
6421
  const forceLogin = args.includes("--login");
5081
- const loginMethodArg = args.find((a) => a.startsWith("--login-method="));
5082
- const loginMethod = loginMethodArg?.split("=")[1];
5083
- const loginOrgArg = args.find((a) => a.startsWith("--login-org="));
5084
- const loginOrgUUID = loginOrgArg?.split("=")[1];
5085
- const modelArg = args.find((a) => a.startsWith("--model="));
5086
- 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");
5087
6425
  let model;
5088
6426
  if (modelValue !== void 0) {
5089
6427
  if (SUPPORTED_MODELS.includes(modelValue)) {
@@ -5098,8 +6436,7 @@ async function main() {
5098
6436
  return;
5099
6437
  }
5100
6438
  }
5101
- const budgetArg = args.find((a) => a.startsWith("--max-budget="));
5102
- const budgetValue = budgetArg?.split("=")[1];
6439
+ const budgetValue = flagValue(args, "--max-budget");
5103
6440
  let maxBudgetFlag;
5104
6441
  if (budgetValue !== void 0) {
5105
6442
  const parsed = Number(budgetValue);
@@ -5115,6 +6452,16 @@ async function main() {
5115
6452
  }
5116
6453
  maxBudgetFlag = parsed;
5117
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
+ }
5118
6465
  loadEnvFile();
5119
6466
  printBanner();
5120
6467
  if (model === void 0 && process.stdin.isTTY) {
@@ -5132,6 +6479,47 @@ async function main() {
5132
6479
  process.stderr.write(import_picocolors14.default.dim("Authenticated via API key\n"));
5133
6480
  }
5134
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
+ }
5135
6523
  const { session: connectedSession, kineticaVersion, degraded } = await connectWithRetry();
5136
6524
  session = connectedSession;
5137
6525
  await runAgent(session, kineticaVersion, degraded, model, {
@@ -5155,6 +6543,7 @@ if (process.env.NODE_ENV !== "test") {
5155
6543
  }
5156
6544
  // Annotate the CommonJS export names for ESM import in node:
5157
6545
  0 && (module.exports = {
6546
+ chooseBundleSessionVersion,
5158
6547
  getSession,
5159
6548
  main,
5160
6549
  verbose