@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.
- package/README.md +61 -11
- package/dist/admin-agent.js +1518 -129
- package/knowledge/references/bundle/support-bundle.md +40 -0
- package/knowledge/references/version-quirks-7.2.md +7 -2
- package/package.json +1 -1
package/dist/admin-agent.js
CHANGED
|
@@ -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(
|
|
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 =
|
|
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
|
|
121
|
+
var import_prompts6 = require("@inquirer/prompts");
|
|
121
122
|
|
|
122
123
|
// src/agent/run-agent.ts
|
|
123
|
-
var
|
|
124
|
-
var
|
|
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 = (
|
|
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(
|
|
589
|
-
if (
|
|
590
|
-
const lines =
|
|
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
|
|
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((
|
|
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
|
-
(
|
|
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
|
-
|
|
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
|
|
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.
|
|
3540
|
-
2. After the report
|
|
3541
|
-
|
|
3542
|
-
|
|
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
|
|
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(
|
|
3674
|
-
if (!
|
|
3675
|
-
return Math.ceil(
|
|
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(
|
|
3722
|
-
if (isValidBudget(
|
|
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.
|
|
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/
|
|
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,
|
|
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
|
|
5136
|
+
let resolve3 = () => {
|
|
3894
5137
|
};
|
|
3895
5138
|
let promise = new Promise((r) => {
|
|
3896
|
-
|
|
5139
|
+
resolve3 = r;
|
|
3897
5140
|
});
|
|
3898
5141
|
return Object.freeze({
|
|
3899
5142
|
wait: () => promise,
|
|
3900
5143
|
open: () => {
|
|
3901
|
-
|
|
5144
|
+
resolve3();
|
|
3902
5145
|
},
|
|
3903
5146
|
close: () => {
|
|
3904
5147
|
promise = new Promise((r) => {
|
|
3905
|
-
|
|
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, (_,
|
|
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(
|
|
3930
|
-
return
|
|
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(
|
|
3985
|
-
if (!
|
|
3986
|
-
const combined = lineBuffer +
|
|
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(
|
|
4079
|
-
return EXIT_COMMANDS.has(
|
|
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,
|
|
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,
|
|
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
|
|
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
|
-
|
|
4194
|
-
|
|
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:
|
|
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
|
|
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
|
|
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
|
-
|
|
4230
|
-
|
|
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
|
-
"
|
|
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,
|
|
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
|
|
4269
|
-
if (
|
|
5580
|
+
const text2 = evt.delta.text ?? "";
|
|
5581
|
+
if (text2) {
|
|
4270
5582
|
spinner.stop();
|
|
4271
|
-
const output = tableAligner.push(
|
|
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
|
|
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,
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
5949
|
+
var import_promises8 = require("fs/promises");
|
|
4638
5950
|
var import_path2 = require("path");
|
|
4639
|
-
var
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
|
6056
|
+
var import_prompts10 = require("@inquirer/prompts");
|
|
4745
6057
|
|
|
4746
6058
|
// src/session/collect.ts
|
|
4747
|
-
var
|
|
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,
|
|
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,
|
|
4763
|
-
const user2 = await (0,
|
|
4764
|
-
const pass2 = await (0,
|
|
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,
|
|
4769
|
-
const user = envUser ?? (prompted.add("user"), await (0,
|
|
4770
|
-
const pass = process.env.KINETICA_PASS ?? await (0,
|
|
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,
|
|
4775
|
-
const pass = await (0,
|
|
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(
|
|
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
|
|
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,
|
|
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,
|
|
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
|
|
5082
|
-
const
|
|
5083
|
-
const
|
|
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
|
|
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
|