@kinetica/admin-agent 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +191 -0
- package/NOTICE +2 -0
- package/README.md +484 -0
- package/dist/admin-agent.js +4961 -0
- package/knowledge/playbooks/config-drift.md +26 -0
- package/knowledge/playbooks/gpu-out-of-memory.md +27 -0
- package/knowledge/playbooks/memory-pressure.md +29 -0
- package/knowledge/playbooks/query-contention.md +28 -0
- package/knowledge/playbooks/resource-group-exhaustion.md +27 -0
- package/knowledge/playbooks/stale-rank.md +26 -0
- package/knowledge/references/catalog-enums.md +82 -0
- package/knowledge/references/catalog-joins.md +105 -0
- package/knowledge/references/gpudb-conf.md +93 -0
- package/knowledge/references/mutation-safety.md +89 -0
- package/knowledge/references/rank-architecture.md +54 -0
- package/knowledge/references/sql-alter-table.md +78 -0
- package/knowledge/references/sql-create-index.md +49 -0
- package/knowledge/references/tiered-objects.md +106 -0
- package/knowledge/references/version-quirks-7.2.md +96 -0
- package/knowledge/templates/report.md +57 -0
- package/package.json +76 -0
|
@@ -0,0 +1,4961 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __export = (target, all) => {
|
|
10
|
+
for (var name in all)
|
|
11
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
12
|
+
};
|
|
13
|
+
var __copyProps = (to, from, except, desc) => {
|
|
14
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
15
|
+
for (let key of __getOwnPropNames(from))
|
|
16
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
17
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
18
|
+
}
|
|
19
|
+
return to;
|
|
20
|
+
};
|
|
21
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
22
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
23
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
24
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
25
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
26
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
27
|
+
mod
|
|
28
|
+
));
|
|
29
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
30
|
+
|
|
31
|
+
// src/cli/index.ts
|
|
32
|
+
var cli_exports = {};
|
|
33
|
+
__export(cli_exports, {
|
|
34
|
+
getSession: () => getSession,
|
|
35
|
+
main: () => main,
|
|
36
|
+
verbose: () => verbose
|
|
37
|
+
});
|
|
38
|
+
module.exports = __toCommonJS(cli_exports);
|
|
39
|
+
var import_picocolors14 = __toESM(require("picocolors"));
|
|
40
|
+
|
|
41
|
+
// src/cli/banner.ts
|
|
42
|
+
var import_picocolors = __toESM(require("picocolors"));
|
|
43
|
+
|
|
44
|
+
// src/cli/version.ts
|
|
45
|
+
var import_fs = require("fs");
|
|
46
|
+
var import_path = require("path");
|
|
47
|
+
function getVersion() {
|
|
48
|
+
try {
|
|
49
|
+
let dir = __dirname;
|
|
50
|
+
while (dir !== (0, import_path.dirname)(dir)) {
|
|
51
|
+
const candidate = (0, import_path.join)(dir, "package.json");
|
|
52
|
+
if ((0, import_fs.existsSync)(candidate)) {
|
|
53
|
+
const pkg = JSON.parse((0, import_fs.readFileSync)(candidate, "utf8"));
|
|
54
|
+
return pkg.version;
|
|
55
|
+
}
|
|
56
|
+
dir = (0, import_path.dirname)(dir);
|
|
57
|
+
}
|
|
58
|
+
return "0.0.0";
|
|
59
|
+
} catch {
|
|
60
|
+
return "0.0.0";
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// src/cli/banner.ts
|
|
65
|
+
var LOGO = ` \u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2557\u2588\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557
|
|
66
|
+
\u2588\u2588\u2551 \u2588\u2588\u2554\u255D\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u2550\u2588\u2588\u2554\u2550\u2550\u255D\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557
|
|
67
|
+
\u2588\u2588\u2588\u2588\u2588\u2554\u255D \u2588\u2588\u2551\u2588\u2588\u2554\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551
|
|
68
|
+
\u2588\u2588\u2554\u2550\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2551\u255A\u2588\u2588\u2557\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u255D \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551
|
|
69
|
+
\u2588\u2588\u2551 \u2588\u2588\u2557\u2588\u2588\u2551\u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551 \u2588\u2588\u2551\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551
|
|
70
|
+
\u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D`;
|
|
71
|
+
function lerpRgb(from, to, t) {
|
|
72
|
+
return [
|
|
73
|
+
Math.round(from[0] + (to[0] - from[0]) * t),
|
|
74
|
+
Math.round(from[1] + (to[1] - from[1]) * t),
|
|
75
|
+
Math.round(from[2] + (to[2] - from[2]) * t)
|
|
76
|
+
];
|
|
77
|
+
}
|
|
78
|
+
function dimRgb(color, factor) {
|
|
79
|
+
return [
|
|
80
|
+
Math.round(color[0] * factor),
|
|
81
|
+
Math.round(color[1] * factor),
|
|
82
|
+
Math.round(color[2] * factor)
|
|
83
|
+
];
|
|
84
|
+
}
|
|
85
|
+
var SHADOW_CHARS = /* @__PURE__ */ new Set(["\u2557", "\u2554", "\u2551", "\u255D", "\u255A", "\u2550"]);
|
|
86
|
+
var DIM_FACTOR = 0.4;
|
|
87
|
+
function gradientize(text) {
|
|
88
|
+
const PURPLE = [147, 51, 234];
|
|
89
|
+
const HOT_PINK = [236, 72, 153];
|
|
90
|
+
const RESET = "\x1B[0m";
|
|
91
|
+
const lines = text.split("\n");
|
|
92
|
+
const maxIdx = Math.max(lines.length - 1, 1);
|
|
93
|
+
return lines.map((line, i) => {
|
|
94
|
+
const bright = lerpRgb(PURPLE, HOT_PINK, i / maxIdx);
|
|
95
|
+
const dim = dimRgb(bright, DIM_FACTOR);
|
|
96
|
+
let result = "";
|
|
97
|
+
let currentMode = null;
|
|
98
|
+
for (const ch of line) {
|
|
99
|
+
const mode = SHADOW_CHARS.has(ch) ? "dim" : "bright";
|
|
100
|
+
if (mode !== currentMode) {
|
|
101
|
+
const [r, g, b] = mode === "dim" ? dim : bright;
|
|
102
|
+
result += `\x1B[38;2;${r};${g};${b}m`;
|
|
103
|
+
currentMode = mode;
|
|
104
|
+
}
|
|
105
|
+
result += ch;
|
|
106
|
+
}
|
|
107
|
+
return result + RESET;
|
|
108
|
+
}).join("\n");
|
|
109
|
+
}
|
|
110
|
+
function printBanner(model) {
|
|
111
|
+
const version = getVersion();
|
|
112
|
+
const subtitle = `admin-agent ${import_picocolors.default.dim(`v${version}`)}`;
|
|
113
|
+
const header = model ? `${subtitle}
|
|
114
|
+
${import_picocolors.default.dim(`model: ${model}`)}` : subtitle;
|
|
115
|
+
process.stderr.write("\n\n" + gradientize(LOGO) + "\n\n" + header + "\n");
|
|
116
|
+
return subtitle;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// src/auth/preflight.ts
|
|
120
|
+
var import_claude_agent_sdk = require("@anthropic-ai/claude-agent-sdk");
|
|
121
|
+
|
|
122
|
+
// src/auth/oauth-flow.ts
|
|
123
|
+
var import_picocolors2 = __toESM(require("picocolors"));
|
|
124
|
+
|
|
125
|
+
// src/auth/open-browser.ts
|
|
126
|
+
var import_child_process = require("child_process");
|
|
127
|
+
function openBrowser(url) {
|
|
128
|
+
try {
|
|
129
|
+
const platform = process.platform;
|
|
130
|
+
const { command, args } = platform === "darwin" ? { command: "open", args: [url] } : platform === "win32" ? { command: "cmd", args: ["/c", "start", "", url] } : { command: "xdg-open", args: [url] };
|
|
131
|
+
const child = (0, import_child_process.spawn)(command, args, { detached: true, stdio: "ignore" });
|
|
132
|
+
child.unref();
|
|
133
|
+
return true;
|
|
134
|
+
} catch {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// src/auth/oauth-flow.ts
|
|
140
|
+
async function resolveAuthentication(agentQuery, options) {
|
|
141
|
+
if (options.hasApiKey && !options.forceLogin) {
|
|
142
|
+
return { method: "api_key" };
|
|
143
|
+
}
|
|
144
|
+
const query3 = agentQuery;
|
|
145
|
+
try {
|
|
146
|
+
const { manualUrl, automaticUrl } = await query3.claudeAuthenticate(options.loginWithClaudeAi);
|
|
147
|
+
const opened = openBrowser(automaticUrl);
|
|
148
|
+
if (opened) {
|
|
149
|
+
process.stderr.write(import_picocolors2.default.dim("Browser opened for login. Waiting for authentication...\n"));
|
|
150
|
+
} else {
|
|
151
|
+
process.stderr.write(`
|
|
152
|
+
Open this URL in your browser to log in:
|
|
153
|
+
${import_picocolors2.default.bold(manualUrl)}
|
|
154
|
+
|
|
155
|
+
`);
|
|
156
|
+
process.stderr.write(import_picocolors2.default.dim("Waiting for browser login to complete...\n"));
|
|
157
|
+
}
|
|
158
|
+
await query3.claudeOAuthWaitForCompletion();
|
|
159
|
+
return { method: "oauth" };
|
|
160
|
+
} catch (err) {
|
|
161
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
162
|
+
process.stderr.write(
|
|
163
|
+
import_picocolors2.default.yellow(`
|
|
164
|
+
Warning: OAuth login failed (${message}). SDK may retry automatically.
|
|
165
|
+
`)
|
|
166
|
+
);
|
|
167
|
+
return { method: "oauth" };
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// src/auth/preflight.ts
|
|
172
|
+
var PROBE_TIMEOUT_MS = 1e4;
|
|
173
|
+
async function probeCachedCredentials(authQuery) {
|
|
174
|
+
try {
|
|
175
|
+
const info = await Promise.race([
|
|
176
|
+
authQuery.accountInfo(),
|
|
177
|
+
new Promise(
|
|
178
|
+
(_, reject) => setTimeout(() => reject(new Error("Probe timed out")), PROBE_TIMEOUT_MS)
|
|
179
|
+
)
|
|
180
|
+
]);
|
|
181
|
+
if (info.email || info.apiKeySource) {
|
|
182
|
+
return info;
|
|
183
|
+
}
|
|
184
|
+
return null;
|
|
185
|
+
} catch {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
async function authenticateAnthropic(options) {
|
|
190
|
+
const hasApiKey = Boolean(process.env.ANTHROPIC_API_KEY);
|
|
191
|
+
if (hasApiKey && !options.forceLogin) {
|
|
192
|
+
return { method: "api_key" };
|
|
193
|
+
}
|
|
194
|
+
if (!process.stdin.isTTY) {
|
|
195
|
+
throw new Error(
|
|
196
|
+
"No ANTHROPIC_API_KEY set and terminal is non-interactive. Set ANTHROPIC_API_KEY or run in an interactive terminal with --login."
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
const env = options.forceLogin ? (() => {
|
|
200
|
+
const { ANTHROPIC_API_KEY: _stripped, ...rest } = process.env;
|
|
201
|
+
return { ...rest, CLAUDE_AGENT_SDK_CLIENT_APP: "admin-agent" };
|
|
202
|
+
})() : { ...process.env, CLAUDE_AGENT_SDK_CLIENT_APP: "admin-agent" };
|
|
203
|
+
const abortController = new AbortController();
|
|
204
|
+
async function* hangingPrompt() {
|
|
205
|
+
await new Promise(() => {
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
const authQuery = (0, import_claude_agent_sdk.query)({
|
|
209
|
+
prompt: hangingPrompt(),
|
|
210
|
+
options: {
|
|
211
|
+
persistSession: false,
|
|
212
|
+
abortController,
|
|
213
|
+
env,
|
|
214
|
+
...options.loginMethod ? { forceLoginMethod: options.loginMethod } : {},
|
|
215
|
+
...options.loginOrgUUID ? { forceLoginOrgUUID: options.loginOrgUUID } : {}
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
try {
|
|
219
|
+
if (!options.forceLogin) {
|
|
220
|
+
const cached = await probeCachedCredentials(authQuery);
|
|
221
|
+
if (cached) {
|
|
222
|
+
return { method: "oauth", email: cached.email };
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return await resolveAuthentication(authQuery, {
|
|
226
|
+
forceLogin: options.forceLogin,
|
|
227
|
+
loginWithClaudeAi: (options.loginMethod ?? "claudeai") === "claudeai",
|
|
228
|
+
hasApiKey
|
|
229
|
+
});
|
|
230
|
+
} finally {
|
|
231
|
+
abortController.abort();
|
|
232
|
+
await authQuery.return().catch(() => {
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// src/auth/logout.ts
|
|
238
|
+
var import_child_process2 = require("child_process");
|
|
239
|
+
var import_node_module = require("module");
|
|
240
|
+
var import_node_path = __toESM(require("path"));
|
|
241
|
+
var import_util = require("util");
|
|
242
|
+
var execFileAsync = (0, import_util.promisify)(import_child_process2.execFile);
|
|
243
|
+
function resolveSdkCliPath() {
|
|
244
|
+
const require_ = (0, import_node_module.createRequire)(__filename);
|
|
245
|
+
return import_node_path.default.join(import_node_path.default.dirname(require_.resolve("@anthropic-ai/claude-agent-sdk")), "cli.js");
|
|
246
|
+
}
|
|
247
|
+
async function logout() {
|
|
248
|
+
try {
|
|
249
|
+
const sdkCliPath = resolveSdkCliPath();
|
|
250
|
+
const { stdout, stderr } = await execFileAsync(process.execPath, [
|
|
251
|
+
sdkCliPath,
|
|
252
|
+
"auth",
|
|
253
|
+
"logout"
|
|
254
|
+
]);
|
|
255
|
+
const output = (stdout || stderr || "").trim();
|
|
256
|
+
return { success: true, message: output || "Logged out successfully." };
|
|
257
|
+
} catch (err) {
|
|
258
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
259
|
+
return { success: false, message: `Logout failed: ${message}` };
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// src/session/env-file.ts
|
|
264
|
+
var import_fs2 = require("fs");
|
|
265
|
+
var import_promises = require("fs/promises");
|
|
266
|
+
var import_path2 = require("path");
|
|
267
|
+
var import_prompts = require("@inquirer/prompts");
|
|
268
|
+
var import_picocolors3 = __toESM(require("picocolors"));
|
|
269
|
+
function parseEnvContent(content) {
|
|
270
|
+
const entries = [];
|
|
271
|
+
for (const line of content.split("\n")) {
|
|
272
|
+
const trimmed = line.trim();
|
|
273
|
+
if (trimmed === "" || trimmed.startsWith("#")) continue;
|
|
274
|
+
const match = /^([A-Za-z_]\w*)=(.*)/.exec(trimmed);
|
|
275
|
+
if (!match) continue;
|
|
276
|
+
const key = match[1];
|
|
277
|
+
let value = match[2];
|
|
278
|
+
if (value.startsWith('"') && value.endsWith('"') && value.length >= 2) {
|
|
279
|
+
value = value.slice(1, -1).replace(/\\(["\\])/g, "$1");
|
|
280
|
+
} else if (value.startsWith("'") && value.endsWith("'") && value.length >= 2) {
|
|
281
|
+
value = value.slice(1, -1);
|
|
282
|
+
}
|
|
283
|
+
entries.push([key, value]);
|
|
284
|
+
}
|
|
285
|
+
return new Map(entries);
|
|
286
|
+
}
|
|
287
|
+
var FORBIDDEN_VALUE_CHARS = /[\n\r\0]/;
|
|
288
|
+
var REQUIRES_QUOTING = /[\s"'#]/;
|
|
289
|
+
function escapeEnvValue(value) {
|
|
290
|
+
if (FORBIDDEN_VALUE_CHARS.test(value)) {
|
|
291
|
+
throw new Error(
|
|
292
|
+
"Env value contains a forbidden character (newline or null byte); refusing to write"
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
if (!REQUIRES_QUOTING.test(value)) {
|
|
296
|
+
return value;
|
|
297
|
+
}
|
|
298
|
+
const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
299
|
+
return `"${escaped}"`;
|
|
300
|
+
}
|
|
301
|
+
var ENV_TEMPLATE = `# Anthropic API key (optional \u2014 if not set, OAuth login via browser is used)
|
|
302
|
+
ANTHROPIC_API_KEY=
|
|
303
|
+
|
|
304
|
+
# Kinetica connection details (prompted interactively if not set)
|
|
305
|
+
# If password is omitted, the agent will prompt for it at startup
|
|
306
|
+
KINETICA_URL={url}
|
|
307
|
+
KINETICA_USER={user}
|
|
308
|
+
KINETICA_PASS=
|
|
309
|
+
`;
|
|
310
|
+
function buildEnvContent(url, user, existingContent) {
|
|
311
|
+
const safeUrl = escapeEnvValue(url);
|
|
312
|
+
const safeUser = escapeEnvValue(user);
|
|
313
|
+
if (!existingContent?.trim()) {
|
|
314
|
+
return ENV_TEMPLATE.replace("{url}", safeUrl).replace("{user}", safeUser);
|
|
315
|
+
}
|
|
316
|
+
const lines = existingContent.split("\n");
|
|
317
|
+
let urlReplaced = false;
|
|
318
|
+
let userReplaced = false;
|
|
319
|
+
const updated = lines.map((line) => {
|
|
320
|
+
if (/^KINETICA_URL=/.exec(line)) {
|
|
321
|
+
urlReplaced = true;
|
|
322
|
+
return `KINETICA_URL=${safeUrl}`;
|
|
323
|
+
}
|
|
324
|
+
if (/^KINETICA_USER=/.exec(line)) {
|
|
325
|
+
userReplaced = true;
|
|
326
|
+
return `KINETICA_USER=${safeUser}`;
|
|
327
|
+
}
|
|
328
|
+
return line;
|
|
329
|
+
});
|
|
330
|
+
if (!urlReplaced) updated.push(`KINETICA_URL=${safeUrl}`);
|
|
331
|
+
if (!userReplaced) updated.push(`KINETICA_USER=${safeUser}`);
|
|
332
|
+
return updated.join("\n");
|
|
333
|
+
}
|
|
334
|
+
function loadEnvFile(dir, env = process.env) {
|
|
335
|
+
try {
|
|
336
|
+
const filePath = (0, import_path2.join)(dir ?? process.cwd(), ".env");
|
|
337
|
+
const content = (0, import_fs2.readFileSync)(filePath, "utf8");
|
|
338
|
+
const parsed = parseEnvContent(content);
|
|
339
|
+
for (const [key, value] of parsed) {
|
|
340
|
+
if (env[key] === void 0 && value !== "") {
|
|
341
|
+
env[key] = value;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
} catch {
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
async function offerSaveCredentials(url, user, dir) {
|
|
348
|
+
if (!process.stdin.isTTY) return;
|
|
349
|
+
try {
|
|
350
|
+
const shouldSave = await (0, import_prompts.confirm)({
|
|
351
|
+
message: "Save KINETICA_URL and KINETICA_USER to .env? (password is never saved)",
|
|
352
|
+
default: true
|
|
353
|
+
});
|
|
354
|
+
if (!shouldSave) return;
|
|
355
|
+
const filePath = (0, import_path2.join)(dir ?? process.cwd(), ".env");
|
|
356
|
+
let existing;
|
|
357
|
+
try {
|
|
358
|
+
existing = await (0, import_promises.readFile)(filePath, "utf8");
|
|
359
|
+
} catch {
|
|
360
|
+
}
|
|
361
|
+
const content = buildEnvContent(url, user, existing);
|
|
362
|
+
await (0, import_promises.writeFile)(filePath, content, "utf8");
|
|
363
|
+
console.error(import_picocolors3.default.dim("Saved to .env"));
|
|
364
|
+
} catch (err) {
|
|
365
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
366
|
+
console.error(import_picocolors3.default.yellow(`Could not save .env file: ${message}`));
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// src/session/verify.ts
|
|
371
|
+
var import_picocolors6 = __toESM(require("picocolors"));
|
|
372
|
+
var import_prompts4 = require("@inquirer/prompts");
|
|
373
|
+
|
|
374
|
+
// src/session/collect.ts
|
|
375
|
+
var import_prompts2 = require("@inquirer/prompts");
|
|
376
|
+
var import_picocolors4 = __toESM(require("picocolors"));
|
|
377
|
+
async function collectCredentials() {
|
|
378
|
+
const prompted = /* @__PURE__ */ new Set();
|
|
379
|
+
const envUrl = process.env.KINETICA_URL;
|
|
380
|
+
const envUser = process.env.KINETICA_USER;
|
|
381
|
+
if (envUrl && envUser && process.stdin.isTTY) {
|
|
382
|
+
console.error(import_picocolors4.default.dim(`Saved connection: ${envUrl} (${envUser})`));
|
|
383
|
+
const useSaved = await (0, import_prompts2.confirm)({
|
|
384
|
+
message: "Use saved connection?",
|
|
385
|
+
default: true
|
|
386
|
+
});
|
|
387
|
+
if (!useSaved) {
|
|
388
|
+
prompted.add("url");
|
|
389
|
+
prompted.add("user");
|
|
390
|
+
const url2 = await (0, import_prompts2.input)({ message: "Kinetica endpoint URL:" });
|
|
391
|
+
const user2 = await (0, import_prompts2.input)({ message: "Admin username:" });
|
|
392
|
+
const pass2 = await (0, import_prompts2.password)({ message: "Admin password:", mask: "*" });
|
|
393
|
+
return { credentials: { url: url2, user: user2, pass: pass2 }, prompted };
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
const url = envUrl ?? (prompted.add("url"), await (0, import_prompts2.input)({ message: "Kinetica endpoint URL:" }));
|
|
397
|
+
const user = envUser ?? (prompted.add("user"), await (0, import_prompts2.input)({ message: "Admin username:" }));
|
|
398
|
+
const pass = process.env.KINETICA_PASS ?? await (0, import_prompts2.password)({ message: "Admin password:", mask: "*" });
|
|
399
|
+
return { credentials: { url, user, pass }, prompted };
|
|
400
|
+
}
|
|
401
|
+
async function repromptCredentials() {
|
|
402
|
+
const user = await (0, import_prompts2.input)({ message: "Admin username:" });
|
|
403
|
+
const pass = await (0, import_prompts2.password)({ message: "Admin password:", mask: "*" });
|
|
404
|
+
return { user, pass };
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// src/session/KineticaSession.ts
|
|
408
|
+
var REQUEST_TIMEOUT_MS = 3e4;
|
|
409
|
+
function replacePort(baseUrl, port) {
|
|
410
|
+
const parsed = new URL(baseUrl);
|
|
411
|
+
parsed.port = String(port);
|
|
412
|
+
return parsed.origin;
|
|
413
|
+
}
|
|
414
|
+
function createSession(url, user, pass) {
|
|
415
|
+
const authHeader = "Basic " + Buffer.from(`${user}:${pass}`).toString("base64");
|
|
416
|
+
const doFetch = async (fullUrl, body) => {
|
|
417
|
+
if (process.env.DEBUG) {
|
|
418
|
+
console.error(`[DEBUG] POST ${fullUrl}`);
|
|
419
|
+
}
|
|
420
|
+
return fetch(fullUrl, {
|
|
421
|
+
method: "POST",
|
|
422
|
+
headers: {
|
|
423
|
+
Authorization: authHeader,
|
|
424
|
+
"Content-Type": "application/json"
|
|
425
|
+
},
|
|
426
|
+
body: body !== void 0 ? JSON.stringify(body) : void 0,
|
|
427
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
428
|
+
});
|
|
429
|
+
};
|
|
430
|
+
return {
|
|
431
|
+
baseUrl: url,
|
|
432
|
+
makeRequest: (endpoint, body) => doFetch(`${url}${endpoint}`, body),
|
|
433
|
+
makeRequestToPort: (port, endpoint, body) => doFetch(`${replacePort(url, port)}${endpoint}`, body)
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// src/session/resolve-url.ts
|
|
438
|
+
var import_picocolors5 = __toESM(require("picocolors"));
|
|
439
|
+
var import_prompts3 = require("@inquirer/prompts");
|
|
440
|
+
var PROBE_TIMEOUT_MS2 = 3e3;
|
|
441
|
+
var HTTP_PROTOCOL_RE = /^https?:\/\//i;
|
|
442
|
+
var ANY_PROTOCOL_RE = /^[a-z][a-z0-9+.-]*:\/\//i;
|
|
443
|
+
function hasProtocol(input5) {
|
|
444
|
+
return HTTP_PROTOCOL_RE.test(input5);
|
|
445
|
+
}
|
|
446
|
+
function stripTrailingSlashes(url) {
|
|
447
|
+
return url.replace(/\/+$/, "");
|
|
448
|
+
}
|
|
449
|
+
async function probeProtocol(url) {
|
|
450
|
+
try {
|
|
451
|
+
await fetch(url, {
|
|
452
|
+
method: "HEAD",
|
|
453
|
+
signal: AbortSignal.timeout(PROBE_TIMEOUT_MS2)
|
|
454
|
+
});
|
|
455
|
+
return true;
|
|
456
|
+
} catch {
|
|
457
|
+
return false;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
function isHttpsOnly() {
|
|
461
|
+
return process.env.KINETICA_HTTPS_ONLY === "1";
|
|
462
|
+
}
|
|
463
|
+
function isInteractive() {
|
|
464
|
+
return Boolean(process.stdin.isTTY);
|
|
465
|
+
}
|
|
466
|
+
async function confirmHttpFallback(host) {
|
|
467
|
+
process.stderr.write(
|
|
468
|
+
"\n" + import_picocolors5.default.red(
|
|
469
|
+
import_picocolors5.default.bold(
|
|
470
|
+
` WARNING: HTTPS unavailable at ${host}.
|
|
471
|
+
Falling back to plaintext HTTP will transmit your Kinetica credentials in the clear.
|
|
472
|
+
`
|
|
473
|
+
)
|
|
474
|
+
) + import_picocolors5.default.dim(
|
|
475
|
+
` Set KINETICA_HTTPS_ONLY=1 to refuse this fallback automatically, or pass an explicit http:// prefix to silence this prompt.
|
|
476
|
+
|
|
477
|
+
`
|
|
478
|
+
)
|
|
479
|
+
);
|
|
480
|
+
try {
|
|
481
|
+
return await (0, import_prompts3.confirm)({
|
|
482
|
+
message: "Continue over plaintext HTTP?",
|
|
483
|
+
default: false
|
|
484
|
+
});
|
|
485
|
+
} catch {
|
|
486
|
+
return false;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
async function resolveUrl(input5) {
|
|
490
|
+
const trimmed = input5.trim();
|
|
491
|
+
if (trimmed === "") {
|
|
492
|
+
return { ok: false, error: "URL is empty" };
|
|
493
|
+
}
|
|
494
|
+
const normalized = stripTrailingSlashes(trimmed);
|
|
495
|
+
if (hasProtocol(normalized)) {
|
|
496
|
+
return { ok: true, url: normalized };
|
|
497
|
+
}
|
|
498
|
+
if (ANY_PROTOCOL_RE.test(normalized)) {
|
|
499
|
+
const scheme = normalized.split("://")[0];
|
|
500
|
+
return {
|
|
501
|
+
ok: false,
|
|
502
|
+
error: `Unsupported protocol: ${scheme}. Use http:// or https://`
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
const httpsUrl = `https://${normalized}`;
|
|
506
|
+
const httpUrl = `http://${normalized}`;
|
|
507
|
+
console.error(import_picocolors5.default.dim("Detecting protocol..."));
|
|
508
|
+
if (await probeProtocol(httpsUrl)) {
|
|
509
|
+
return { ok: true, url: httpsUrl };
|
|
510
|
+
}
|
|
511
|
+
if (isHttpsOnly()) {
|
|
512
|
+
return {
|
|
513
|
+
ok: false,
|
|
514
|
+
error: `HTTPS probe to ${httpsUrl} failed and KINETICA_HTTPS_ONLY=1; refusing to fall back to plaintext HTTP.`
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
if (!await probeProtocol(httpUrl)) {
|
|
518
|
+
return {
|
|
519
|
+
ok: false,
|
|
520
|
+
error: `Could not connect to ${normalized} via https:// or http://`
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
if (!isInteractive()) {
|
|
524
|
+
return {
|
|
525
|
+
ok: false,
|
|
526
|
+
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.`
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
const approved = await confirmHttpFallback(normalized);
|
|
530
|
+
if (!approved) {
|
|
531
|
+
return {
|
|
532
|
+
ok: false,
|
|
533
|
+
error: "User declined plaintext HTTP fallback"
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
return { ok: true, url: httpUrl };
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// src/session/verify.ts
|
|
540
|
+
var MAX_RETRIES = 3;
|
|
541
|
+
var MAX_REPROMPTS = 2;
|
|
542
|
+
var DEFAULT_HM_PORT = 9300;
|
|
543
|
+
function extractVersion(responseBody) {
|
|
544
|
+
try {
|
|
545
|
+
const outer = JSON.parse(responseBody);
|
|
546
|
+
if (typeof outer.data_str !== "string") return void 0;
|
|
547
|
+
const inner = JSON.parse(outer.data_str);
|
|
548
|
+
const systemStr = inner.status_map?.system;
|
|
549
|
+
if (typeof systemStr !== "string") return void 0;
|
|
550
|
+
const system = JSON.parse(systemStr);
|
|
551
|
+
return typeof system.version === "string" ? system.version : void 0;
|
|
552
|
+
} catch {
|
|
553
|
+
return void 0;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
function extractVersionFromHostManager(responseBody) {
|
|
557
|
+
try {
|
|
558
|
+
const parsed = JSON.parse(responseBody);
|
|
559
|
+
return typeof parsed.version === "string" ? parsed.version : void 0;
|
|
560
|
+
} catch {
|
|
561
|
+
return void 0;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
async function probeHostManager(session2) {
|
|
565
|
+
if (!session2.makeRequestToPort) {
|
|
566
|
+
return { ok: false };
|
|
567
|
+
}
|
|
568
|
+
try {
|
|
569
|
+
const response = await session2.makeRequestToPort(DEFAULT_HM_PORT, "/", void 0);
|
|
570
|
+
if (!response.ok) return { ok: false };
|
|
571
|
+
const body = await response.text();
|
|
572
|
+
return { ok: true, version: extractVersionFromHostManager(body) };
|
|
573
|
+
} catch {
|
|
574
|
+
return { ok: false };
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
async function verifyConnectivity(session2) {
|
|
578
|
+
const response = await session2.makeRequest("/show/system/status", {});
|
|
579
|
+
if (!response.ok) {
|
|
580
|
+
const body2 = await response.text();
|
|
581
|
+
throw new Error(`HTTP ${response.status}: ${body2}`);
|
|
582
|
+
}
|
|
583
|
+
const body = await response.text();
|
|
584
|
+
return extractVersion(body);
|
|
585
|
+
}
|
|
586
|
+
function isCredentialError(errorMessage) {
|
|
587
|
+
return errorMessage.startsWith("HTTP 401") || errorMessage.startsWith("HTTP 403");
|
|
588
|
+
}
|
|
589
|
+
async function connectWithRetry() {
|
|
590
|
+
const { credentials, prompted } = await collectCredentials();
|
|
591
|
+
const resolved = await resolveUrl(credentials.url);
|
|
592
|
+
if (!resolved.ok) {
|
|
593
|
+
console.error(import_picocolors6.default.red(resolved.error));
|
|
594
|
+
process.exit(1);
|
|
595
|
+
}
|
|
596
|
+
const resolvedUrl = resolved.url;
|
|
597
|
+
let currentUser = credentials.user;
|
|
598
|
+
let currentPass = credentials.pass;
|
|
599
|
+
let wasReprompted = false;
|
|
600
|
+
let repromptCount = 0;
|
|
601
|
+
let session2 = createSession(resolvedUrl, currentUser, currentPass);
|
|
602
|
+
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
|
603
|
+
try {
|
|
604
|
+
const kineticaVersion = await verifyConnectivity(session2);
|
|
605
|
+
console.error(import_picocolors6.default.green("Connected to Kinetica successfully."));
|
|
606
|
+
if (prompted.size > 0 || wasReprompted) {
|
|
607
|
+
await offerSaveCredentials(resolvedUrl, currentUser);
|
|
608
|
+
}
|
|
609
|
+
return { session: session2, kineticaVersion, degraded: false };
|
|
610
|
+
} catch (err) {
|
|
611
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
612
|
+
console.error(import_picocolors6.default.red(`Connection failed (attempt ${attempt}/${MAX_RETRIES}): ${msg}`));
|
|
613
|
+
if (isCredentialError(msg)) {
|
|
614
|
+
if (process.stdin.isTTY && repromptCount < MAX_REPROMPTS) {
|
|
615
|
+
const shouldRetry = await (0, import_prompts4.confirm)({
|
|
616
|
+
message: "Credentials may be incorrect. Re-enter?",
|
|
617
|
+
default: true
|
|
618
|
+
});
|
|
619
|
+
if (shouldRetry) {
|
|
620
|
+
const fresh = await repromptCredentials();
|
|
621
|
+
currentUser = fresh.user;
|
|
622
|
+
currentPass = fresh.pass;
|
|
623
|
+
wasReprompted = true;
|
|
624
|
+
repromptCount++;
|
|
625
|
+
session2 = createSession(resolvedUrl, currentUser, currentPass);
|
|
626
|
+
attempt = 0;
|
|
627
|
+
continue;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
console.error(import_picocolors6.default.red("Authentication failed. Exiting."));
|
|
631
|
+
process.exit(1);
|
|
632
|
+
}
|
|
633
|
+
if (attempt === MAX_RETRIES) {
|
|
634
|
+
console.error(import_picocolors6.default.yellow("DB engine unreachable. Probing host manager on port 9300..."));
|
|
635
|
+
const hmResult = await probeHostManager(session2);
|
|
636
|
+
if (hmResult.ok) {
|
|
637
|
+
console.error(
|
|
638
|
+
import_picocolors6.default.yellow(
|
|
639
|
+
"Connected in DEGRADED MODE (host manager only). Most diagnostic tools will be unavailable."
|
|
640
|
+
)
|
|
641
|
+
);
|
|
642
|
+
if (prompted.size > 0 || wasReprompted) {
|
|
643
|
+
await offerSaveCredentials(resolvedUrl, currentUser);
|
|
644
|
+
}
|
|
645
|
+
return { session: session2, kineticaVersion: hmResult.version, degraded: true };
|
|
646
|
+
}
|
|
647
|
+
console.error(import_picocolors6.default.red("Host manager also unreachable. Exiting."));
|
|
648
|
+
process.exit(1);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
throw new Error("unreachable");
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// src/agent/run-agent.ts
|
|
656
|
+
var import_claude_agent_sdk5 = require("@anthropic-ai/claude-agent-sdk");
|
|
657
|
+
var import_prompts8 = require("@inquirer/prompts");
|
|
658
|
+
var import_picocolors13 = __toESM(require("picocolors"));
|
|
659
|
+
|
|
660
|
+
// src/agent/diagnostic-sql.ts
|
|
661
|
+
function has(columns, name) {
|
|
662
|
+
return columns.includes(name);
|
|
663
|
+
}
|
|
664
|
+
var FALLBACK_QUERY_HISTORY_SQL = `-- Slow queries in the last hour
|
|
665
|
+
SELECT query_id, user_name, query_text, start_time, stop_time,
|
|
666
|
+
TIMESTAMPDIFF(SECOND, start_time, stop_time) AS elapsed_sec
|
|
667
|
+
FROM ki_catalog.ki_query_history
|
|
668
|
+
WHERE start_time > NOW() - INTERVAL '1' HOUR
|
|
669
|
+
ORDER BY elapsed_sec DESC
|
|
670
|
+
LIMIT 20;`;
|
|
671
|
+
var FALLBACK_ACTIVE_QUERIES_SQL = `-- Currently active queries
|
|
672
|
+
SELECT query_id, user_name, query_text, start_time, execution_status
|
|
673
|
+
FROM ki_catalog.ki_query_active_all
|
|
674
|
+
ORDER BY start_time ASC;`;
|
|
675
|
+
var FALLBACK_TIERED_OBJECTS_SQL = `-- Objects in disk tier (potential memory pressure)
|
|
676
|
+
-- NOTE: id is a string (e.g. @schema@oid[col][0]), NOT a numeric OID. Filter by table: WHERE id LIKE '%table_name%'
|
|
677
|
+
-- For per-table tier placement, prefer kinetica_resource_objects with table_names filter.
|
|
678
|
+
SELECT id, tier, size, source_rank, owner_resource_group
|
|
679
|
+
FROM ki_catalog.ki_tiered_objects
|
|
680
|
+
WHERE tier != 'VRAM'
|
|
681
|
+
ORDER BY size DESC
|
|
682
|
+
LIMIT 20;`;
|
|
683
|
+
var FALLBACK_OBJ_STAT_SQL = `-- Table sizes and row counts
|
|
684
|
+
SELECT object_name, total_bytes, row_count
|
|
685
|
+
FROM ki_catalog.ki_obj_stat
|
|
686
|
+
ORDER BY total_bytes DESC
|
|
687
|
+
LIMIT 30;`;
|
|
688
|
+
var FALLBACK_COLUMNS_SQL = `-- Structural column metadata (use kinetica_show_table for Kinetica-native types)
|
|
689
|
+
SELECT c.table_name, c.column_name, c.column_position
|
|
690
|
+
FROM ki_catalog.ki_columns c
|
|
691
|
+
WHERE c.table_name = '<TABLE_NAME>'
|
|
692
|
+
ORDER BY c.column_position;`;
|
|
693
|
+
var FALLBACK_DATATYPES_SQL = `-- Resolve column_type_oid to human-readable type name
|
|
694
|
+
SELECT oid, name, sql_typename
|
|
695
|
+
FROM ki_catalog.ki_datatypes
|
|
696
|
+
ORDER BY oid;`;
|
|
697
|
+
var FALLBACK_QUERY_SPAN_METRICS_SQL = `-- Query span metrics for a specific query
|
|
698
|
+
SELECT query_id, span_id, parent_span_id, operator, sql_step,
|
|
699
|
+
metric_data, start_time, stop_time, source_rank
|
|
700
|
+
FROM ki_catalog.ki_query_span_metrics_all
|
|
701
|
+
WHERE query_id = '<QUERY_ID>'
|
|
702
|
+
ORDER BY start_time;`;
|
|
703
|
+
var FALLBACK_QUERY_WORKERS_SQL = `-- Active query workers (non-idle)
|
|
704
|
+
SELECT job_id, worker_id, type, status, elapsed_time_ms, source_rank
|
|
705
|
+
FROM ki_catalog.ki_query_workers
|
|
706
|
+
WHERE status != 'IDLE'
|
|
707
|
+
ORDER BY elapsed_time_ms DESC;`;
|
|
708
|
+
var FALLBACK_OBJECTS_SQL = `-- Object registry and metadata
|
|
709
|
+
SELECT oid, object_name, schema_name, type_id, persistence, obj_kind,
|
|
710
|
+
creation_time, last_read_time, read_count, write_count
|
|
711
|
+
FROM ki_catalog.ki_objects
|
|
712
|
+
ORDER BY last_read_time DESC
|
|
713
|
+
LIMIT 30;`;
|
|
714
|
+
var FALLBACK_PARTITIONS_SQL = `-- Partition sizes and tier distribution
|
|
715
|
+
SELECT oid, object_name, schema_name, rank_num, partition_type,
|
|
716
|
+
partition_id, num_rows, actual_bytes, tier
|
|
717
|
+
FROM ki_catalog.ki_partitions
|
|
718
|
+
ORDER BY actual_bytes DESC
|
|
719
|
+
LIMIT 30;`;
|
|
720
|
+
var FALLBACK_INDEXES_SQL = `-- Index definitions
|
|
721
|
+
SELECT oid, object_name, schema_name, index_type, index_columns
|
|
722
|
+
FROM ki_catalog.ki_indexes
|
|
723
|
+
ORDER BY object_name;`;
|
|
724
|
+
var FALLBACK_PERIODIC_OBJECTS_SQL = `-- Periodic refresh schedules
|
|
725
|
+
SELECT oid, object_name, schema_name, last_refresh_time,
|
|
726
|
+
next_refresh_time, additional_info
|
|
727
|
+
FROM ki_catalog.ki_periodic_objects
|
|
728
|
+
ORDER BY next_refresh_time;`;
|
|
729
|
+
var FALLBACK_USERS_AND_ROLES_SQL = `-- Users and roles
|
|
730
|
+
SELECT oid, name, can_login, is_superuser, resource_group
|
|
731
|
+
FROM ki_catalog.ki_users_and_roles
|
|
732
|
+
ORDER BY name;`;
|
|
733
|
+
var FALLBACK_OBJECT_PERMISSIONS_SQL = `-- Object permissions
|
|
734
|
+
SELECT role_name, permission_type, object_type, object_name, with_grant_option
|
|
735
|
+
FROM ki_catalog.ki_object_permissions
|
|
736
|
+
ORDER BY object_name, role_name;`;
|
|
737
|
+
var FALLBACK_DEPEND_SQL = `-- Object dependency graph
|
|
738
|
+
SELECT src_obj_oid, src_obj_kind, dep_obj_oid, dep_obj_kind, mv_oid, dep_kind
|
|
739
|
+
FROM ki_catalog.ki_depend;`;
|
|
740
|
+
var FALLBACK_LOAD_HISTORY_SQL = `-- Recent data load history
|
|
741
|
+
SELECT table_oid, datasource_oid, user_name, load_kind,
|
|
742
|
+
start_time, end_time, rows_inserted, event_message
|
|
743
|
+
FROM ki_catalog.ki_load_history
|
|
744
|
+
WHERE start_time > NOW() - INTERVAL '1' HOUR
|
|
745
|
+
ORDER BY start_time DESC
|
|
746
|
+
LIMIT 20;`;
|
|
747
|
+
var FALLBACK_BACKUP_HISTORY_SQL = `-- Backup history
|
|
748
|
+
SELECT backup_name, operation, status, start_time, end_time,
|
|
749
|
+
num_files, num_bytes, num_records
|
|
750
|
+
FROM ki_catalog.ki_backup_history
|
|
751
|
+
ORDER BY start_time DESC
|
|
752
|
+
LIMIT 20;`;
|
|
753
|
+
var FALLBACK_KAFKA_LAG_INFO_SQL = `-- Kafka consumer lag
|
|
754
|
+
SELECT datasource_oid, table_oid, schema_name, table_name,
|
|
755
|
+
partition_id, highest_offset, last_committed_offset
|
|
756
|
+
FROM ki_catalog.ki_kafka_lag_info
|
|
757
|
+
ORDER BY datasource_oid, partition_id;`;
|
|
758
|
+
function buildQueryHistorySql(columns) {
|
|
759
|
+
const select = columns.join(", ");
|
|
760
|
+
const elapsed = has(columns, "start_time") && has(columns, "stop_time") ? ",\n TIMESTAMPDIFF(SECOND, start_time, stop_time) AS elapsed_sec" : "";
|
|
761
|
+
const where = has(columns, "start_time") ? "\nWHERE start_time > NOW() - INTERVAL '1' HOUR" : "";
|
|
762
|
+
const orderBy = has(columns, "start_time") && has(columns, "stop_time") ? (
|
|
763
|
+
// Kinetica does not support timestamp arithmetic in ORDER BY; use the elapsed_sec alias instead
|
|
764
|
+
"\nORDER BY elapsed_sec DESC"
|
|
765
|
+
) : "";
|
|
766
|
+
return `-- Slow queries in the last hour
|
|
767
|
+
SELECT ${select}${elapsed}
|
|
768
|
+
FROM ki_catalog.ki_query_history${where}${orderBy}
|
|
769
|
+
LIMIT 20;`;
|
|
770
|
+
}
|
|
771
|
+
function buildActiveQueriesSql(columns) {
|
|
772
|
+
const select = columns.join(", ");
|
|
773
|
+
const orderBy = has(columns, "start_time") ? "\nORDER BY start_time ASC" : "";
|
|
774
|
+
return `-- Currently active queries
|
|
775
|
+
SELECT ${select}
|
|
776
|
+
FROM ki_catalog.ki_query_active_all${orderBy};`;
|
|
777
|
+
}
|
|
778
|
+
function buildTieredObjectsSql(columns) {
|
|
779
|
+
const select = columns.join(", ");
|
|
780
|
+
const where = has(columns, "tier") ? "\nWHERE tier != 'VRAM'" : "";
|
|
781
|
+
const orderBy = has(columns, "size") ? "\nORDER BY size DESC" : "";
|
|
782
|
+
const idNote = has(columns, "id") ? "\n-- NOTE: id is a string (e.g. @schema@oid[col][0]), NOT a numeric OID. Filter by table: WHERE id LIKE '%table_name%'" : "";
|
|
783
|
+
return `-- Objects in disk tier (potential memory pressure)${idNote}
|
|
784
|
+
SELECT ${select}
|
|
785
|
+
FROM ki_catalog.ki_tiered_objects${where}${orderBy}
|
|
786
|
+
LIMIT 20;`;
|
|
787
|
+
}
|
|
788
|
+
function buildObjStatSql(columns) {
|
|
789
|
+
const select = columns.join(", ");
|
|
790
|
+
const orderBy = has(columns, "total_bytes") ? "\nORDER BY total_bytes DESC" : "";
|
|
791
|
+
return `-- Table sizes and row counts
|
|
792
|
+
SELECT ${select}
|
|
793
|
+
FROM ki_catalog.ki_obj_stat${orderBy}
|
|
794
|
+
LIMIT 30;`;
|
|
795
|
+
}
|
|
796
|
+
var STRUCTURAL_COLUMNS = /* @__PURE__ */ new Set([
|
|
797
|
+
"table_name",
|
|
798
|
+
"column_name",
|
|
799
|
+
"column_position",
|
|
800
|
+
"is_nullable",
|
|
801
|
+
"is_shard_key",
|
|
802
|
+
"is_primary_key",
|
|
803
|
+
"is_dict_encoded",
|
|
804
|
+
"default_value",
|
|
805
|
+
"bytes_on_disk_uncompressed",
|
|
806
|
+
"bytes_on_disk_compressed"
|
|
807
|
+
]);
|
|
808
|
+
function buildColumnsSql(columns) {
|
|
809
|
+
const filtered = columns.filter((c) => STRUCTURAL_COLUMNS.has(c));
|
|
810
|
+
const selectCols = filtered.length > 0 ? filtered : columns;
|
|
811
|
+
const select = selectCols.map((c) => `c.${c}`).join(", ");
|
|
812
|
+
const where = has(selectCols, "table_name") ? "\nWHERE c.table_name = '<TABLE_NAME>'" : "";
|
|
813
|
+
const orderBy = has(selectCols, "column_position") ? "\nORDER BY c.column_position" : has(selectCols, "column_name") ? "\nORDER BY c.column_name" : "";
|
|
814
|
+
return `-- Structural column metadata (use kinetica_show_table for Kinetica-native types)
|
|
815
|
+
SELECT ${select}
|
|
816
|
+
FROM ki_catalog.ki_columns c${where}${orderBy};`;
|
|
817
|
+
}
|
|
818
|
+
function buildDatatypesSql(columns) {
|
|
819
|
+
const select = columns.join(", ");
|
|
820
|
+
const orderBy = has(columns, "oid") ? "\nORDER BY oid" : "";
|
|
821
|
+
return `-- Resolve column_type_oid to human-readable type name
|
|
822
|
+
SELECT ${select}
|
|
823
|
+
FROM ki_catalog.ki_datatypes${orderBy};`;
|
|
824
|
+
}
|
|
825
|
+
function buildQuerySpanMetricsSql(columns) {
|
|
826
|
+
const select = columns.join(", ");
|
|
827
|
+
const where = has(columns, "query_id") ? "\nWHERE query_id = '<QUERY_ID>'" : "";
|
|
828
|
+
const orderBy = has(columns, "start_time") ? "\nORDER BY start_time" : "";
|
|
829
|
+
return `-- Query span metrics for a specific query
|
|
830
|
+
SELECT ${select}
|
|
831
|
+
FROM ki_catalog.ki_query_span_metrics_all${where}${orderBy};`;
|
|
832
|
+
}
|
|
833
|
+
function buildQueryWorkersSql(columns) {
|
|
834
|
+
const select = columns.join(", ");
|
|
835
|
+
const where = has(columns, "status") ? "\nWHERE status != 'IDLE'" : "";
|
|
836
|
+
const orderBy = has(columns, "elapsed_time_ms") ? "\nORDER BY elapsed_time_ms DESC" : "";
|
|
837
|
+
return `-- Active query workers (non-idle)
|
|
838
|
+
SELECT ${select}
|
|
839
|
+
FROM ki_catalog.ki_query_workers${where}${orderBy};`;
|
|
840
|
+
}
|
|
841
|
+
function buildObjectsSql(columns) {
|
|
842
|
+
const select = columns.join(", ");
|
|
843
|
+
const orderBy = has(columns, "last_read_time") ? "\nORDER BY last_read_time DESC" : has(columns, "creation_time") ? "\nORDER BY creation_time DESC" : "";
|
|
844
|
+
return `-- Object registry and metadata
|
|
845
|
+
SELECT ${select}
|
|
846
|
+
FROM ki_catalog.ki_objects${orderBy}
|
|
847
|
+
LIMIT 30;`;
|
|
848
|
+
}
|
|
849
|
+
function buildPartitionsSql(columns) {
|
|
850
|
+
const select = columns.join(", ");
|
|
851
|
+
const orderBy = has(columns, "actual_bytes") ? "\nORDER BY actual_bytes DESC" : "";
|
|
852
|
+
return `-- Partition sizes and tier distribution
|
|
853
|
+
SELECT ${select}
|
|
854
|
+
FROM ki_catalog.ki_partitions${orderBy}
|
|
855
|
+
LIMIT 30;`;
|
|
856
|
+
}
|
|
857
|
+
function buildIndexesSql(columns) {
|
|
858
|
+
const select = columns.join(", ");
|
|
859
|
+
const orderBy = has(columns, "object_name") ? "\nORDER BY object_name" : "";
|
|
860
|
+
return `-- Index definitions
|
|
861
|
+
SELECT ${select}
|
|
862
|
+
FROM ki_catalog.ki_indexes${orderBy};`;
|
|
863
|
+
}
|
|
864
|
+
function buildPeriodicObjectsSql(columns) {
|
|
865
|
+
const select = columns.join(", ");
|
|
866
|
+
const orderBy = has(columns, "next_refresh_time") ? "\nORDER BY next_refresh_time" : "";
|
|
867
|
+
return `-- Periodic refresh schedules
|
|
868
|
+
SELECT ${select}
|
|
869
|
+
FROM ki_catalog.ki_periodic_objects${orderBy};`;
|
|
870
|
+
}
|
|
871
|
+
function buildUsersAndRolesSql(columns) {
|
|
872
|
+
const select = columns.join(", ");
|
|
873
|
+
const orderBy = has(columns, "name") ? "\nORDER BY name" : "";
|
|
874
|
+
return `-- Users and roles
|
|
875
|
+
SELECT ${select}
|
|
876
|
+
FROM ki_catalog.ki_users_and_roles${orderBy};`;
|
|
877
|
+
}
|
|
878
|
+
function buildObjectPermissionsSql(columns) {
|
|
879
|
+
const select = columns.join(", ");
|
|
880
|
+
const orderBy = has(columns, "object_name") && has(columns, "role_name") ? "\nORDER BY object_name, role_name" : has(columns, "object_name") ? "\nORDER BY object_name" : "";
|
|
881
|
+
return `-- Object permissions
|
|
882
|
+
SELECT ${select}
|
|
883
|
+
FROM ki_catalog.ki_object_permissions${orderBy};`;
|
|
884
|
+
}
|
|
885
|
+
function buildDependSql(columns) {
|
|
886
|
+
const select = columns.join(", ");
|
|
887
|
+
return `-- Object dependency graph
|
|
888
|
+
SELECT ${select}
|
|
889
|
+
FROM ki_catalog.ki_depend;`;
|
|
890
|
+
}
|
|
891
|
+
function buildLoadHistorySql(columns) {
|
|
892
|
+
const select = columns.join(", ");
|
|
893
|
+
const where = has(columns, "start_time") ? "\nWHERE start_time > NOW() - INTERVAL '1' HOUR" : "";
|
|
894
|
+
const orderBy = has(columns, "start_time") ? "\nORDER BY start_time DESC" : "";
|
|
895
|
+
return `-- Recent data load history
|
|
896
|
+
SELECT ${select}
|
|
897
|
+
FROM ki_catalog.ki_load_history${where}${orderBy}
|
|
898
|
+
LIMIT 20;`;
|
|
899
|
+
}
|
|
900
|
+
function buildBackupHistorySql(columns) {
|
|
901
|
+
const select = columns.join(", ");
|
|
902
|
+
const orderBy = has(columns, "start_time") ? "\nORDER BY start_time DESC" : "";
|
|
903
|
+
return `-- Backup history
|
|
904
|
+
SELECT ${select}
|
|
905
|
+
FROM ki_catalog.ki_backup_history${orderBy}
|
|
906
|
+
LIMIT 20;`;
|
|
907
|
+
}
|
|
908
|
+
function buildKafkaLagInfoSql(columns) {
|
|
909
|
+
const select = columns.join(", ");
|
|
910
|
+
const orderBy = has(columns, "datasource_oid") && has(columns, "partition_id") ? "\nORDER BY datasource_oid, partition_id" : has(columns, "datasource_oid") ? "\nORDER BY datasource_oid" : "";
|
|
911
|
+
return `-- Kafka consumer lag
|
|
912
|
+
SELECT ${select}
|
|
913
|
+
FROM ki_catalog.ki_kafka_lag_info${orderBy};`;
|
|
914
|
+
}
|
|
915
|
+
var BUILDER_REGISTRY = [
|
|
916
|
+
// Query History and Performance
|
|
917
|
+
{
|
|
918
|
+
table: "ki_query_history",
|
|
919
|
+
section: "Query History and Performance",
|
|
920
|
+
build: buildQueryHistorySql,
|
|
921
|
+
fallback: FALLBACK_QUERY_HISTORY_SQL
|
|
922
|
+
},
|
|
923
|
+
{
|
|
924
|
+
table: "ki_query_active_all",
|
|
925
|
+
section: "Query History and Performance",
|
|
926
|
+
build: buildActiveQueriesSql,
|
|
927
|
+
fallback: FALLBACK_ACTIVE_QUERIES_SQL
|
|
928
|
+
},
|
|
929
|
+
{
|
|
930
|
+
table: "ki_query_span_metrics_all",
|
|
931
|
+
section: "Query History and Performance",
|
|
932
|
+
build: buildQuerySpanMetricsSql,
|
|
933
|
+
fallback: FALLBACK_QUERY_SPAN_METRICS_SQL
|
|
934
|
+
},
|
|
935
|
+
{
|
|
936
|
+
table: "ki_query_workers",
|
|
937
|
+
section: "Query History and Performance",
|
|
938
|
+
build: buildQueryWorkersSql,
|
|
939
|
+
fallback: FALLBACK_QUERY_WORKERS_SQL
|
|
940
|
+
},
|
|
941
|
+
// Memory and Storage Tiers
|
|
942
|
+
{
|
|
943
|
+
table: "ki_tiered_objects",
|
|
944
|
+
section: "Memory and Storage Tiers",
|
|
945
|
+
build: buildTieredObjectsSql,
|
|
946
|
+
fallback: FALLBACK_TIERED_OBJECTS_SQL
|
|
947
|
+
},
|
|
948
|
+
{
|
|
949
|
+
table: "ki_obj_stat",
|
|
950
|
+
section: "Memory and Storage Tiers",
|
|
951
|
+
build: buildObjStatSql,
|
|
952
|
+
fallback: FALLBACK_OBJ_STAT_SQL
|
|
953
|
+
},
|
|
954
|
+
{
|
|
955
|
+
table: "ki_partitions",
|
|
956
|
+
section: "Memory and Storage Tiers",
|
|
957
|
+
build: buildPartitionsSql,
|
|
958
|
+
fallback: FALLBACK_PARTITIONS_SQL
|
|
959
|
+
},
|
|
960
|
+
// Object Registry and Metadata
|
|
961
|
+
{
|
|
962
|
+
table: "ki_objects",
|
|
963
|
+
section: "Object Registry and Metadata",
|
|
964
|
+
build: buildObjectsSql,
|
|
965
|
+
fallback: FALLBACK_OBJECTS_SQL
|
|
966
|
+
},
|
|
967
|
+
{
|
|
968
|
+
table: "ki_indexes",
|
|
969
|
+
section: "Object Registry and Metadata",
|
|
970
|
+
build: buildIndexesSql,
|
|
971
|
+
fallback: FALLBACK_INDEXES_SQL
|
|
972
|
+
},
|
|
973
|
+
{
|
|
974
|
+
table: "ki_periodic_objects",
|
|
975
|
+
section: "Object Registry and Metadata",
|
|
976
|
+
build: buildPeriodicObjectsSql,
|
|
977
|
+
fallback: FALLBACK_PERIODIC_OBJECTS_SQL
|
|
978
|
+
},
|
|
979
|
+
{
|
|
980
|
+
table: "ki_depend",
|
|
981
|
+
section: "Object Registry and Metadata",
|
|
982
|
+
build: buildDependSql,
|
|
983
|
+
fallback: FALLBACK_DEPEND_SQL
|
|
984
|
+
},
|
|
985
|
+
// Security and Access Control
|
|
986
|
+
{
|
|
987
|
+
table: "ki_users_and_roles",
|
|
988
|
+
section: "Security and Access Control",
|
|
989
|
+
build: buildUsersAndRolesSql,
|
|
990
|
+
fallback: FALLBACK_USERS_AND_ROLES_SQL
|
|
991
|
+
},
|
|
992
|
+
{
|
|
993
|
+
table: "ki_object_permissions",
|
|
994
|
+
section: "Security and Access Control",
|
|
995
|
+
build: buildObjectPermissionsSql,
|
|
996
|
+
fallback: FALLBACK_OBJECT_PERMISSIONS_SQL
|
|
997
|
+
},
|
|
998
|
+
// Data Ingestion and Operations
|
|
999
|
+
{
|
|
1000
|
+
table: "ki_load_history",
|
|
1001
|
+
section: "Data Ingestion and Operations",
|
|
1002
|
+
build: buildLoadHistorySql,
|
|
1003
|
+
fallback: FALLBACK_LOAD_HISTORY_SQL
|
|
1004
|
+
},
|
|
1005
|
+
{
|
|
1006
|
+
table: "ki_backup_history",
|
|
1007
|
+
section: "Data Ingestion and Operations",
|
|
1008
|
+
build: buildBackupHistorySql,
|
|
1009
|
+
fallback: FALLBACK_BACKUP_HISTORY_SQL
|
|
1010
|
+
},
|
|
1011
|
+
{
|
|
1012
|
+
table: "ki_kafka_lag_info",
|
|
1013
|
+
section: "Data Ingestion and Operations",
|
|
1014
|
+
build: buildKafkaLagInfoSql,
|
|
1015
|
+
fallback: FALLBACK_KAFKA_LAG_INFO_SQL
|
|
1016
|
+
},
|
|
1017
|
+
// Schema Inspection
|
|
1018
|
+
{
|
|
1019
|
+
table: "ki_columns",
|
|
1020
|
+
section: "Schema Inspection",
|
|
1021
|
+
build: buildColumnsSql,
|
|
1022
|
+
fallback: FALLBACK_COLUMNS_SQL
|
|
1023
|
+
},
|
|
1024
|
+
{
|
|
1025
|
+
table: "ki_datatypes",
|
|
1026
|
+
section: "Schema Inspection",
|
|
1027
|
+
build: buildDatatypesSql,
|
|
1028
|
+
fallback: FALLBACK_DATATYPES_SQL
|
|
1029
|
+
}
|
|
1030
|
+
];
|
|
1031
|
+
|
|
1032
|
+
// src/tools/index.ts
|
|
1033
|
+
var import_claude_agent_sdk3 = require("@anthropic-ai/claude-agent-sdk");
|
|
1034
|
+
var import_zod16 = require("zod");
|
|
1035
|
+
var import_picocolors9 = __toESM(require("picocolors"));
|
|
1036
|
+
|
|
1037
|
+
// src/approval/registry.ts
|
|
1038
|
+
var DEFAULT_READ_ONLY_TOOLS = /* @__PURE__ */ new Set();
|
|
1039
|
+
function createRegistry(tools = DEFAULT_READ_ONLY_TOOLS) {
|
|
1040
|
+
return {
|
|
1041
|
+
isReadOnlyTool: (toolName) => tools.has(toolName),
|
|
1042
|
+
// Returns a NEW registry — never mutates the current one
|
|
1043
|
+
registerReadOnlyTool: (toolName) => createRegistry(/* @__PURE__ */ new Set([...tools, toolName])),
|
|
1044
|
+
tools
|
|
1045
|
+
};
|
|
1046
|
+
}
|
|
1047
|
+
var defaultRegistry = createRegistry();
|
|
1048
|
+
var isReadOnlyTool = defaultRegistry.isReadOnlyTool;
|
|
1049
|
+
var READ_ONLY_TOOLS = defaultRegistry.tools;
|
|
1050
|
+
|
|
1051
|
+
// src/output/stringify.ts
|
|
1052
|
+
function stringifyValue(v) {
|
|
1053
|
+
if (v === void 0 || v === null) return "";
|
|
1054
|
+
if (typeof v === "string") return v;
|
|
1055
|
+
if (typeof v === "number" || typeof v === "boolean" || typeof v === "bigint") {
|
|
1056
|
+
return v.toString();
|
|
1057
|
+
}
|
|
1058
|
+
if (typeof v === "object") return JSON.stringify(v);
|
|
1059
|
+
if (typeof v === "symbol") return v.toString();
|
|
1060
|
+
return "";
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
// src/output/format.ts
|
|
1064
|
+
function formatOutput(json) {
|
|
1065
|
+
if (json === null || json === void 0) {
|
|
1066
|
+
return "(empty)";
|
|
1067
|
+
}
|
|
1068
|
+
if (Array.isArray(json)) {
|
|
1069
|
+
return formatArray(json);
|
|
1070
|
+
}
|
|
1071
|
+
if (typeof json === "object") {
|
|
1072
|
+
return formatObject(json);
|
|
1073
|
+
}
|
|
1074
|
+
return stringifyValue(json);
|
|
1075
|
+
}
|
|
1076
|
+
function formatArray(arr) {
|
|
1077
|
+
if (arr.length === 0) {
|
|
1078
|
+
return "(no results)";
|
|
1079
|
+
}
|
|
1080
|
+
const first = arr[0];
|
|
1081
|
+
if (typeof first === "object" && first !== null && !Array.isArray(first)) {
|
|
1082
|
+
return formatTableArray(arr);
|
|
1083
|
+
}
|
|
1084
|
+
return arr.map(stringifyValue).join("\n");
|
|
1085
|
+
}
|
|
1086
|
+
function formatTableArray(rows) {
|
|
1087
|
+
const headers = Object.keys(rows[0]);
|
|
1088
|
+
const cells = rows.map((row) => headers.map((h) => stringifyValue(row[h])));
|
|
1089
|
+
const colWidths = headers.map(
|
|
1090
|
+
(h, i) => Math.max(h.length, ...cells.map((row) => row[i].length), 3)
|
|
1091
|
+
);
|
|
1092
|
+
const pad = (text, col) => text.padEnd(colWidths[col]);
|
|
1093
|
+
const headerRow = `| ${headers.map((h, i) => pad(h, i)).join(" | ")} |`;
|
|
1094
|
+
const separatorRow = `| ${colWidths.map((w) => "-".repeat(w)).join(" | ")} |`;
|
|
1095
|
+
const dataRows = cells.map((row) => `| ${row.map((cell, i) => pad(cell, i)).join(" | ")} |`);
|
|
1096
|
+
return [headerRow, separatorRow, ...dataRows].join("\n");
|
|
1097
|
+
}
|
|
1098
|
+
function formatObject(obj) {
|
|
1099
|
+
return Object.entries(obj).map(([key, value]) => {
|
|
1100
|
+
if (typeof value === "object" && value !== null) {
|
|
1101
|
+
return `**${key}:**
|
|
1102
|
+
${formatOutput(value)}`;
|
|
1103
|
+
}
|
|
1104
|
+
return `**${key}:** ${stringifyValue(value)}`;
|
|
1105
|
+
}).join("\n");
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
// src/output/format-tool-name.ts
|
|
1109
|
+
function formatToolName(toolName) {
|
|
1110
|
+
const stripped = toolName.replace(/^mcp__[^_]+__/, "").replace(/^kinetica_/, "");
|
|
1111
|
+
return stripped.replace(/_/g, " ");
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
// src/types/index.ts
|
|
1115
|
+
var DEFAULT_TRUNCATION = {
|
|
1116
|
+
headLines: 150,
|
|
1117
|
+
tailLines: 50
|
|
1118
|
+
};
|
|
1119
|
+
|
|
1120
|
+
// src/output/truncate.ts
|
|
1121
|
+
function truncateOutput(text, options = DEFAULT_TRUNCATION) {
|
|
1122
|
+
if (text === "") return "";
|
|
1123
|
+
const lines = text.split("\n");
|
|
1124
|
+
const { headLines, tailLines } = options;
|
|
1125
|
+
const threshold = headLines + tailLines;
|
|
1126
|
+
if (lines.length <= threshold) {
|
|
1127
|
+
return text;
|
|
1128
|
+
}
|
|
1129
|
+
const truncatedCount = lines.length - headLines - tailLines;
|
|
1130
|
+
const head = lines.slice(0, headLines);
|
|
1131
|
+
const tail = lines.slice(lines.length - tailLines);
|
|
1132
|
+
return [...head, "", `[... ${truncatedCount} lines truncated ...]`, "", ...tail].join("\n");
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
// src/output/reshape.ts
|
|
1136
|
+
function safeString(v) {
|
|
1137
|
+
if (typeof v === "object" && v !== null) {
|
|
1138
|
+
return JSON.stringify(v);
|
|
1139
|
+
}
|
|
1140
|
+
return String(v);
|
|
1141
|
+
}
|
|
1142
|
+
function flatObjectToRows(obj, keyLabel = "key", valueLabel = "value") {
|
|
1143
|
+
return Object.entries(obj).map(([k, v]) => ({
|
|
1144
|
+
[keyLabel]: k,
|
|
1145
|
+
[valueLabel]: safeString(v)
|
|
1146
|
+
}));
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
// src/tools/rest/parse-data-str.ts
|
|
1150
|
+
function parseDataStr(outerDataStr, raw) {
|
|
1151
|
+
if (typeof outerDataStr !== "string") {
|
|
1152
|
+
return { ok: true, data: void 0 };
|
|
1153
|
+
}
|
|
1154
|
+
try {
|
|
1155
|
+
return { ok: true, data: JSON.parse(outerDataStr) };
|
|
1156
|
+
} catch (err) {
|
|
1157
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1158
|
+
return { ok: false, status: 200, error: `data_str parse error: ${message}`, raw };
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
// src/tools/rest/health.ts
|
|
1163
|
+
async function healthCheck(session2) {
|
|
1164
|
+
let response;
|
|
1165
|
+
let rawText;
|
|
1166
|
+
try {
|
|
1167
|
+
response = await session2.makeRequest("/show/system/status", {});
|
|
1168
|
+
rawText = await response.text();
|
|
1169
|
+
} catch (err) {
|
|
1170
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1171
|
+
return {
|
|
1172
|
+
ok: false,
|
|
1173
|
+
status: 0,
|
|
1174
|
+
error: message,
|
|
1175
|
+
raw: ""
|
|
1176
|
+
};
|
|
1177
|
+
}
|
|
1178
|
+
if (!response.ok) {
|
|
1179
|
+
return {
|
|
1180
|
+
ok: false,
|
|
1181
|
+
status: response.status,
|
|
1182
|
+
error: `HTTP ${response.status}`,
|
|
1183
|
+
raw: rawText
|
|
1184
|
+
};
|
|
1185
|
+
}
|
|
1186
|
+
let parsed;
|
|
1187
|
+
try {
|
|
1188
|
+
parsed = JSON.parse(rawText);
|
|
1189
|
+
} catch (err) {
|
|
1190
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1191
|
+
return {
|
|
1192
|
+
ok: false,
|
|
1193
|
+
status: 200,
|
|
1194
|
+
error: `JSON parse error: ${message}`,
|
|
1195
|
+
raw: rawText
|
|
1196
|
+
};
|
|
1197
|
+
}
|
|
1198
|
+
const inner = parseDataStr(parsed.data_str, rawText);
|
|
1199
|
+
if (!inner.ok) return inner;
|
|
1200
|
+
const statusMap = inner.data?.status_map ?? {};
|
|
1201
|
+
return {
|
|
1202
|
+
ok: true,
|
|
1203
|
+
data: flatObjectToRows(statusMap, "component", "status")
|
|
1204
|
+
};
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
// src/tools/rest/decode-nested-json.ts
|
|
1208
|
+
function decodeNestedJsonStrings(obj) {
|
|
1209
|
+
return Object.fromEntries(
|
|
1210
|
+
Object.entries(obj).map(([k, v]) => {
|
|
1211
|
+
if (typeof v !== "string") return [k, v];
|
|
1212
|
+
try {
|
|
1213
|
+
const parsed = JSON.parse(v);
|
|
1214
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
1215
|
+
return [k, decodeNestedJsonStrings(parsed)];
|
|
1216
|
+
}
|
|
1217
|
+
return [k, parsed];
|
|
1218
|
+
} catch {
|
|
1219
|
+
return [k, v];
|
|
1220
|
+
}
|
|
1221
|
+
})
|
|
1222
|
+
);
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
// src/tools/rest/flatten-rank-stats.ts
|
|
1226
|
+
function findTier(tiers, prefix) {
|
|
1227
|
+
for (const [name, data] of Object.entries(tiers)) {
|
|
1228
|
+
if (name === prefix || name.startsWith(`${prefix}.`) || name.startsWith(`${prefix}_`) || name.startsWith(prefix) && /^\d/.test(name.slice(prefix.length))) {
|
|
1229
|
+
if (typeof data === "object" && data !== null && !Array.isArray(data)) {
|
|
1230
|
+
return data;
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
return void 0;
|
|
1235
|
+
}
|
|
1236
|
+
function overallField(tier, field) {
|
|
1237
|
+
if (!tier) return "";
|
|
1238
|
+
const overall = tier.overall;
|
|
1239
|
+
if (typeof overall !== "object" || overall === null) return "";
|
|
1240
|
+
const value = overall[field];
|
|
1241
|
+
return stringifyValue(value);
|
|
1242
|
+
}
|
|
1243
|
+
function computePercent(used, limit) {
|
|
1244
|
+
if (!used || !limit) return "";
|
|
1245
|
+
const usedN = Number(used);
|
|
1246
|
+
const limitN = Number(limit);
|
|
1247
|
+
if (!Number.isFinite(usedN) || !Number.isFinite(limitN) || limitN === 0) return "";
|
|
1248
|
+
return `${(usedN / limitN * 100).toFixed(1)}%`;
|
|
1249
|
+
}
|
|
1250
|
+
function flattenRanksSummary(decoded) {
|
|
1251
|
+
const ranksRaw = typeof decoded.ranks === "object" && decoded.ranks !== null ? decoded.ranks : decoded;
|
|
1252
|
+
return Object.entries(ranksRaw).filter(([, v]) => typeof v === "object" && v !== null && !Array.isArray(v)).map(([rankId, rankData]) => {
|
|
1253
|
+
const rank = rankData;
|
|
1254
|
+
const tiers = typeof rank.tiers === "object" && rank.tiers !== null ? rank.tiers : {};
|
|
1255
|
+
const ramUsed = overallField(findTier(tiers, "RAM"), "used");
|
|
1256
|
+
const ramLimit = overallField(findTier(tiers, "RAM"), "limit");
|
|
1257
|
+
return {
|
|
1258
|
+
rank: rankId,
|
|
1259
|
+
ram_used: ramUsed,
|
|
1260
|
+
ram_limit: ramLimit,
|
|
1261
|
+
ram_percent: computePercent(ramUsed, ramLimit),
|
|
1262
|
+
persist_used: overallField(findTier(tiers, "PERSIST"), "used"),
|
|
1263
|
+
disk_used: overallField(findTier(tiers, "DISK"), "used"),
|
|
1264
|
+
vram_used: overallField(findTier(tiers, "VRAM"), "used")
|
|
1265
|
+
};
|
|
1266
|
+
});
|
|
1267
|
+
}
|
|
1268
|
+
function flattenRankDetail(rankData) {
|
|
1269
|
+
const tiersRaw = typeof rankData.tiers === "object" && rankData.tiers !== null ? rankData.tiers : {};
|
|
1270
|
+
const tiers = Object.entries(tiersRaw).filter(([, v]) => typeof v === "object" && v !== null && !Array.isArray(v)).map(([tierName, tierData]) => {
|
|
1271
|
+
const tier = tierData;
|
|
1272
|
+
const overall = typeof tier.overall === "object" && tier.overall !== null ? tier.overall : {};
|
|
1273
|
+
const stats = typeof tier.stats === "object" && tier.stats !== null ? tier.stats : {};
|
|
1274
|
+
const row = { tier: tierName };
|
|
1275
|
+
for (const [k, v] of Object.entries(overall)) {
|
|
1276
|
+
row[k] = stringifyValue(v);
|
|
1277
|
+
}
|
|
1278
|
+
for (const [k, v] of Object.entries(stats)) {
|
|
1279
|
+
row[k] = stringifyValue(v);
|
|
1280
|
+
}
|
|
1281
|
+
return row;
|
|
1282
|
+
});
|
|
1283
|
+
const rgRaw = typeof rankData.resource_groups === "object" && rankData.resource_groups !== null ? rankData.resource_groups : {};
|
|
1284
|
+
const resource_groups = Object.entries(rgRaw).filter(([, v]) => typeof v === "object" && v !== null && !Array.isArray(v)).map(([rgName, rgData]) => {
|
|
1285
|
+
const rg = rgData;
|
|
1286
|
+
const row = { name: rgName };
|
|
1287
|
+
for (const [k, v] of Object.entries(rg)) {
|
|
1288
|
+
row[k] = stringifyValue(v);
|
|
1289
|
+
}
|
|
1290
|
+
return row;
|
|
1291
|
+
});
|
|
1292
|
+
return { tiers, resource_groups };
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
// src/tools/rest/metrics.ts
|
|
1296
|
+
async function getMetrics(session2, nodeId) {
|
|
1297
|
+
let response;
|
|
1298
|
+
let rawText;
|
|
1299
|
+
try {
|
|
1300
|
+
response = await session2.makeRequest("/show/resource/statistics", {
|
|
1301
|
+
options: {}
|
|
1302
|
+
});
|
|
1303
|
+
rawText = await response.text();
|
|
1304
|
+
} catch (err) {
|
|
1305
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1306
|
+
return { ok: false, status: 0, error: message, raw: "" };
|
|
1307
|
+
}
|
|
1308
|
+
if (!response.ok) {
|
|
1309
|
+
return {
|
|
1310
|
+
ok: false,
|
|
1311
|
+
status: response.status,
|
|
1312
|
+
error: `HTTP ${response.status}`,
|
|
1313
|
+
raw: rawText
|
|
1314
|
+
};
|
|
1315
|
+
}
|
|
1316
|
+
let parsed;
|
|
1317
|
+
try {
|
|
1318
|
+
parsed = JSON.parse(rawText);
|
|
1319
|
+
} catch (err) {
|
|
1320
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1321
|
+
return {
|
|
1322
|
+
ok: false,
|
|
1323
|
+
status: 200,
|
|
1324
|
+
error: `JSON parse error: ${message}`,
|
|
1325
|
+
raw: rawText
|
|
1326
|
+
};
|
|
1327
|
+
}
|
|
1328
|
+
const inner = parseDataStr(parsed.data_str, rawText);
|
|
1329
|
+
if (!inner.ok) return inner;
|
|
1330
|
+
const statisticsMap = inner.data?.statistics_map ?? {};
|
|
1331
|
+
const decoded = decodeNestedJsonStrings(statisticsMap);
|
|
1332
|
+
const rows = flattenRanksSummary(decoded);
|
|
1333
|
+
if (nodeId !== void 0) {
|
|
1334
|
+
return { ok: true, data: rows, note: `Filtered for node: ${nodeId}` };
|
|
1335
|
+
}
|
|
1336
|
+
return { ok: true, data: rows };
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
// src/tools/rest/summarize-shards.ts
|
|
1340
|
+
var EMPTY_SUMMARY = {
|
|
1341
|
+
shard_array_version: void 0,
|
|
1342
|
+
total_shards: 0,
|
|
1343
|
+
rank_count: 0,
|
|
1344
|
+
distribution: [],
|
|
1345
|
+
balanced: true
|
|
1346
|
+
};
|
|
1347
|
+
function summarizeShards(raw) {
|
|
1348
|
+
if (raw === null || raw === void 0 || typeof raw !== "object") {
|
|
1349
|
+
return EMPTY_SUMMARY;
|
|
1350
|
+
}
|
|
1351
|
+
const obj = raw;
|
|
1352
|
+
const version = typeof obj.shard_array_version === "number" ? obj.shard_array_version : void 0;
|
|
1353
|
+
const shardMap = obj.shard_map;
|
|
1354
|
+
if (shardMap === null || shardMap === void 0 || typeof shardMap !== "object" || Array.isArray(shardMap)) {
|
|
1355
|
+
return { ...EMPTY_SUMMARY, shard_array_version: version };
|
|
1356
|
+
}
|
|
1357
|
+
const entries = Object.values(shardMap);
|
|
1358
|
+
if (entries.length === 0) {
|
|
1359
|
+
return { ...EMPTY_SUMMARY, shard_array_version: version };
|
|
1360
|
+
}
|
|
1361
|
+
const counts = entries.reduce((acc, rank) => {
|
|
1362
|
+
const map = new Map(acc);
|
|
1363
|
+
map.set(rank, (acc.get(rank) ?? 0) + 1);
|
|
1364
|
+
return map;
|
|
1365
|
+
}, /* @__PURE__ */ new Map());
|
|
1366
|
+
const totalShards = entries.length;
|
|
1367
|
+
const distribution = [...counts.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([rank, count]) => ({
|
|
1368
|
+
rank,
|
|
1369
|
+
shard_count: count,
|
|
1370
|
+
percent: `${(count / totalShards * 100).toFixed(1)}%`
|
|
1371
|
+
}));
|
|
1372
|
+
const shardCounts = distribution.map((d) => d.shard_count);
|
|
1373
|
+
const balanced = Math.max(...shardCounts) - Math.min(...shardCounts) <= 1;
|
|
1374
|
+
return {
|
|
1375
|
+
shard_array_version: version,
|
|
1376
|
+
total_shards: totalShards,
|
|
1377
|
+
rank_count: counts.size,
|
|
1378
|
+
distribution,
|
|
1379
|
+
balanced
|
|
1380
|
+
};
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
// src/tools/rest/system-properties.ts
|
|
1384
|
+
var import_zod = require("zod");
|
|
1385
|
+
var GetSystemPropertiesSchema = import_zod.z.object({
|
|
1386
|
+
category: import_zod.z.string().optional(),
|
|
1387
|
+
key_pattern: import_zod.z.string().optional()
|
|
1388
|
+
});
|
|
1389
|
+
async function getSystemProperties(session2, input5) {
|
|
1390
|
+
try {
|
|
1391
|
+
const response = await session2.makeRequest("/show/system/properties", {
|
|
1392
|
+
options: {}
|
|
1393
|
+
});
|
|
1394
|
+
if (!response.ok) {
|
|
1395
|
+
const raw2 = await response.text();
|
|
1396
|
+
return {
|
|
1397
|
+
ok: false,
|
|
1398
|
+
status: response.status,
|
|
1399
|
+
error: `HTTP ${response.status}`,
|
|
1400
|
+
raw: raw2
|
|
1401
|
+
};
|
|
1402
|
+
}
|
|
1403
|
+
const raw = await response.text();
|
|
1404
|
+
let parsed;
|
|
1405
|
+
try {
|
|
1406
|
+
parsed = JSON.parse(raw);
|
|
1407
|
+
} catch (parseError) {
|
|
1408
|
+
const message = parseError instanceof Error ? parseError.message : String(parseError);
|
|
1409
|
+
return {
|
|
1410
|
+
ok: false,
|
|
1411
|
+
status: 200,
|
|
1412
|
+
error: `JSON parse error: ${message}`,
|
|
1413
|
+
raw
|
|
1414
|
+
};
|
|
1415
|
+
}
|
|
1416
|
+
const inner = parseDataStr(parsed.data_str, raw);
|
|
1417
|
+
if (!inner.ok) return inner;
|
|
1418
|
+
const propertyMap = inner.data?.property_map ?? {};
|
|
1419
|
+
const filteredMap = applyFilters(propertyMap, input5);
|
|
1420
|
+
return {
|
|
1421
|
+
ok: true,
|
|
1422
|
+
data: flatObjectToRows(filteredMap, "property", "value"),
|
|
1423
|
+
rowCount: Object.keys(filteredMap).length
|
|
1424
|
+
};
|
|
1425
|
+
} catch (error) {
|
|
1426
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1427
|
+
return {
|
|
1428
|
+
ok: false,
|
|
1429
|
+
status: 0,
|
|
1430
|
+
error: message,
|
|
1431
|
+
raw: ""
|
|
1432
|
+
};
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
function applyFilters(propertyMap, input5) {
|
|
1436
|
+
const { category, key_pattern } = input5;
|
|
1437
|
+
if (category === void 0 && key_pattern === void 0) {
|
|
1438
|
+
return propertyMap;
|
|
1439
|
+
}
|
|
1440
|
+
const patternLower = key_pattern?.toLowerCase();
|
|
1441
|
+
return Object.fromEntries(
|
|
1442
|
+
Object.entries(propertyMap).filter(([key]) => {
|
|
1443
|
+
const matchesCategory = category === void 0 || key.startsWith(category);
|
|
1444
|
+
const matchesPattern = patternLower === void 0 || key.toLowerCase().includes(patternLower);
|
|
1445
|
+
return matchesCategory && matchesPattern;
|
|
1446
|
+
})
|
|
1447
|
+
);
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
// src/tools/rest/discover-hm-port.ts
|
|
1451
|
+
var DEFAULT_HM_PORT2 = 9300;
|
|
1452
|
+
async function discoverHmPort(session2) {
|
|
1453
|
+
const result = await getSystemProperties(session2, { key_pattern: "hm_http_port" });
|
|
1454
|
+
if (!result.ok) return DEFAULT_HM_PORT2;
|
|
1455
|
+
const rows = result.data;
|
|
1456
|
+
const entry = rows.find((r) => r.property?.includes("hm_http_port"));
|
|
1457
|
+
if (!entry?.value) return DEFAULT_HM_PORT2;
|
|
1458
|
+
const port = parseInt(entry.value, 10);
|
|
1459
|
+
return Number.isFinite(port) ? port : DEFAULT_HM_PORT2;
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
// src/tools/rest/cluster.ts
|
|
1463
|
+
async function fetchJson(session2, endpoint, body) {
|
|
1464
|
+
let response;
|
|
1465
|
+
let rawText;
|
|
1466
|
+
try {
|
|
1467
|
+
response = await session2.makeRequest(endpoint, body);
|
|
1468
|
+
rawText = await response.text();
|
|
1469
|
+
} catch (err) {
|
|
1470
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1471
|
+
return { ok: false, status: 0, error: message, raw: "" };
|
|
1472
|
+
}
|
|
1473
|
+
if (!response.ok) {
|
|
1474
|
+
return {
|
|
1475
|
+
ok: false,
|
|
1476
|
+
status: response.status,
|
|
1477
|
+
error: `HTTP ${response.status}`,
|
|
1478
|
+
raw: rawText
|
|
1479
|
+
};
|
|
1480
|
+
}
|
|
1481
|
+
try {
|
|
1482
|
+
const parsed = JSON.parse(rawText);
|
|
1483
|
+
return { ok: true, data: parsed };
|
|
1484
|
+
} catch (err) {
|
|
1485
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1486
|
+
return {
|
|
1487
|
+
ok: false,
|
|
1488
|
+
status: 200,
|
|
1489
|
+
error: `JSON parse error: ${message}`,
|
|
1490
|
+
raw: rawText
|
|
1491
|
+
};
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
async function fetchJsonOnPort(session2, port, endpoint, body) {
|
|
1495
|
+
if (!session2.makeRequestToPort) {
|
|
1496
|
+
return {
|
|
1497
|
+
ok: false,
|
|
1498
|
+
status: 0,
|
|
1499
|
+
error: "makeRequestToPort not available on this session",
|
|
1500
|
+
raw: ""
|
|
1501
|
+
};
|
|
1502
|
+
}
|
|
1503
|
+
let response;
|
|
1504
|
+
let rawText;
|
|
1505
|
+
try {
|
|
1506
|
+
response = await session2.makeRequestToPort(port, endpoint, body);
|
|
1507
|
+
rawText = await response.text();
|
|
1508
|
+
} catch (err) {
|
|
1509
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1510
|
+
return { ok: false, status: 0, error: message, raw: "" };
|
|
1511
|
+
}
|
|
1512
|
+
if (!response.ok) {
|
|
1513
|
+
return {
|
|
1514
|
+
ok: false,
|
|
1515
|
+
status: response.status,
|
|
1516
|
+
error: `HTTP ${response.status}`,
|
|
1517
|
+
raw: rawText
|
|
1518
|
+
};
|
|
1519
|
+
}
|
|
1520
|
+
try {
|
|
1521
|
+
const parsed = JSON.parse(rawText);
|
|
1522
|
+
return { ok: true, data: parsed };
|
|
1523
|
+
} catch (err) {
|
|
1524
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1525
|
+
return {
|
|
1526
|
+
ok: false,
|
|
1527
|
+
status: 200,
|
|
1528
|
+
error: `JSON parse error: ${message}`,
|
|
1529
|
+
raw: rawText
|
|
1530
|
+
};
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
async function clusterStatus(session2) {
|
|
1534
|
+
const opsResult = await fetchJson(session2, "/admin/show/cluster/operations", { options: {} });
|
|
1535
|
+
if (!opsResult.ok) return opsResult;
|
|
1536
|
+
const shardsResult = await fetchJson(session2, "/admin/show/shards", {
|
|
1537
|
+
options: {}
|
|
1538
|
+
});
|
|
1539
|
+
if (!shardsResult.ok) return shardsResult;
|
|
1540
|
+
const hmPort = await discoverHmPort(session2);
|
|
1541
|
+
const alertsResult = await fetchJsonOnPort(session2, hmPort, "/admin/show/alerts", {
|
|
1542
|
+
num_alerts: 50,
|
|
1543
|
+
options: {}
|
|
1544
|
+
});
|
|
1545
|
+
const alertsAvailable = alertsResult.ok;
|
|
1546
|
+
const jobsResult = await fetchJson(session2, "/admin/show/jobs", {
|
|
1547
|
+
options: { show_async_jobs: "true", show_worker_info: "true" }
|
|
1548
|
+
});
|
|
1549
|
+
if (!jobsResult.ok) return jobsResult;
|
|
1550
|
+
let alerts = [];
|
|
1551
|
+
if (alertsAvailable) {
|
|
1552
|
+
const alertsOuter = alertsResult.data;
|
|
1553
|
+
const alertsInner = parseDataStr(
|
|
1554
|
+
alertsOuter.data_str,
|
|
1555
|
+
JSON.stringify(alertsResult.data)
|
|
1556
|
+
);
|
|
1557
|
+
if (!alertsInner.ok) return alertsInner;
|
|
1558
|
+
const timestamps = alertsInner.data?.timestamps ?? [];
|
|
1559
|
+
const alertTypes = alertsInner.data?.types ?? [];
|
|
1560
|
+
const alertParams = alertsInner.data?.params ?? [];
|
|
1561
|
+
alerts = timestamps.map((ts, i) => ({
|
|
1562
|
+
timestamp: ts,
|
|
1563
|
+
type: alertTypes[i] ?? "",
|
|
1564
|
+
params: alertParams[i] ?? ""
|
|
1565
|
+
}));
|
|
1566
|
+
}
|
|
1567
|
+
const jobsOuter = jobsResult.data;
|
|
1568
|
+
const jobsInner = parseDataStr(jobsOuter.data_str, JSON.stringify(jobsResult.data));
|
|
1569
|
+
if (!jobsInner.ok) return jobsInner;
|
|
1570
|
+
const jobIds = jobsInner.data?.job_id ?? [];
|
|
1571
|
+
const jobStatuses = jobsInner.data?.status ?? [];
|
|
1572
|
+
const jobEndpoints = jobsInner.data?.endpoint_name ?? [];
|
|
1573
|
+
const jobs = jobIds.map((id, i) => ({
|
|
1574
|
+
job_id: id,
|
|
1575
|
+
status: jobStatuses[i] ?? "",
|
|
1576
|
+
endpoint: jobEndpoints[i] ?? ""
|
|
1577
|
+
}));
|
|
1578
|
+
return {
|
|
1579
|
+
ok: true,
|
|
1580
|
+
data: {
|
|
1581
|
+
operations: opsResult.data,
|
|
1582
|
+
shards: summarizeShards(shardsResult.data),
|
|
1583
|
+
alerts,
|
|
1584
|
+
jobs
|
|
1585
|
+
},
|
|
1586
|
+
...alertsAvailable ? {} : { note: "/admin/show/alerts not available on this version \u2014 alerts array is empty" }
|
|
1587
|
+
};
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
// src/tools/rest/node.ts
|
|
1591
|
+
async function nodeDetails(session2, nodeId) {
|
|
1592
|
+
let response;
|
|
1593
|
+
let rawText;
|
|
1594
|
+
try {
|
|
1595
|
+
response = await session2.makeRequest("/show/resource/statistics", {
|
|
1596
|
+
options: {}
|
|
1597
|
+
});
|
|
1598
|
+
rawText = await response.text();
|
|
1599
|
+
} catch (err) {
|
|
1600
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1601
|
+
return { ok: false, status: 0, error: message, raw: "" };
|
|
1602
|
+
}
|
|
1603
|
+
if (!response.ok) {
|
|
1604
|
+
return {
|
|
1605
|
+
ok: false,
|
|
1606
|
+
status: response.status,
|
|
1607
|
+
error: `HTTP ${response.status}`,
|
|
1608
|
+
raw: rawText
|
|
1609
|
+
};
|
|
1610
|
+
}
|
|
1611
|
+
let parsed;
|
|
1612
|
+
try {
|
|
1613
|
+
parsed = JSON.parse(rawText);
|
|
1614
|
+
} catch (err) {
|
|
1615
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1616
|
+
return {
|
|
1617
|
+
ok: false,
|
|
1618
|
+
status: 200,
|
|
1619
|
+
error: `JSON parse error: ${message}`,
|
|
1620
|
+
raw: rawText
|
|
1621
|
+
};
|
|
1622
|
+
}
|
|
1623
|
+
const inner = parseDataStr(
|
|
1624
|
+
parsed.data_str,
|
|
1625
|
+
rawText
|
|
1626
|
+
);
|
|
1627
|
+
if (!inner.ok) return inner;
|
|
1628
|
+
const rawMap = inner.data?.statistics_map ?? {};
|
|
1629
|
+
const decoded = decodeNestedJsonStrings(rawMap);
|
|
1630
|
+
const ranks = typeof decoded.ranks === "object" && decoded.ranks !== null ? decoded.ranks : decoded;
|
|
1631
|
+
if (nodeId !== void 0) {
|
|
1632
|
+
if (Object.prototype.hasOwnProperty.call(ranks, nodeId)) {
|
|
1633
|
+
return {
|
|
1634
|
+
ok: true,
|
|
1635
|
+
data: flattenRankDetail(ranks[nodeId])
|
|
1636
|
+
};
|
|
1637
|
+
}
|
|
1638
|
+
return {
|
|
1639
|
+
ok: true,
|
|
1640
|
+
data: flattenRanksSummary(decoded),
|
|
1641
|
+
note: `node_id '${nodeId}' not found in statistics_map \u2014 returning all nodes`
|
|
1642
|
+
};
|
|
1643
|
+
}
|
|
1644
|
+
return { ok: true, data: flattenRanksSummary(decoded) };
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
// src/tools/rest/logs.ts
|
|
1648
|
+
var import_zod2 = require("zod");
|
|
1649
|
+
var LOG_SOURCES = ["kinetica", "rank", "syslog", "gadmin", "reveal", "workbench"];
|
|
1650
|
+
var LOG_SEVERITIES = ["DEBUG", "INFO", "WARN", "ERROR", "FATAL"];
|
|
1651
|
+
var GetLogsSchema = import_zod2.z.object({
|
|
1652
|
+
source: import_zod2.z.enum(LOG_SOURCES),
|
|
1653
|
+
min_severity: import_zod2.z.enum(LOG_SEVERITIES).default("INFO"),
|
|
1654
|
+
duration: import_zod2.z.string().regex(
|
|
1655
|
+
/^\d+[mhd]$/,
|
|
1656
|
+
"Duration must match pattern: digits followed by m, h, or d (e.g. '1h', '30m', '7d')"
|
|
1657
|
+
).optional(),
|
|
1658
|
+
start_time: import_zod2.z.string().datetime().optional(),
|
|
1659
|
+
end_time: import_zod2.z.string().datetime().optional(),
|
|
1660
|
+
node_id: import_zod2.z.string().optional(),
|
|
1661
|
+
limit: import_zod2.z.number().int().min(1).max(5e3).default(500)
|
|
1662
|
+
}).refine(
|
|
1663
|
+
(data) => {
|
|
1664
|
+
const hasDuration = data.duration !== void 0;
|
|
1665
|
+
const hasAbsoluteTime = data.start_time !== void 0 || data.end_time !== void 0;
|
|
1666
|
+
return !(hasDuration && hasAbsoluteTime);
|
|
1667
|
+
},
|
|
1668
|
+
{
|
|
1669
|
+
message: "duration and start_time/end_time are mutually exclusive -- use one or the other, not both"
|
|
1670
|
+
}
|
|
1671
|
+
);
|
|
1672
|
+
function buildStubResponse(input5) {
|
|
1673
|
+
return {
|
|
1674
|
+
ok: true,
|
|
1675
|
+
data: {
|
|
1676
|
+
note: "The /admin/show/logs endpoint is not yet implemented on this Kinetica server. Use kinetica_execute_sql to query ki_catalog system tables for log-like diagnostic data.",
|
|
1677
|
+
endpoint: "/admin/show/logs",
|
|
1678
|
+
status: "stub",
|
|
1679
|
+
requested_params: {
|
|
1680
|
+
source: input5.source,
|
|
1681
|
+
min_severity: input5.min_severity,
|
|
1682
|
+
duration: input5.duration,
|
|
1683
|
+
start_time: input5.start_time,
|
|
1684
|
+
end_time: input5.end_time,
|
|
1685
|
+
node_id: input5.node_id,
|
|
1686
|
+
limit: input5.limit
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
};
|
|
1690
|
+
}
|
|
1691
|
+
async function getLogs(session2, input5) {
|
|
1692
|
+
const params = {
|
|
1693
|
+
source: input5.source,
|
|
1694
|
+
severity: input5.min_severity,
|
|
1695
|
+
limit: input5.limit
|
|
1696
|
+
};
|
|
1697
|
+
if (input5.duration !== void 0) {
|
|
1698
|
+
params.duration = input5.duration;
|
|
1699
|
+
}
|
|
1700
|
+
if (input5.start_time !== void 0) {
|
|
1701
|
+
params.start_time = input5.start_time;
|
|
1702
|
+
}
|
|
1703
|
+
if (input5.end_time !== void 0) {
|
|
1704
|
+
params.end_time = input5.end_time;
|
|
1705
|
+
}
|
|
1706
|
+
if (input5.node_id !== void 0) {
|
|
1707
|
+
params.node_id = input5.node_id;
|
|
1708
|
+
}
|
|
1709
|
+
try {
|
|
1710
|
+
const response = await session2.makeRequest("/admin/show/logs", params);
|
|
1711
|
+
if (!response.ok) {
|
|
1712
|
+
return buildStubResponse(input5);
|
|
1713
|
+
}
|
|
1714
|
+
const raw = await response.text();
|
|
1715
|
+
try {
|
|
1716
|
+
const data = JSON.parse(raw);
|
|
1717
|
+
return {
|
|
1718
|
+
ok: true,
|
|
1719
|
+
data
|
|
1720
|
+
};
|
|
1721
|
+
} catch {
|
|
1722
|
+
return buildStubResponse(input5);
|
|
1723
|
+
}
|
|
1724
|
+
} catch {
|
|
1725
|
+
return buildStubResponse(input5);
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
// src/tools/rest/show-configuration.ts
|
|
1730
|
+
var import_zod3 = require("zod");
|
|
1731
|
+
|
|
1732
|
+
// src/report/scrub.ts
|
|
1733
|
+
var CONFIG_SECRET_PATTERN = /([^\r\n=:]*(?:password|passwd|passphrase|license[_-]?key|private[_-]?key|secret)[^\r\n=:]*[:=][ \t]*)[^\r\n]+/gi;
|
|
1734
|
+
function redactConfigSecrets(content) {
|
|
1735
|
+
return content.replace(CONFIG_SECRET_PATTERN, "$1[REDACTED]");
|
|
1736
|
+
}
|
|
1737
|
+
var DEFAULT_SCRUB_PATTERNS = [
|
|
1738
|
+
/https?:\/\/[^\s"'`)\]]+/gi,
|
|
1739
|
+
// HTTP/HTTPS URLs
|
|
1740
|
+
/Basic\s+[A-Za-z0-9+/=]+/gi,
|
|
1741
|
+
// Basic auth headers
|
|
1742
|
+
/Bearer\s+[A-Za-z0-9._-]+/gi,
|
|
1743
|
+
// Bearer tokens
|
|
1744
|
+
/password[:\s]+[^\s"'`)\]]+/gi,
|
|
1745
|
+
// Password values (bare form: password: value)
|
|
1746
|
+
/"password"\s*:\s*"[^"]*"/gi,
|
|
1747
|
+
// JSON form: "password":"..."
|
|
1748
|
+
/(api[_-]?key|access[_-]?token|secret)["']?\s*[:=]\s*['"]?[^\s"'`)\]&,;]+/gi,
|
|
1749
|
+
// api_key / access_token / secret
|
|
1750
|
+
/(set-)?cookie\s*:\s*[^\r\n]+/gi,
|
|
1751
|
+
// Cookie / Set-Cookie headers
|
|
1752
|
+
/Authorization[:\s]+[^\s"'`)\]]+/gi
|
|
1753
|
+
// Authorization header values
|
|
1754
|
+
];
|
|
1755
|
+
function scrubCredentials(content, patterns = DEFAULT_SCRUB_PATTERNS) {
|
|
1756
|
+
const configRedacted = redactConfigSecrets(content);
|
|
1757
|
+
return patterns.reduce((text, pattern) => text.replace(pattern, "[REDACTED]"), configRedacted);
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
// src/tools/rest/show-configuration.ts
|
|
1761
|
+
var ShowConfigurationSchema = import_zod3.z.object({});
|
|
1762
|
+
async function showConfiguration(session2, _input) {
|
|
1763
|
+
if (!session2.makeRequestToPort) {
|
|
1764
|
+
return {
|
|
1765
|
+
ok: false,
|
|
1766
|
+
status: 0,
|
|
1767
|
+
error: "makeRequestToPort not available on this session",
|
|
1768
|
+
raw: ""
|
|
1769
|
+
};
|
|
1770
|
+
}
|
|
1771
|
+
const hmPort = await discoverHmPort(session2);
|
|
1772
|
+
let response;
|
|
1773
|
+
let rawText;
|
|
1774
|
+
try {
|
|
1775
|
+
response = await session2.makeRequestToPort(hmPort, "/admin/show/configuration", {});
|
|
1776
|
+
rawText = await response.text();
|
|
1777
|
+
} catch (err) {
|
|
1778
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1779
|
+
return { ok: false, status: 0, error: message, raw: "" };
|
|
1780
|
+
}
|
|
1781
|
+
if (!response.ok) {
|
|
1782
|
+
return {
|
|
1783
|
+
ok: false,
|
|
1784
|
+
status: response.status,
|
|
1785
|
+
error: `HTTP ${response.status}`,
|
|
1786
|
+
raw: rawText
|
|
1787
|
+
};
|
|
1788
|
+
}
|
|
1789
|
+
let outer;
|
|
1790
|
+
try {
|
|
1791
|
+
outer = JSON.parse(rawText);
|
|
1792
|
+
} catch (err) {
|
|
1793
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1794
|
+
return {
|
|
1795
|
+
ok: false,
|
|
1796
|
+
status: 200,
|
|
1797
|
+
error: `JSON parse error: ${message}`,
|
|
1798
|
+
raw: rawText
|
|
1799
|
+
};
|
|
1800
|
+
}
|
|
1801
|
+
const inner = parseDataStr(outer.data_str, rawText);
|
|
1802
|
+
if (!inner.ok) return inner;
|
|
1803
|
+
return {
|
|
1804
|
+
ok: true,
|
|
1805
|
+
data: {
|
|
1806
|
+
config_string: redactConfigSecrets(inner.data?.config_string ?? ""),
|
|
1807
|
+
info: inner.data?.info ?? {}
|
|
1808
|
+
}
|
|
1809
|
+
};
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
// src/tools/rest/system-timing.ts
|
|
1813
|
+
async function systemTiming(session2) {
|
|
1814
|
+
let response;
|
|
1815
|
+
let rawText;
|
|
1816
|
+
try {
|
|
1817
|
+
response = await session2.makeRequest("/show/system/timing", { options: {} });
|
|
1818
|
+
rawText = await response.text();
|
|
1819
|
+
} catch (err) {
|
|
1820
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1821
|
+
return {
|
|
1822
|
+
ok: false,
|
|
1823
|
+
status: 0,
|
|
1824
|
+
error: message,
|
|
1825
|
+
raw: ""
|
|
1826
|
+
};
|
|
1827
|
+
}
|
|
1828
|
+
if (!response.ok) {
|
|
1829
|
+
return {
|
|
1830
|
+
ok: false,
|
|
1831
|
+
status: response.status,
|
|
1832
|
+
error: `HTTP ${response.status}`,
|
|
1833
|
+
raw: rawText
|
|
1834
|
+
};
|
|
1835
|
+
}
|
|
1836
|
+
let parsed;
|
|
1837
|
+
try {
|
|
1838
|
+
parsed = JSON.parse(rawText);
|
|
1839
|
+
} catch (err) {
|
|
1840
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1841
|
+
return {
|
|
1842
|
+
ok: false,
|
|
1843
|
+
status: 200,
|
|
1844
|
+
error: `JSON parse error: ${message}`,
|
|
1845
|
+
raw: rawText
|
|
1846
|
+
};
|
|
1847
|
+
}
|
|
1848
|
+
const inner = parseDataStr(parsed.data_str, rawText);
|
|
1849
|
+
if (!inner.ok) return inner;
|
|
1850
|
+
const endpoints = inner.data?.endpoints ?? [];
|
|
1851
|
+
const timings = inner.data?.time_in_ms ?? [];
|
|
1852
|
+
const jobIds = inner.data?.jobIds ?? [];
|
|
1853
|
+
const data = endpoints.map((endpoint, i) => ({
|
|
1854
|
+
endpoint,
|
|
1855
|
+
time_in_ms: timings[i] ?? 0,
|
|
1856
|
+
job_id: jobIds[i] ?? ""
|
|
1857
|
+
}));
|
|
1858
|
+
return {
|
|
1859
|
+
ok: true,
|
|
1860
|
+
data
|
|
1861
|
+
};
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1864
|
+
// src/tools/rest/resource-groups.ts
|
|
1865
|
+
var import_zod4 = require("zod");
|
|
1866
|
+
var ResourceGroupsSchema = import_zod4.z.object({
|
|
1867
|
+
names: import_zod4.z.array(import_zod4.z.string()).optional().default([""]),
|
|
1868
|
+
show_tier_usage: import_zod4.z.boolean().optional()
|
|
1869
|
+
});
|
|
1870
|
+
async function getResourceGroups(session2, input5) {
|
|
1871
|
+
try {
|
|
1872
|
+
const response = await session2.makeRequest("/show/resourcegroups", {
|
|
1873
|
+
names: input5.names,
|
|
1874
|
+
options: {
|
|
1875
|
+
show_tier_usage: String(input5.show_tier_usage ?? false),
|
|
1876
|
+
show_default_values: "true",
|
|
1877
|
+
show_default_group: "true"
|
|
1878
|
+
}
|
|
1879
|
+
});
|
|
1880
|
+
if (!response.ok) {
|
|
1881
|
+
const raw2 = await response.text();
|
|
1882
|
+
return {
|
|
1883
|
+
ok: false,
|
|
1884
|
+
status: response.status,
|
|
1885
|
+
error: `HTTP ${response.status}`,
|
|
1886
|
+
raw: raw2
|
|
1887
|
+
};
|
|
1888
|
+
}
|
|
1889
|
+
const raw = await response.text();
|
|
1890
|
+
let parsed;
|
|
1891
|
+
try {
|
|
1892
|
+
parsed = JSON.parse(raw);
|
|
1893
|
+
} catch (parseError) {
|
|
1894
|
+
const message = parseError instanceof Error ? parseError.message : String(parseError);
|
|
1895
|
+
return {
|
|
1896
|
+
ok: false,
|
|
1897
|
+
status: 200,
|
|
1898
|
+
error: `JSON parse error: ${message}`,
|
|
1899
|
+
raw
|
|
1900
|
+
};
|
|
1901
|
+
}
|
|
1902
|
+
const inner = parseDataStr(parsed.data_str, raw);
|
|
1903
|
+
if (!inner.ok) return inner;
|
|
1904
|
+
return {
|
|
1905
|
+
ok: true,
|
|
1906
|
+
data: inner.data ?? {}
|
|
1907
|
+
};
|
|
1908
|
+
} catch (error) {
|
|
1909
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1910
|
+
return {
|
|
1911
|
+
ok: false,
|
|
1912
|
+
status: 0,
|
|
1913
|
+
error: message,
|
|
1914
|
+
raw: ""
|
|
1915
|
+
};
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1919
|
+
// src/tools/rest/verify-db.ts
|
|
1920
|
+
var import_zod5 = require("zod");
|
|
1921
|
+
var VerifyDbSchema = import_zod5.z.object({
|
|
1922
|
+
verify_nulls: import_zod5.z.boolean().optional(),
|
|
1923
|
+
verify_persist: import_zod5.z.boolean().optional(),
|
|
1924
|
+
verify_rank0: import_zod5.z.boolean().optional()
|
|
1925
|
+
});
|
|
1926
|
+
async function verifyDb(session2, input5) {
|
|
1927
|
+
const options = {
|
|
1928
|
+
concurrent_safe: "true"
|
|
1929
|
+
};
|
|
1930
|
+
if (input5.verify_nulls !== void 0) {
|
|
1931
|
+
options.verify_nulls = String(input5.verify_nulls);
|
|
1932
|
+
}
|
|
1933
|
+
if (input5.verify_persist !== void 0) {
|
|
1934
|
+
options.verify_persist = String(input5.verify_persist);
|
|
1935
|
+
}
|
|
1936
|
+
if (input5.verify_rank0 !== void 0) {
|
|
1937
|
+
options.verify_rank0 = String(input5.verify_rank0);
|
|
1938
|
+
}
|
|
1939
|
+
try {
|
|
1940
|
+
const response = await session2.makeRequest("/admin/verifydb", { options });
|
|
1941
|
+
if (!response.ok) {
|
|
1942
|
+
const raw2 = await response.text();
|
|
1943
|
+
return {
|
|
1944
|
+
ok: false,
|
|
1945
|
+
status: response.status,
|
|
1946
|
+
error: `HTTP ${response.status}`,
|
|
1947
|
+
raw: raw2
|
|
1948
|
+
};
|
|
1949
|
+
}
|
|
1950
|
+
const raw = await response.text();
|
|
1951
|
+
let parsed;
|
|
1952
|
+
try {
|
|
1953
|
+
parsed = JSON.parse(raw);
|
|
1954
|
+
} catch (parseError) {
|
|
1955
|
+
const message = parseError instanceof Error ? parseError.message : String(parseError);
|
|
1956
|
+
return {
|
|
1957
|
+
ok: false,
|
|
1958
|
+
status: 200,
|
|
1959
|
+
error: `JSON parse error: ${message}`,
|
|
1960
|
+
raw
|
|
1961
|
+
};
|
|
1962
|
+
}
|
|
1963
|
+
const inner = parseDataStr(parsed.data_str, raw);
|
|
1964
|
+
if (!inner.ok) return inner;
|
|
1965
|
+
const data = {
|
|
1966
|
+
verified_ok: inner.data?.verified_ok ?? false,
|
|
1967
|
+
error_list: inner.data?.error_list ?? [],
|
|
1968
|
+
orphaned_tables_total_size: inner.data?.orphaned_tables_total_size ?? 0
|
|
1969
|
+
};
|
|
1970
|
+
return {
|
|
1971
|
+
ok: true,
|
|
1972
|
+
data
|
|
1973
|
+
};
|
|
1974
|
+
} catch (error) {
|
|
1975
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1976
|
+
return {
|
|
1977
|
+
ok: false,
|
|
1978
|
+
status: 0,
|
|
1979
|
+
error: message,
|
|
1980
|
+
raw: ""
|
|
1981
|
+
};
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1985
|
+
// src/tools/rest/security.ts
|
|
1986
|
+
var import_zod6 = require("zod");
|
|
1987
|
+
var ShowSecuritySchema = import_zod6.z.object({
|
|
1988
|
+
names: import_zod6.z.array(import_zod6.z.string()).optional().default([""])
|
|
1989
|
+
});
|
|
1990
|
+
async function showSecurity(session2, input5) {
|
|
1991
|
+
try {
|
|
1992
|
+
const response = await session2.makeRequest("/show/security", {
|
|
1993
|
+
names: input5.names,
|
|
1994
|
+
options: {}
|
|
1995
|
+
});
|
|
1996
|
+
if (!response.ok) {
|
|
1997
|
+
const raw2 = await response.text();
|
|
1998
|
+
return {
|
|
1999
|
+
ok: false,
|
|
2000
|
+
status: response.status,
|
|
2001
|
+
error: `HTTP ${response.status}`,
|
|
2002
|
+
raw: raw2
|
|
2003
|
+
};
|
|
2004
|
+
}
|
|
2005
|
+
const raw = await response.text();
|
|
2006
|
+
let parsed;
|
|
2007
|
+
try {
|
|
2008
|
+
parsed = JSON.parse(raw);
|
|
2009
|
+
} catch (parseError) {
|
|
2010
|
+
const message = parseError instanceof Error ? parseError.message : String(parseError);
|
|
2011
|
+
return {
|
|
2012
|
+
ok: false,
|
|
2013
|
+
status: 200,
|
|
2014
|
+
error: `JSON parse error: ${message}`,
|
|
2015
|
+
raw
|
|
2016
|
+
};
|
|
2017
|
+
}
|
|
2018
|
+
const inner = parseDataStr(parsed.data_str, raw);
|
|
2019
|
+
if (!inner.ok) return inner;
|
|
2020
|
+
return {
|
|
2021
|
+
ok: true,
|
|
2022
|
+
data: inner.data ?? {}
|
|
2023
|
+
};
|
|
2024
|
+
} catch (error) {
|
|
2025
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2026
|
+
return {
|
|
2027
|
+
ok: false,
|
|
2028
|
+
status: 0,
|
|
2029
|
+
error: message,
|
|
2030
|
+
raw: ""
|
|
2031
|
+
};
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
2034
|
+
|
|
2035
|
+
// src/tools/rest/show-table.ts
|
|
2036
|
+
var import_zod7 = require("zod");
|
|
2037
|
+
|
|
2038
|
+
// src/tools/rest/parse-type-schema.ts
|
|
2039
|
+
function resolveUnionType(union) {
|
|
2040
|
+
for (const entry of union) {
|
|
2041
|
+
if (typeof entry === "string" && entry !== "null") {
|
|
2042
|
+
return entry;
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
return "null";
|
|
2046
|
+
}
|
|
2047
|
+
function resolveFieldType(fieldType) {
|
|
2048
|
+
if (typeof fieldType === "string") {
|
|
2049
|
+
return fieldType;
|
|
2050
|
+
}
|
|
2051
|
+
if (Array.isArray(fieldType)) {
|
|
2052
|
+
return resolveUnionType(fieldType);
|
|
2053
|
+
}
|
|
2054
|
+
return void 0;
|
|
2055
|
+
}
|
|
2056
|
+
function parseTypeSchema(schemaJson) {
|
|
2057
|
+
let parsed;
|
|
2058
|
+
try {
|
|
2059
|
+
parsed = JSON.parse(schemaJson);
|
|
2060
|
+
} catch {
|
|
2061
|
+
return [];
|
|
2062
|
+
}
|
|
2063
|
+
if (typeof parsed !== "object" || parsed === null || !("type" in parsed) || !("fields" in parsed)) {
|
|
2064
|
+
return [];
|
|
2065
|
+
}
|
|
2066
|
+
const record = parsed;
|
|
2067
|
+
if (record.type !== "record" || !Array.isArray(record.fields)) {
|
|
2068
|
+
return [];
|
|
2069
|
+
}
|
|
2070
|
+
const columns = [];
|
|
2071
|
+
for (const field of record.fields) {
|
|
2072
|
+
if (typeof field !== "object" || field === null) continue;
|
|
2073
|
+
const f = field;
|
|
2074
|
+
if (typeof f.name !== "string" || f.type === void 0) continue;
|
|
2075
|
+
const resolvedType = resolveFieldType(f.type);
|
|
2076
|
+
if (resolvedType === void 0) continue;
|
|
2077
|
+
columns.push({ name: f.name, type: resolvedType });
|
|
2078
|
+
}
|
|
2079
|
+
return columns;
|
|
2080
|
+
}
|
|
2081
|
+
|
|
2082
|
+
// src/tools/rest/show-table.ts
|
|
2083
|
+
var ShowTableSchema = import_zod7.z.object({
|
|
2084
|
+
table_name: import_zod7.z.string().optional().default(""),
|
|
2085
|
+
get_sizes: import_zod7.z.boolean().optional(),
|
|
2086
|
+
get_access_data: import_zod7.z.boolean().optional(),
|
|
2087
|
+
get_column_info: import_zod7.z.boolean().optional()
|
|
2088
|
+
});
|
|
2089
|
+
function resolveColumnInfoOption(input5) {
|
|
2090
|
+
if (input5.get_column_info === true) return "true";
|
|
2091
|
+
if (input5.get_column_info === false) return "false";
|
|
2092
|
+
return input5.table_name !== "" ? "true" : "false";
|
|
2093
|
+
}
|
|
2094
|
+
function parseColumnProperties(propertiesJson) {
|
|
2095
|
+
try {
|
|
2096
|
+
const parsed = JSON.parse(propertiesJson);
|
|
2097
|
+
const result = /* @__PURE__ */ new Map();
|
|
2098
|
+
for (const [colName, props] of Object.entries(parsed)) {
|
|
2099
|
+
if (Array.isArray(props)) {
|
|
2100
|
+
result.set(colName, props.join(", "));
|
|
2101
|
+
}
|
|
2102
|
+
}
|
|
2103
|
+
return result;
|
|
2104
|
+
} catch {
|
|
2105
|
+
return /* @__PURE__ */ new Map();
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2108
|
+
function buildColumnEntries(typeSchemaJson, propertiesStr) {
|
|
2109
|
+
if (!typeSchemaJson) return [];
|
|
2110
|
+
const columnInfos = parseTypeSchema(typeSchemaJson);
|
|
2111
|
+
if (columnInfos.length === 0) return [];
|
|
2112
|
+
const propsMap = propertiesStr !== void 0 ? parseColumnProperties(propertiesStr) : /* @__PURE__ */ new Map();
|
|
2113
|
+
return columnInfos.map((col) => ({
|
|
2114
|
+
name: col.name,
|
|
2115
|
+
type: col.type,
|
|
2116
|
+
properties: propsMap.get(col.name) ?? ""
|
|
2117
|
+
}));
|
|
2118
|
+
}
|
|
2119
|
+
function escapeSqlString(value) {
|
|
2120
|
+
return value.replace(/'/g, "''");
|
|
2121
|
+
}
|
|
2122
|
+
function splitSchemaTable(tableName) {
|
|
2123
|
+
const dot = tableName.indexOf(".");
|
|
2124
|
+
if (dot === -1) return null;
|
|
2125
|
+
return { schema: tableName.slice(0, dot), table: tableName.slice(dot + 1) };
|
|
2126
|
+
}
|
|
2127
|
+
async function fetchIndexes(session2, tableName) {
|
|
2128
|
+
const parts = splitSchemaTable(tableName);
|
|
2129
|
+
if (!parts) return [];
|
|
2130
|
+
const schema = escapeSqlString(parts.schema);
|
|
2131
|
+
const table = escapeSqlString(parts.table);
|
|
2132
|
+
const statement = `SELECT index_type, index_columns FROM ki_catalog.ki_indexes WHERE schema_name = '${schema}' AND object_name = '${table}'`;
|
|
2133
|
+
try {
|
|
2134
|
+
const response = await session2.makeRequest("/execute/sql", {
|
|
2135
|
+
statement,
|
|
2136
|
+
offset: 0,
|
|
2137
|
+
limit: 100,
|
|
2138
|
+
encoding: "json",
|
|
2139
|
+
options: {}
|
|
2140
|
+
});
|
|
2141
|
+
if (!response.ok) return [];
|
|
2142
|
+
const rawText = await response.text();
|
|
2143
|
+
const outer = JSON.parse(rawText);
|
|
2144
|
+
if (outer.status === "ERROR") return [];
|
|
2145
|
+
const dataStr = JSON.parse(outer.data_str);
|
|
2146
|
+
if (dataStr.total_number_of_records === 0) return [];
|
|
2147
|
+
const cols = JSON.parse(dataStr.json_encoded_response);
|
|
2148
|
+
const types = cols.column_1 ?? [];
|
|
2149
|
+
const indexCols = cols.column_2 ?? [];
|
|
2150
|
+
return types.map((t, i) => ({
|
|
2151
|
+
index_type: t,
|
|
2152
|
+
index_columns: indexCols[i] ?? ""
|
|
2153
|
+
}));
|
|
2154
|
+
} catch {
|
|
2155
|
+
return [];
|
|
2156
|
+
}
|
|
2157
|
+
}
|
|
2158
|
+
async function showTable(session2, input5) {
|
|
2159
|
+
const options = {
|
|
2160
|
+
get_sizes: String(input5.get_sizes ?? true),
|
|
2161
|
+
show_children: "false",
|
|
2162
|
+
no_error_if_not_exists: "true",
|
|
2163
|
+
get_column_info: resolveColumnInfoOption(input5)
|
|
2164
|
+
};
|
|
2165
|
+
if (input5.get_access_data !== void 0) {
|
|
2166
|
+
options.get_access_data = String(input5.get_access_data);
|
|
2167
|
+
}
|
|
2168
|
+
try {
|
|
2169
|
+
const response = await session2.makeRequest("/show/table", {
|
|
2170
|
+
table_name: input5.table_name,
|
|
2171
|
+
options
|
|
2172
|
+
});
|
|
2173
|
+
if (!response.ok) {
|
|
2174
|
+
const raw2 = await response.text();
|
|
2175
|
+
return {
|
|
2176
|
+
ok: false,
|
|
2177
|
+
status: response.status,
|
|
2178
|
+
error: `HTTP ${response.status}`,
|
|
2179
|
+
raw: raw2
|
|
2180
|
+
};
|
|
2181
|
+
}
|
|
2182
|
+
const raw = await response.text();
|
|
2183
|
+
let outer;
|
|
2184
|
+
try {
|
|
2185
|
+
outer = JSON.parse(raw);
|
|
2186
|
+
} catch (parseError) {
|
|
2187
|
+
const message = parseError instanceof Error ? parseError.message : String(parseError);
|
|
2188
|
+
return {
|
|
2189
|
+
ok: false,
|
|
2190
|
+
status: 200,
|
|
2191
|
+
error: `JSON parse error: ${message}`,
|
|
2192
|
+
raw
|
|
2193
|
+
};
|
|
2194
|
+
}
|
|
2195
|
+
let inner = {};
|
|
2196
|
+
if (typeof outer.data_str === "string") {
|
|
2197
|
+
try {
|
|
2198
|
+
inner = JSON.parse(outer.data_str);
|
|
2199
|
+
} catch (parseError) {
|
|
2200
|
+
const message = parseError instanceof Error ? parseError.message : String(parseError);
|
|
2201
|
+
return {
|
|
2202
|
+
ok: false,
|
|
2203
|
+
status: 200,
|
|
2204
|
+
error: `data_str parse error: ${message}`,
|
|
2205
|
+
raw
|
|
2206
|
+
};
|
|
2207
|
+
}
|
|
2208
|
+
}
|
|
2209
|
+
const tableNames = inner.table_names ?? [];
|
|
2210
|
+
const descriptions = inner.table_descriptions ?? [];
|
|
2211
|
+
const sizes = inner.sizes ?? [];
|
|
2212
|
+
const properties = inner.properties ?? [];
|
|
2213
|
+
const typeSchemas = inner.type_schemas;
|
|
2214
|
+
if (options.get_column_info === "true" && tableNames.length > 0) {
|
|
2215
|
+
const table = {
|
|
2216
|
+
table_name: tableNames[0],
|
|
2217
|
+
description: descriptions[0] ?? "",
|
|
2218
|
+
size: sizes[0] ?? "",
|
|
2219
|
+
properties: properties[0] ?? ""
|
|
2220
|
+
};
|
|
2221
|
+
const columns = buildColumnEntries(typeSchemas?.[0], typeSchemas ? properties[0] : void 0);
|
|
2222
|
+
const indexes = await fetchIndexes(session2, input5.table_name);
|
|
2223
|
+
return {
|
|
2224
|
+
ok: true,
|
|
2225
|
+
data: { table, columns, indexes }
|
|
2226
|
+
};
|
|
2227
|
+
}
|
|
2228
|
+
const data = tableNames.map((name, i) => ({
|
|
2229
|
+
table_name: name,
|
|
2230
|
+
description: descriptions[i] ?? "",
|
|
2231
|
+
size: sizes[i] ?? "",
|
|
2232
|
+
properties: properties[i] ?? ""
|
|
2233
|
+
}));
|
|
2234
|
+
return {
|
|
2235
|
+
ok: true,
|
|
2236
|
+
data
|
|
2237
|
+
};
|
|
2238
|
+
} catch (error) {
|
|
2239
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2240
|
+
return {
|
|
2241
|
+
ok: false,
|
|
2242
|
+
status: 0,
|
|
2243
|
+
error: message,
|
|
2244
|
+
raw: ""
|
|
2245
|
+
};
|
|
2246
|
+
}
|
|
2247
|
+
}
|
|
2248
|
+
|
|
2249
|
+
// src/tools/rest/resource-objects.ts
|
|
2250
|
+
var import_zod8 = require("zod");
|
|
2251
|
+
var ResourceObjectsSchema = import_zod8.z.object({
|
|
2252
|
+
table_names: import_zod8.z.string().optional().default("*"),
|
|
2253
|
+
tiers: import_zod8.z.string().optional(),
|
|
2254
|
+
order_by: import_zod8.z.string().optional(),
|
|
2255
|
+
limit: import_zod8.z.number().int().min(1).max(1e4).optional().default(100)
|
|
2256
|
+
});
|
|
2257
|
+
async function getResourceObjects(session2, input5) {
|
|
2258
|
+
const options = {
|
|
2259
|
+
table_names: input5.table_names ?? "*",
|
|
2260
|
+
limit: String(input5.limit ?? 100)
|
|
2261
|
+
};
|
|
2262
|
+
if (input5.tiers !== void 0) {
|
|
2263
|
+
options.tiers = input5.tiers;
|
|
2264
|
+
}
|
|
2265
|
+
if (input5.order_by !== void 0) {
|
|
2266
|
+
options.order_by = input5.order_by;
|
|
2267
|
+
}
|
|
2268
|
+
try {
|
|
2269
|
+
const response = await session2.makeRequest("/show/resource/objects", {
|
|
2270
|
+
options
|
|
2271
|
+
});
|
|
2272
|
+
if (!response.ok) {
|
|
2273
|
+
const raw2 = await response.text();
|
|
2274
|
+
return {
|
|
2275
|
+
ok: false,
|
|
2276
|
+
status: response.status,
|
|
2277
|
+
error: `HTTP ${response.status}`,
|
|
2278
|
+
raw: raw2
|
|
2279
|
+
};
|
|
2280
|
+
}
|
|
2281
|
+
const raw = await response.text();
|
|
2282
|
+
let parsed;
|
|
2283
|
+
try {
|
|
2284
|
+
parsed = JSON.parse(raw);
|
|
2285
|
+
} catch (parseError) {
|
|
2286
|
+
const message = parseError instanceof Error ? parseError.message : String(parseError);
|
|
2287
|
+
return {
|
|
2288
|
+
ok: false,
|
|
2289
|
+
status: 200,
|
|
2290
|
+
error: `JSON parse error: ${message}`,
|
|
2291
|
+
raw
|
|
2292
|
+
};
|
|
2293
|
+
}
|
|
2294
|
+
const inner = parseDataStr(parsed.data_str, raw);
|
|
2295
|
+
if (!inner.ok) return inner;
|
|
2296
|
+
return {
|
|
2297
|
+
ok: true,
|
|
2298
|
+
data: inner.data ?? {}
|
|
2299
|
+
};
|
|
2300
|
+
} catch (error) {
|
|
2301
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2302
|
+
return {
|
|
2303
|
+
ok: false,
|
|
2304
|
+
status: 0,
|
|
2305
|
+
error: message,
|
|
2306
|
+
raw: ""
|
|
2307
|
+
};
|
|
2308
|
+
}
|
|
2309
|
+
}
|
|
2310
|
+
|
|
2311
|
+
// src/tools/rest/host-manager.ts
|
|
2312
|
+
async function hostManagerStatus(session2) {
|
|
2313
|
+
if (!session2.makeRequestToPort) {
|
|
2314
|
+
return {
|
|
2315
|
+
ok: false,
|
|
2316
|
+
status: 0,
|
|
2317
|
+
error: "makeRequestToPort not available on this session",
|
|
2318
|
+
raw: ""
|
|
2319
|
+
};
|
|
2320
|
+
}
|
|
2321
|
+
const hmPort = await discoverHmPort(session2);
|
|
2322
|
+
let response;
|
|
2323
|
+
let rawText;
|
|
2324
|
+
try {
|
|
2325
|
+
response = await session2.makeRequestToPort(hmPort, "/", void 0);
|
|
2326
|
+
rawText = await response.text();
|
|
2327
|
+
} catch (err) {
|
|
2328
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2329
|
+
return {
|
|
2330
|
+
ok: false,
|
|
2331
|
+
status: 0,
|
|
2332
|
+
error: message,
|
|
2333
|
+
raw: ""
|
|
2334
|
+
};
|
|
2335
|
+
}
|
|
2336
|
+
if (!response.ok) {
|
|
2337
|
+
return {
|
|
2338
|
+
ok: false,
|
|
2339
|
+
status: response.status,
|
|
2340
|
+
error: `HTTP ${response.status}`,
|
|
2341
|
+
raw: rawText
|
|
2342
|
+
};
|
|
2343
|
+
}
|
|
2344
|
+
let parsed;
|
|
2345
|
+
try {
|
|
2346
|
+
parsed = JSON.parse(rawText);
|
|
2347
|
+
} catch (err) {
|
|
2348
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2349
|
+
return {
|
|
2350
|
+
ok: false,
|
|
2351
|
+
status: 200,
|
|
2352
|
+
error: `JSON parse error: ${message}`,
|
|
2353
|
+
raw: rawText
|
|
2354
|
+
};
|
|
2355
|
+
}
|
|
2356
|
+
return {
|
|
2357
|
+
ok: true,
|
|
2358
|
+
data: flatObjectToRows(parsed, "key", "value")
|
|
2359
|
+
};
|
|
2360
|
+
}
|
|
2361
|
+
async function hostManagerAlerts(session2) {
|
|
2362
|
+
if (!session2.makeRequestToPort) {
|
|
2363
|
+
return {
|
|
2364
|
+
ok: false,
|
|
2365
|
+
status: 0,
|
|
2366
|
+
error: "makeRequestToPort not available on this session",
|
|
2367
|
+
raw: ""
|
|
2368
|
+
};
|
|
2369
|
+
}
|
|
2370
|
+
const hmPort = await discoverHmPort(session2);
|
|
2371
|
+
let response;
|
|
2372
|
+
let rawText;
|
|
2373
|
+
try {
|
|
2374
|
+
response = await session2.makeRequestToPort(hmPort, "/admin/show/alerts", {
|
|
2375
|
+
num_alerts: 50,
|
|
2376
|
+
options: {}
|
|
2377
|
+
});
|
|
2378
|
+
rawText = await response.text();
|
|
2379
|
+
} catch (err) {
|
|
2380
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2381
|
+
return { ok: false, status: 0, error: message, raw: "" };
|
|
2382
|
+
}
|
|
2383
|
+
if (!response.ok) {
|
|
2384
|
+
return {
|
|
2385
|
+
ok: false,
|
|
2386
|
+
status: response.status,
|
|
2387
|
+
error: `HTTP ${response.status}`,
|
|
2388
|
+
raw: rawText
|
|
2389
|
+
};
|
|
2390
|
+
}
|
|
2391
|
+
let outer;
|
|
2392
|
+
try {
|
|
2393
|
+
outer = JSON.parse(rawText);
|
|
2394
|
+
} catch (err) {
|
|
2395
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2396
|
+
return {
|
|
2397
|
+
ok: false,
|
|
2398
|
+
status: 200,
|
|
2399
|
+
error: `JSON parse error: ${message}`,
|
|
2400
|
+
raw: rawText
|
|
2401
|
+
};
|
|
2402
|
+
}
|
|
2403
|
+
const inner = parseDataStr(outer.data_str, rawText);
|
|
2404
|
+
if (!inner.ok) return inner;
|
|
2405
|
+
const timestamps = inner.data?.timestamps ?? [];
|
|
2406
|
+
const alertTypes = inner.data?.types ?? [];
|
|
2407
|
+
const alertParams = inner.data?.params ?? [];
|
|
2408
|
+
const alerts = timestamps.map((ts, i) => ({
|
|
2409
|
+
timestamp: ts,
|
|
2410
|
+
type: alertTypes[i] ?? "",
|
|
2411
|
+
params: alertParams[i] ?? ""
|
|
2412
|
+
}));
|
|
2413
|
+
return { ok: true, data: alerts };
|
|
2414
|
+
}
|
|
2415
|
+
|
|
2416
|
+
// src/tools/sql/execute.ts
|
|
2417
|
+
var import_zod9 = require("zod");
|
|
2418
|
+
var ExecuteSqlSchema = import_zod9.z.object({
|
|
2419
|
+
statement: import_zod9.z.string().min(1),
|
|
2420
|
+
limit: import_zod9.z.number().int().min(1).max(1e4).default(100)
|
|
2421
|
+
});
|
|
2422
|
+
var READ_ONLY_PREFIXES = /^\s*(SELECT|EXPLAIN|DESCRIBE|DESC)\b/i;
|
|
2423
|
+
var SQL_COMMENTS = /\/\*[\s\S]*?\*\/|--[^\n]*/g;
|
|
2424
|
+
function isCteReadOnly(stripped) {
|
|
2425
|
+
let depth = 0;
|
|
2426
|
+
let pastFirstParen = false;
|
|
2427
|
+
let finalStart = -1;
|
|
2428
|
+
for (let i = 0; i < stripped.length; i++) {
|
|
2429
|
+
const ch = stripped[i];
|
|
2430
|
+
if (ch === "(") {
|
|
2431
|
+
depth++;
|
|
2432
|
+
pastFirstParen = true;
|
|
2433
|
+
} else if (ch === ")") {
|
|
2434
|
+
depth--;
|
|
2435
|
+
if (depth === 0 && pastFirstParen) {
|
|
2436
|
+
finalStart = i + 1;
|
|
2437
|
+
}
|
|
2438
|
+
}
|
|
2439
|
+
}
|
|
2440
|
+
if (finalStart === -1) {
|
|
2441
|
+
return false;
|
|
2442
|
+
}
|
|
2443
|
+
const tail = stripped.slice(finalStart).trim();
|
|
2444
|
+
return READ_ONLY_PREFIXES.test(tail);
|
|
2445
|
+
}
|
|
2446
|
+
function isReadOnlySql(statement) {
|
|
2447
|
+
const stripped = statement.replace(SQL_COMMENTS, " ").trim();
|
|
2448
|
+
if (READ_ONLY_PREFIXES.test(stripped)) {
|
|
2449
|
+
return true;
|
|
2450
|
+
}
|
|
2451
|
+
if (/^\s*WITH\b/i.test(stripped)) {
|
|
2452
|
+
return isCteReadOnly(stripped);
|
|
2453
|
+
}
|
|
2454
|
+
return false;
|
|
2455
|
+
}
|
|
2456
|
+
async function executeSql(session2, statement, limit = 100) {
|
|
2457
|
+
if (!isReadOnlySql(statement)) {
|
|
2458
|
+
return {
|
|
2459
|
+
ok: false,
|
|
2460
|
+
status: 400,
|
|
2461
|
+
error: "SQL rejected: only SELECT, WITH, and EXPLAIN statements are permitted",
|
|
2462
|
+
raw: statement
|
|
2463
|
+
};
|
|
2464
|
+
}
|
|
2465
|
+
let response;
|
|
2466
|
+
let rawText;
|
|
2467
|
+
try {
|
|
2468
|
+
response = await session2.makeRequest("/execute/sql", {
|
|
2469
|
+
statement,
|
|
2470
|
+
offset: 0,
|
|
2471
|
+
limit,
|
|
2472
|
+
encoding: "json",
|
|
2473
|
+
options: {}
|
|
2474
|
+
});
|
|
2475
|
+
rawText = await response.text();
|
|
2476
|
+
} catch (err) {
|
|
2477
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2478
|
+
return { ok: false, status: 0, error: message, raw: "" };
|
|
2479
|
+
}
|
|
2480
|
+
if (!response.ok) {
|
|
2481
|
+
return {
|
|
2482
|
+
ok: false,
|
|
2483
|
+
status: response.status,
|
|
2484
|
+
error: `HTTP ${response.status}`,
|
|
2485
|
+
raw: rawText
|
|
2486
|
+
};
|
|
2487
|
+
}
|
|
2488
|
+
let outer;
|
|
2489
|
+
try {
|
|
2490
|
+
outer = JSON.parse(rawText);
|
|
2491
|
+
} catch (err) {
|
|
2492
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2493
|
+
return {
|
|
2494
|
+
ok: false,
|
|
2495
|
+
status: 200,
|
|
2496
|
+
error: `JSON parse error: ${message}`,
|
|
2497
|
+
raw: rawText
|
|
2498
|
+
};
|
|
2499
|
+
}
|
|
2500
|
+
if (outer.status === "ERROR") {
|
|
2501
|
+
return {
|
|
2502
|
+
ok: false,
|
|
2503
|
+
status: 400,
|
|
2504
|
+
error: outer.message,
|
|
2505
|
+
raw: rawText
|
|
2506
|
+
};
|
|
2507
|
+
}
|
|
2508
|
+
let dataStr;
|
|
2509
|
+
try {
|
|
2510
|
+
dataStr = JSON.parse(outer.data_str);
|
|
2511
|
+
} catch (err) {
|
|
2512
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2513
|
+
return {
|
|
2514
|
+
ok: false,
|
|
2515
|
+
status: 200,
|
|
2516
|
+
error: `data_str parse error: ${message}`,
|
|
2517
|
+
raw: rawText
|
|
2518
|
+
};
|
|
2519
|
+
}
|
|
2520
|
+
let rows;
|
|
2521
|
+
try {
|
|
2522
|
+
rows = JSON.parse(dataStr.json_encoded_response);
|
|
2523
|
+
} catch (err) {
|
|
2524
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2525
|
+
return {
|
|
2526
|
+
ok: false,
|
|
2527
|
+
status: 200,
|
|
2528
|
+
error: `JSON parse error: ${message}`,
|
|
2529
|
+
raw: rawText
|
|
2530
|
+
};
|
|
2531
|
+
}
|
|
2532
|
+
const totalRecords = dataStr.total_number_of_records;
|
|
2533
|
+
if (totalRecords === 0) {
|
|
2534
|
+
return {
|
|
2535
|
+
ok: true,
|
|
2536
|
+
data: [],
|
|
2537
|
+
rowCount: 0,
|
|
2538
|
+
note: "Query returned 0 rows"
|
|
2539
|
+
};
|
|
2540
|
+
}
|
|
2541
|
+
return {
|
|
2542
|
+
ok: true,
|
|
2543
|
+
data: rows,
|
|
2544
|
+
rowCount: totalRecords
|
|
2545
|
+
};
|
|
2546
|
+
}
|
|
2547
|
+
|
|
2548
|
+
// src/tools/sql/explain.ts
|
|
2549
|
+
var import_zod10 = require("zod");
|
|
2550
|
+
var ExplainQuerySchema = import_zod10.z.object({
|
|
2551
|
+
statement: import_zod10.z.string().min(1),
|
|
2552
|
+
limit: import_zod10.z.number().int().min(1).max(1e4).default(100)
|
|
2553
|
+
});
|
|
2554
|
+
async function explainQuery(session2, statement, limit = 100) {
|
|
2555
|
+
return executeSql(session2, "EXPLAIN " + statement.trimStart(), limit);
|
|
2556
|
+
}
|
|
2557
|
+
|
|
2558
|
+
// src/tools/sql/enrich-error.ts
|
|
2559
|
+
var KI_CATALOG_TABLE_RE = /(?:FROM|JOIN)\s+ki_catalog\.(\w+)/i;
|
|
2560
|
+
function enrichSqlError(error, statement, schemas) {
|
|
2561
|
+
if (!schemas) return error;
|
|
2562
|
+
const match = KI_CATALOG_TABLE_RE.exec(statement);
|
|
2563
|
+
if (!match) return error;
|
|
2564
|
+
const tableName = match[1];
|
|
2565
|
+
const columns = schemas.tables.get(tableName);
|
|
2566
|
+
if (!columns) return error;
|
|
2567
|
+
return `${error}
|
|
2568
|
+
|
|
2569
|
+
Verified columns for ${tableName}: ${columns.join(", ")}`;
|
|
2570
|
+
}
|
|
2571
|
+
|
|
2572
|
+
// src/tools/mutation/alter-system-properties.ts
|
|
2573
|
+
var import_zod11 = require("zod");
|
|
2574
|
+
var AlterSystemPropertiesSchema = import_zod11.z.object({
|
|
2575
|
+
/**
|
|
2576
|
+
* Map of property key -> new value to apply at runtime.
|
|
2577
|
+
* At least one entry is required.
|
|
2578
|
+
*/
|
|
2579
|
+
property_updates_map: import_zod11.z.record(import_zod11.z.string(), import_zod11.z.string()).refine((map) => Object.keys(map).length >= 1, {
|
|
2580
|
+
message: "property_updates_map must have at least one entry"
|
|
2581
|
+
})
|
|
2582
|
+
});
|
|
2583
|
+
var ALTERABLE_PROPERTIES = /* @__PURE__ */ new Set([
|
|
2584
|
+
"concurrent_kernel_execution",
|
|
2585
|
+
"subtask_concurrency_limit",
|
|
2586
|
+
"chunk_size",
|
|
2587
|
+
"chunk_column_max_memory",
|
|
2588
|
+
"chunk_max_memory",
|
|
2589
|
+
"execution_mode",
|
|
2590
|
+
"external_files_directory",
|
|
2591
|
+
"request_timeout",
|
|
2592
|
+
"max_get_records_size",
|
|
2593
|
+
"enable_audit",
|
|
2594
|
+
"audit_headers",
|
|
2595
|
+
"audit_body",
|
|
2596
|
+
"audit_data",
|
|
2597
|
+
"audit_response",
|
|
2598
|
+
"shadow_agg_size",
|
|
2599
|
+
"shadow_filter_size",
|
|
2600
|
+
"enable_overlapped_equi_join",
|
|
2601
|
+
"enable_one_step_compound_equi_join",
|
|
2602
|
+
"kafka_batch_size",
|
|
2603
|
+
"kafka_poll_timeout",
|
|
2604
|
+
"kafka_wait_time",
|
|
2605
|
+
"egress_parquet_compression",
|
|
2606
|
+
"egress_single_file_max_size",
|
|
2607
|
+
"max_concurrent_kernels",
|
|
2608
|
+
"system_metadata_retention_period",
|
|
2609
|
+
"tcs_per_tom",
|
|
2610
|
+
"tps_per_tom",
|
|
2611
|
+
"background_worker_threads",
|
|
2612
|
+
"log_debug_job_info",
|
|
2613
|
+
"enable_thread_hang_logging",
|
|
2614
|
+
"ai_enable_rag",
|
|
2615
|
+
"ai_api_provider",
|
|
2616
|
+
"ai_api_url",
|
|
2617
|
+
"ai_api_key",
|
|
2618
|
+
"ai_api_connection_timeout",
|
|
2619
|
+
"ai_api_embeddings_model",
|
|
2620
|
+
"telm_persist_query_metrics",
|
|
2621
|
+
"postgres_proxy_idle_connection_timeout",
|
|
2622
|
+
"postgres_proxy_keep_alive",
|
|
2623
|
+
"kifs_directory_data_limit",
|
|
2624
|
+
"compression_codec",
|
|
2625
|
+
"disk_auto_optimize_timeout",
|
|
2626
|
+
"ha_consumer_replay_offset"
|
|
2627
|
+
]);
|
|
2628
|
+
var BLOCKED_PROPERTIES = /* @__PURE__ */ new Set([
|
|
2629
|
+
"ai_api_key",
|
|
2630
|
+
// credential — would appear in audit logs
|
|
2631
|
+
"external_files_directory"
|
|
2632
|
+
// filesystem path — potential path traversal
|
|
2633
|
+
]);
|
|
2634
|
+
function findDisallowedProperties(requestedKeys) {
|
|
2635
|
+
return requestedKeys.filter(
|
|
2636
|
+
(key) => !ALTERABLE_PROPERTIES.has(key) || BLOCKED_PROPERTIES.has(key)
|
|
2637
|
+
);
|
|
2638
|
+
}
|
|
2639
|
+
async function readRequestedProperties(session2, requestedKeys) {
|
|
2640
|
+
try {
|
|
2641
|
+
const response = await session2.makeRequest("/show/system/properties", {
|
|
2642
|
+
options: {}
|
|
2643
|
+
});
|
|
2644
|
+
if (!response.ok) return {};
|
|
2645
|
+
const raw = await response.text();
|
|
2646
|
+
let parsed;
|
|
2647
|
+
try {
|
|
2648
|
+
parsed = JSON.parse(raw);
|
|
2649
|
+
} catch {
|
|
2650
|
+
return {};
|
|
2651
|
+
}
|
|
2652
|
+
const inner = parseDataStr(parsed.data_str, raw);
|
|
2653
|
+
if (!inner.ok) return {};
|
|
2654
|
+
const propertyMap = inner.data?.property_map ?? {};
|
|
2655
|
+
return Object.fromEntries(
|
|
2656
|
+
requestedKeys.filter((key) => Object.prototype.hasOwnProperty.call(propertyMap, key)).map((key) => [key, propertyMap[key]])
|
|
2657
|
+
);
|
|
2658
|
+
} catch {
|
|
2659
|
+
return {};
|
|
2660
|
+
}
|
|
2661
|
+
}
|
|
2662
|
+
function computeVerification(requestedMap, afterState) {
|
|
2663
|
+
for (const [key, expectedValue] of Object.entries(requestedMap)) {
|
|
2664
|
+
if (afterState[key] !== expectedValue) {
|
|
2665
|
+
return "failed";
|
|
2666
|
+
}
|
|
2667
|
+
}
|
|
2668
|
+
return "confirmed";
|
|
2669
|
+
}
|
|
2670
|
+
async function alterSystemProperties(session2, input5) {
|
|
2671
|
+
const requestedKeys = Object.keys(input5.property_updates_map);
|
|
2672
|
+
const disallowed = findDisallowedProperties(requestedKeys);
|
|
2673
|
+
if (disallowed.length > 0) {
|
|
2674
|
+
return {
|
|
2675
|
+
ok: false,
|
|
2676
|
+
status: 400,
|
|
2677
|
+
error: `Property rejected: ${disallowed.map((k) => `'${k}'`).join(", ")} not supported by /alter/system/properties`,
|
|
2678
|
+
raw: ""
|
|
2679
|
+
};
|
|
2680
|
+
}
|
|
2681
|
+
const beforeState = await readRequestedProperties(session2, requestedKeys);
|
|
2682
|
+
let mutationResponse;
|
|
2683
|
+
let rawText;
|
|
2684
|
+
try {
|
|
2685
|
+
mutationResponse = await session2.makeRequest("/alter/system/properties", {
|
|
2686
|
+
property_updates_map: input5.property_updates_map
|
|
2687
|
+
});
|
|
2688
|
+
rawText = await mutationResponse.text();
|
|
2689
|
+
} catch (error) {
|
|
2690
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2691
|
+
return { ok: false, status: 0, error: message, raw: "" };
|
|
2692
|
+
}
|
|
2693
|
+
if (!mutationResponse.ok) {
|
|
2694
|
+
return {
|
|
2695
|
+
ok: false,
|
|
2696
|
+
status: mutationResponse.status,
|
|
2697
|
+
error: `HTTP ${mutationResponse.status}`,
|
|
2698
|
+
raw: rawText
|
|
2699
|
+
};
|
|
2700
|
+
}
|
|
2701
|
+
let parsedMutation;
|
|
2702
|
+
try {
|
|
2703
|
+
parsedMutation = JSON.parse(rawText);
|
|
2704
|
+
} catch (parseError) {
|
|
2705
|
+
const message = parseError instanceof Error ? parseError.message : String(parseError);
|
|
2706
|
+
return {
|
|
2707
|
+
ok: false,
|
|
2708
|
+
status: 200,
|
|
2709
|
+
error: `JSON parse error: ${message}`,
|
|
2710
|
+
raw: rawText
|
|
2711
|
+
};
|
|
2712
|
+
}
|
|
2713
|
+
const innerMutation = parseDataStr(parsedMutation.data_str, rawText);
|
|
2714
|
+
if (!innerMutation.ok) return innerMutation;
|
|
2715
|
+
const updatedPropertiesMap = innerMutation.data?.updated_properties_map ?? {};
|
|
2716
|
+
let afterState;
|
|
2717
|
+
let verification;
|
|
2718
|
+
try {
|
|
2719
|
+
const verifyResponse = await session2.makeRequest("/show/system/properties", { options: {} });
|
|
2720
|
+
if (!verifyResponse.ok) {
|
|
2721
|
+
afterState = {};
|
|
2722
|
+
verification = "unavailable";
|
|
2723
|
+
} else {
|
|
2724
|
+
const verifyRaw = await verifyResponse.text();
|
|
2725
|
+
let parsedVerify;
|
|
2726
|
+
try {
|
|
2727
|
+
parsedVerify = JSON.parse(verifyRaw);
|
|
2728
|
+
} catch {
|
|
2729
|
+
afterState = {};
|
|
2730
|
+
verification = "unavailable";
|
|
2731
|
+
const data2 = {
|
|
2732
|
+
updated_properties_map: updatedPropertiesMap,
|
|
2733
|
+
before_state: beforeState,
|
|
2734
|
+
after_state: afterState,
|
|
2735
|
+
verification
|
|
2736
|
+
};
|
|
2737
|
+
return { ok: true, data: data2 };
|
|
2738
|
+
}
|
|
2739
|
+
const innerVerify = parseDataStr(parsedVerify.data_str, verifyRaw);
|
|
2740
|
+
if (!innerVerify.ok) {
|
|
2741
|
+
afterState = {};
|
|
2742
|
+
verification = "unavailable";
|
|
2743
|
+
} else {
|
|
2744
|
+
const verifyPropertyMap = innerVerify.data?.property_map ?? {};
|
|
2745
|
+
afterState = Object.fromEntries(
|
|
2746
|
+
requestedKeys.filter((key) => Object.prototype.hasOwnProperty.call(verifyPropertyMap, key)).map((key) => [key, verifyPropertyMap[key]])
|
|
2747
|
+
);
|
|
2748
|
+
verification = computeVerification(input5.property_updates_map, afterState);
|
|
2749
|
+
}
|
|
2750
|
+
}
|
|
2751
|
+
} catch {
|
|
2752
|
+
afterState = {};
|
|
2753
|
+
verification = "unavailable";
|
|
2754
|
+
}
|
|
2755
|
+
const data = {
|
|
2756
|
+
updated_properties_map: updatedPropertiesMap,
|
|
2757
|
+
before_state: beforeState,
|
|
2758
|
+
after_state: afterState,
|
|
2759
|
+
verification
|
|
2760
|
+
};
|
|
2761
|
+
return { ok: true, data };
|
|
2762
|
+
}
|
|
2763
|
+
|
|
2764
|
+
// src/tools/mutation/execute-mutation-sql.ts
|
|
2765
|
+
var import_zod12 = require("zod");
|
|
2766
|
+
var ExecuteMutationSqlSchema = import_zod12.z.object({
|
|
2767
|
+
statement: import_zod12.z.string().min(1),
|
|
2768
|
+
limit: import_zod12.z.number().int().min(1).max(1e4).default(100)
|
|
2769
|
+
});
|
|
2770
|
+
var SQL_COMMENTS2 = /\/\*[\s\S]*?\*\/|--[^\n]*/g;
|
|
2771
|
+
var DENY_LIST_PATTERN = /\b(DROP\s+(TABLE|SCHEMA|DATABASE|INDEX|VIEW|MATERIALIZED\s+VIEW|PROCEDURE|FUNCTION|SEQUENCE|TYPE)\b|TRUNCATE(\s+TABLE)?\b|DELETE\s+FROM\b|DELETE\b(?!\s+INDEX)|(?<!FOR\s)UPDATE\s+\w)/i;
|
|
2772
|
+
function isDeniedMutationSql(statement) {
|
|
2773
|
+
const stripped = statement.replace(SQL_COMMENTS2, " ").trim();
|
|
2774
|
+
return DENY_LIST_PATTERN.test(stripped);
|
|
2775
|
+
}
|
|
2776
|
+
async function executeMutationSql(session2, statement, limit = 100) {
|
|
2777
|
+
if (isDeniedMutationSql(statement)) {
|
|
2778
|
+
return {
|
|
2779
|
+
ok: false,
|
|
2780
|
+
status: 400,
|
|
2781
|
+
error: "SQL rejected: destructive statements (DROP, TRUNCATE, DELETE, UPDATE) are not permitted, even inside a CTE",
|
|
2782
|
+
raw: statement
|
|
2783
|
+
};
|
|
2784
|
+
}
|
|
2785
|
+
let response;
|
|
2786
|
+
let rawText;
|
|
2787
|
+
try {
|
|
2788
|
+
response = await session2.makeRequest("/execute/sql", {
|
|
2789
|
+
statement,
|
|
2790
|
+
offset: 0,
|
|
2791
|
+
limit,
|
|
2792
|
+
encoding: "json",
|
|
2793
|
+
options: {}
|
|
2794
|
+
});
|
|
2795
|
+
rawText = await response.text();
|
|
2796
|
+
} catch (err) {
|
|
2797
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2798
|
+
return { ok: false, status: 0, error: message, raw: "" };
|
|
2799
|
+
}
|
|
2800
|
+
if (!response.ok) {
|
|
2801
|
+
return {
|
|
2802
|
+
ok: false,
|
|
2803
|
+
status: response.status,
|
|
2804
|
+
error: `HTTP ${response.status}`,
|
|
2805
|
+
raw: rawText
|
|
2806
|
+
};
|
|
2807
|
+
}
|
|
2808
|
+
let outer;
|
|
2809
|
+
try {
|
|
2810
|
+
outer = JSON.parse(rawText);
|
|
2811
|
+
} catch (err) {
|
|
2812
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2813
|
+
return {
|
|
2814
|
+
ok: false,
|
|
2815
|
+
status: 200,
|
|
2816
|
+
error: `JSON parse error: ${message}`,
|
|
2817
|
+
raw: rawText
|
|
2818
|
+
};
|
|
2819
|
+
}
|
|
2820
|
+
if (outer.status === "ERROR") {
|
|
2821
|
+
return {
|
|
2822
|
+
ok: false,
|
|
2823
|
+
status: 400,
|
|
2824
|
+
error: outer.message,
|
|
2825
|
+
raw: rawText
|
|
2826
|
+
};
|
|
2827
|
+
}
|
|
2828
|
+
let dataStr;
|
|
2829
|
+
try {
|
|
2830
|
+
dataStr = JSON.parse(outer.data_str);
|
|
2831
|
+
} catch (err) {
|
|
2832
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2833
|
+
return {
|
|
2834
|
+
ok: false,
|
|
2835
|
+
status: 200,
|
|
2836
|
+
error: `data_str parse error: ${message}`,
|
|
2837
|
+
raw: rawText
|
|
2838
|
+
};
|
|
2839
|
+
}
|
|
2840
|
+
let rows;
|
|
2841
|
+
try {
|
|
2842
|
+
rows = JSON.parse(dataStr.json_encoded_response);
|
|
2843
|
+
} catch (err) {
|
|
2844
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2845
|
+
return {
|
|
2846
|
+
ok: false,
|
|
2847
|
+
status: 200,
|
|
2848
|
+
error: `JSON parse error: ${message}`,
|
|
2849
|
+
raw: rawText
|
|
2850
|
+
};
|
|
2851
|
+
}
|
|
2852
|
+
const data = {
|
|
2853
|
+
rows,
|
|
2854
|
+
total_records: dataStr.total_number_of_records
|
|
2855
|
+
};
|
|
2856
|
+
return { ok: true, data };
|
|
2857
|
+
}
|
|
2858
|
+
|
|
2859
|
+
// src/tools/mutation/admin-rebalance.ts
|
|
2860
|
+
var import_zod13 = require("zod");
|
|
2861
|
+
var AdminRebalanceSchema = import_zod13.z.object({
|
|
2862
|
+
rebalance_sharded_data: import_zod13.z.boolean().optional(),
|
|
2863
|
+
rebalance_unsharded_data: import_zod13.z.boolean().optional(),
|
|
2864
|
+
table_includes: import_zod13.z.string().optional(),
|
|
2865
|
+
table_excludes: import_zod13.z.string().optional(),
|
|
2866
|
+
aggressiveness: import_zod13.z.number().int().min(1).max(5).optional(),
|
|
2867
|
+
compact_after_rebalance: import_zod13.z.boolean().optional(),
|
|
2868
|
+
compact_only: import_zod13.z.boolean().optional()
|
|
2869
|
+
});
|
|
2870
|
+
async function readShardState(session2) {
|
|
2871
|
+
try {
|
|
2872
|
+
const response = await session2.makeRequest("/show/system/status", {});
|
|
2873
|
+
if (!response.ok) return {};
|
|
2874
|
+
const raw = await response.text();
|
|
2875
|
+
let parsed;
|
|
2876
|
+
try {
|
|
2877
|
+
parsed = JSON.parse(raw);
|
|
2878
|
+
} catch {
|
|
2879
|
+
return {};
|
|
2880
|
+
}
|
|
2881
|
+
const inner = parseDataStr(parsed.data_str, raw);
|
|
2882
|
+
if (!inner.ok || !inner.data) return {};
|
|
2883
|
+
const data = inner.data;
|
|
2884
|
+
return {
|
|
2885
|
+
shard_map: data.shard_map ?? {},
|
|
2886
|
+
db_status: data.db_status ?? "unknown"
|
|
2887
|
+
};
|
|
2888
|
+
} catch {
|
|
2889
|
+
return {};
|
|
2890
|
+
}
|
|
2891
|
+
}
|
|
2892
|
+
async function adminRebalance(session2, input5) {
|
|
2893
|
+
const beforeState = await readShardState(session2);
|
|
2894
|
+
const options = {};
|
|
2895
|
+
if (input5.rebalance_sharded_data !== void 0) {
|
|
2896
|
+
options.rebalance_sharded_data = String(input5.rebalance_sharded_data);
|
|
2897
|
+
}
|
|
2898
|
+
if (input5.rebalance_unsharded_data !== void 0) {
|
|
2899
|
+
options.rebalance_unsharded_data = String(input5.rebalance_unsharded_data);
|
|
2900
|
+
}
|
|
2901
|
+
if (input5.table_includes !== void 0) {
|
|
2902
|
+
options.table_includes = input5.table_includes;
|
|
2903
|
+
}
|
|
2904
|
+
if (input5.table_excludes !== void 0) {
|
|
2905
|
+
options.table_excludes = input5.table_excludes;
|
|
2906
|
+
}
|
|
2907
|
+
if (input5.aggressiveness !== void 0) {
|
|
2908
|
+
options.aggressiveness = String(input5.aggressiveness);
|
|
2909
|
+
}
|
|
2910
|
+
if (input5.compact_after_rebalance !== void 0) {
|
|
2911
|
+
options.compact_after_rebalance = String(input5.compact_after_rebalance);
|
|
2912
|
+
}
|
|
2913
|
+
if (input5.compact_only !== void 0) {
|
|
2914
|
+
options.compact_only = String(input5.compact_only);
|
|
2915
|
+
}
|
|
2916
|
+
try {
|
|
2917
|
+
const response = await session2.makeRequest("/admin/rebalance", { options });
|
|
2918
|
+
if (!response.ok) {
|
|
2919
|
+
const raw2 = await response.text();
|
|
2920
|
+
return {
|
|
2921
|
+
ok: false,
|
|
2922
|
+
status: response.status,
|
|
2923
|
+
error: `HTTP ${response.status}`,
|
|
2924
|
+
raw: raw2
|
|
2925
|
+
};
|
|
2926
|
+
}
|
|
2927
|
+
const raw = await response.text();
|
|
2928
|
+
let parsed;
|
|
2929
|
+
try {
|
|
2930
|
+
parsed = JSON.parse(raw);
|
|
2931
|
+
} catch (parseError) {
|
|
2932
|
+
const message = parseError instanceof Error ? parseError.message : String(parseError);
|
|
2933
|
+
return {
|
|
2934
|
+
ok: false,
|
|
2935
|
+
status: 200,
|
|
2936
|
+
error: `JSON parse error: ${message}`,
|
|
2937
|
+
raw
|
|
2938
|
+
};
|
|
2939
|
+
}
|
|
2940
|
+
const inner = parseDataStr(parsed.data_str, raw);
|
|
2941
|
+
if (!inner.ok) return inner;
|
|
2942
|
+
const info = inner.data?.info ?? {};
|
|
2943
|
+
const afterState = await readShardState(session2);
|
|
2944
|
+
const verification = Object.keys(afterState).length > 0 ? "confirmed" : "unavailable";
|
|
2945
|
+
const data = {
|
|
2946
|
+
info,
|
|
2947
|
+
before_state: beforeState,
|
|
2948
|
+
after_state: afterState,
|
|
2949
|
+
verification
|
|
2950
|
+
};
|
|
2951
|
+
return { ok: true, data };
|
|
2952
|
+
} catch (error) {
|
|
2953
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2954
|
+
return {
|
|
2955
|
+
ok: false,
|
|
2956
|
+
status: 0,
|
|
2957
|
+
error: message,
|
|
2958
|
+
raw: ""
|
|
2959
|
+
};
|
|
2960
|
+
}
|
|
2961
|
+
}
|
|
2962
|
+
|
|
2963
|
+
// src/tools/mutation/alter-configuration.ts
|
|
2964
|
+
var import_zod14 = require("zod");
|
|
2965
|
+
var AlterConfigurationSchema = import_zod14.z.object({
|
|
2966
|
+
/**
|
|
2967
|
+
* The full replacement content for the gpudb.conf configuration file.
|
|
2968
|
+
* Must be non-empty.
|
|
2969
|
+
*/
|
|
2970
|
+
config_string: import_zod14.z.string().min(1, { message: "config_string must not be empty" })
|
|
2971
|
+
});
|
|
2972
|
+
var PREVIEW_LINES = 20;
|
|
2973
|
+
var EMPTY_SUMMARY2 = { line_count: 0, preview: "" };
|
|
2974
|
+
function summarizeConfig(configString) {
|
|
2975
|
+
const lines = configString.split("\n");
|
|
2976
|
+
const preview = lines.slice(0, PREVIEW_LINES).join("\n");
|
|
2977
|
+
return { line_count: lines.length, preview };
|
|
2978
|
+
}
|
|
2979
|
+
async function readCurrentConfig(session2) {
|
|
2980
|
+
try {
|
|
2981
|
+
const result = await showConfiguration(session2, {});
|
|
2982
|
+
return result.ok ? result.data.config_string : void 0;
|
|
2983
|
+
} catch {
|
|
2984
|
+
return void 0;
|
|
2985
|
+
}
|
|
2986
|
+
}
|
|
2987
|
+
async function alterConfiguration(session2, input5) {
|
|
2988
|
+
if (!session2.makeRequestToPort) {
|
|
2989
|
+
return {
|
|
2990
|
+
ok: false,
|
|
2991
|
+
status: 0,
|
|
2992
|
+
error: "makeRequestToPort not available on this session",
|
|
2993
|
+
raw: ""
|
|
2994
|
+
};
|
|
2995
|
+
}
|
|
2996
|
+
const beforeConfig = await readCurrentConfig(session2);
|
|
2997
|
+
const beforeSummary = beforeConfig !== void 0 ? summarizeConfig(beforeConfig) : EMPTY_SUMMARY2;
|
|
2998
|
+
const hmPort = await discoverHmPort(session2);
|
|
2999
|
+
let mutationResponse;
|
|
3000
|
+
let rawText;
|
|
3001
|
+
try {
|
|
3002
|
+
mutationResponse = await session2.makeRequestToPort(hmPort, "/admin/alter/configuration", {
|
|
3003
|
+
config_string: input5.config_string
|
|
3004
|
+
});
|
|
3005
|
+
rawText = await mutationResponse.text();
|
|
3006
|
+
} catch (error) {
|
|
3007
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3008
|
+
return { ok: false, status: 0, error: message, raw: "" };
|
|
3009
|
+
}
|
|
3010
|
+
if (!mutationResponse.ok) {
|
|
3011
|
+
return {
|
|
3012
|
+
ok: false,
|
|
3013
|
+
status: mutationResponse.status,
|
|
3014
|
+
error: `HTTP ${mutationResponse.status}`,
|
|
3015
|
+
raw: rawText
|
|
3016
|
+
};
|
|
3017
|
+
}
|
|
3018
|
+
let parsedMutation;
|
|
3019
|
+
try {
|
|
3020
|
+
parsedMutation = JSON.parse(rawText);
|
|
3021
|
+
} catch (parseError) {
|
|
3022
|
+
const message = parseError instanceof Error ? parseError.message : String(parseError);
|
|
3023
|
+
return {
|
|
3024
|
+
ok: false,
|
|
3025
|
+
status: 200,
|
|
3026
|
+
error: `JSON parse error: ${message}`,
|
|
3027
|
+
raw: rawText
|
|
3028
|
+
};
|
|
3029
|
+
}
|
|
3030
|
+
const innerMutation = parseDataStr(parsedMutation.data_str, rawText);
|
|
3031
|
+
if (!innerMutation.ok) return innerMutation;
|
|
3032
|
+
const responseInfo = innerMutation.data?.info ?? {};
|
|
3033
|
+
const afterConfig = await readCurrentConfig(session2);
|
|
3034
|
+
let afterSummary;
|
|
3035
|
+
let verification;
|
|
3036
|
+
if (afterConfig === void 0) {
|
|
3037
|
+
afterSummary = EMPTY_SUMMARY2;
|
|
3038
|
+
verification = "unavailable";
|
|
3039
|
+
} else {
|
|
3040
|
+
afterSummary = summarizeConfig(afterConfig);
|
|
3041
|
+
if (beforeConfig === void 0) {
|
|
3042
|
+
verification = "unavailable";
|
|
3043
|
+
} else {
|
|
3044
|
+
verification = afterConfig !== beforeConfig ? "confirmed" : "failed";
|
|
3045
|
+
}
|
|
3046
|
+
}
|
|
3047
|
+
const data = {
|
|
3048
|
+
before_summary: beforeSummary,
|
|
3049
|
+
after_summary: afterSummary,
|
|
3050
|
+
verification,
|
|
3051
|
+
info: responseInfo
|
|
3052
|
+
};
|
|
3053
|
+
return { ok: true, data };
|
|
3054
|
+
}
|
|
3055
|
+
|
|
3056
|
+
// src/tools/mutation/alter-table-columns.ts
|
|
3057
|
+
var import_zod15 = require("zod");
|
|
3058
|
+
var import_prompts6 = require("@inquirer/prompts");
|
|
3059
|
+
var import_picocolors8 = __toESM(require("picocolors"));
|
|
3060
|
+
var import_claude_agent_sdk2 = require("@anthropic-ai/claude-agent-sdk");
|
|
3061
|
+
|
|
3062
|
+
// src/approval/checklist.ts
|
|
3063
|
+
var import_prompts5 = require("@inquirer/prompts");
|
|
3064
|
+
var import_picocolors7 = __toESM(require("picocolors"));
|
|
3065
|
+
var DIVIDER = import_picocolors7.default.dim("\u2500".repeat(60));
|
|
3066
|
+
function renderChecklist(header, summary, items) {
|
|
3067
|
+
const lines = [
|
|
3068
|
+
"",
|
|
3069
|
+
DIVIDER,
|
|
3070
|
+
` ${import_picocolors7.default.bold(import_picocolors7.default.yellow(header))}`,
|
|
3071
|
+
"",
|
|
3072
|
+
` ${import_picocolors7.default.dim("Summary:")} ${summary}`,
|
|
3073
|
+
"",
|
|
3074
|
+
` ${import_picocolors7.default.bold(`${items.length} proposed column change(s):`)}`,
|
|
3075
|
+
""
|
|
3076
|
+
];
|
|
3077
|
+
for (let i = 0; i < items.length; i++) {
|
|
3078
|
+
const item = items[i];
|
|
3079
|
+
lines.push(` ${import_picocolors7.default.bold(`${i + 1}.`)} ${item.label}`, ` ${import_picocolors7.default.dim(item.description)}`);
|
|
3080
|
+
}
|
|
3081
|
+
lines.push("", DIVIDER, "");
|
|
3082
|
+
return lines.join("\n");
|
|
3083
|
+
}
|
|
3084
|
+
async function showChecklist(header, summary, items) {
|
|
3085
|
+
const panel = renderChecklist(header, summary, items);
|
|
3086
|
+
process.stderr.write(panel);
|
|
3087
|
+
try {
|
|
3088
|
+
const selected = await (0, import_prompts5.checkbox)({
|
|
3089
|
+
message: "Select columns to alter (space to toggle, enter to confirm):",
|
|
3090
|
+
choices: items.map((item, i) => ({
|
|
3091
|
+
value: i,
|
|
3092
|
+
name: item.label,
|
|
3093
|
+
description: item.description,
|
|
3094
|
+
checked: true
|
|
3095
|
+
})),
|
|
3096
|
+
loop: false
|
|
3097
|
+
});
|
|
3098
|
+
if (selected.length === 0) {
|
|
3099
|
+
return { action: "cancelled" };
|
|
3100
|
+
}
|
|
3101
|
+
return { action: "confirmed", selectedIndices: selected };
|
|
3102
|
+
} catch {
|
|
3103
|
+
return { action: "cancelled" };
|
|
3104
|
+
}
|
|
3105
|
+
}
|
|
3106
|
+
|
|
3107
|
+
// src/tools/mutation/alter-table-columns.ts
|
|
3108
|
+
var ColumnChangeSchema = import_zod15.z.object({
|
|
3109
|
+
column_name: import_zod15.z.string().min(1),
|
|
3110
|
+
new_definition: import_zod15.z.string().min(1),
|
|
3111
|
+
description: import_zod15.z.string().min(1)
|
|
3112
|
+
});
|
|
3113
|
+
var AlterTableColumnsSchema = import_zod15.z.object({
|
|
3114
|
+
table_name: import_zod15.z.string().min(1),
|
|
3115
|
+
rationale: import_zod15.z.string().min(1),
|
|
3116
|
+
columns: import_zod15.z.array(ColumnChangeSchema).min(2).max(50)
|
|
3117
|
+
});
|
|
3118
|
+
function buildAlterTableSql(tableName, columns) {
|
|
3119
|
+
const clauses = columns.map((c) => ` ALTER COLUMN ${c.column_name} ${c.new_definition}`);
|
|
3120
|
+
return `ALTER TABLE ${tableName}
|
|
3121
|
+
${clauses.join(",\n")}`;
|
|
3122
|
+
}
|
|
3123
|
+
var SQL_DIVIDER = import_picocolors8.default.dim("\u2500".repeat(60));
|
|
3124
|
+
async function confirmSqlExecution(sql) {
|
|
3125
|
+
const panel = [
|
|
3126
|
+
"",
|
|
3127
|
+
SQL_DIVIDER,
|
|
3128
|
+
` ${import_picocolors8.default.bold(import_picocolors8.default.yellow("Generated SQL:"))}`,
|
|
3129
|
+
"",
|
|
3130
|
+
sql.split("\n").map((line) => ` ${line}`).join("\n"),
|
|
3131
|
+
"",
|
|
3132
|
+
SQL_DIVIDER,
|
|
3133
|
+
""
|
|
3134
|
+
].join("\n");
|
|
3135
|
+
process.stderr.write(panel);
|
|
3136
|
+
try {
|
|
3137
|
+
const response = await (0, import_prompts6.input)({ message: "Execute? (y/n):" });
|
|
3138
|
+
return response.trim().toLowerCase() === "y";
|
|
3139
|
+
} catch {
|
|
3140
|
+
return false;
|
|
3141
|
+
}
|
|
3142
|
+
}
|
|
3143
|
+
function buildChecklistItems(columns) {
|
|
3144
|
+
return columns.map((col) => ({
|
|
3145
|
+
label: `${col.column_name}: ${col.new_definition}`,
|
|
3146
|
+
description: col.description
|
|
3147
|
+
}));
|
|
3148
|
+
}
|
|
3149
|
+
async function alterTableColumns(session2, toolInput) {
|
|
3150
|
+
const { table_name, rationale, columns } = toolInput;
|
|
3151
|
+
const items = buildChecklistItems(columns);
|
|
3152
|
+
const selection = await showChecklist(
|
|
3153
|
+
"ALTER TABLE Column Changes",
|
|
3154
|
+
`Table: ${table_name}
|
|
3155
|
+
${rationale}`,
|
|
3156
|
+
items
|
|
3157
|
+
);
|
|
3158
|
+
if (selection.action === "cancelled") {
|
|
3159
|
+
return {
|
|
3160
|
+
ok: true,
|
|
3161
|
+
data: {
|
|
3162
|
+
status: "cancelled",
|
|
3163
|
+
total_count: columns.length
|
|
3164
|
+
}
|
|
3165
|
+
};
|
|
3166
|
+
}
|
|
3167
|
+
const selectedColumns = selection.selectedIndices.map((i) => {
|
|
3168
|
+
const col = columns[i];
|
|
3169
|
+
return {
|
|
3170
|
+
column_name: col.column_name,
|
|
3171
|
+
new_definition: col.new_definition
|
|
3172
|
+
};
|
|
3173
|
+
});
|
|
3174
|
+
const sql = buildAlterTableSql(table_name, selectedColumns);
|
|
3175
|
+
const confirmed = await confirmSqlExecution(sql);
|
|
3176
|
+
if (!confirmed) {
|
|
3177
|
+
return {
|
|
3178
|
+
ok: true,
|
|
3179
|
+
data: {
|
|
3180
|
+
status: "declined",
|
|
3181
|
+
sql,
|
|
3182
|
+
selected_count: selectedColumns.length,
|
|
3183
|
+
total_count: columns.length
|
|
3184
|
+
}
|
|
3185
|
+
};
|
|
3186
|
+
}
|
|
3187
|
+
const result = await executeMutationSql(session2, sql);
|
|
3188
|
+
if (!result.ok) {
|
|
3189
|
+
return result;
|
|
3190
|
+
}
|
|
3191
|
+
return {
|
|
3192
|
+
ok: true,
|
|
3193
|
+
data: {
|
|
3194
|
+
status: "executed",
|
|
3195
|
+
sql,
|
|
3196
|
+
selected_count: selectedColumns.length,
|
|
3197
|
+
total_count: columns.length,
|
|
3198
|
+
execution_result: result.data
|
|
3199
|
+
}
|
|
3200
|
+
};
|
|
3201
|
+
}
|
|
3202
|
+
function makeAlterTableColumnsTool(session2, deps) {
|
|
3203
|
+
return (0, import_claude_agent_sdk2.tool)(
|
|
3204
|
+
"kinetica_alter_table_columns",
|
|
3205
|
+
"Batch multiple column type/property changes on a SINGLE table into one efficient ALTER TABLE statement. Use when recommending 2+ column changes on the same table (e.g., adding DICT encoding to multiple columns, adding TEXT_SEARCH, changing column types). Each column change requires: column_name, new_definition (full type definition with properties and nullability), and description (human-readable reason). The operator selects which columns to alter via interactive checklist, then confirms the generated SQL. For a single column change, use kinetica_execute_mutation_sql directly. Kinetica ALTER TABLE syntax requires repeating the FULL column definition \u2014 properties go INSIDE parentheses: VARCHAR(50, DICT), not VARCHAR(50) DICT.",
|
|
3206
|
+
AlterTableColumnsSchema.shape,
|
|
3207
|
+
async (args) => {
|
|
3208
|
+
const parsed = AlterTableColumnsSchema.parse(args);
|
|
3209
|
+
const result = await alterTableColumns(session2, parsed);
|
|
3210
|
+
deps.logMutationAudit("kinetica_alter_table_columns", result, {
|
|
3211
|
+
table_name: parsed.table_name,
|
|
3212
|
+
columns_proposed: parsed.columns.length
|
|
3213
|
+
});
|
|
3214
|
+
return {
|
|
3215
|
+
content: [{ type: "text", text: deps.applyOutputPipeline(result) }]
|
|
3216
|
+
};
|
|
3217
|
+
},
|
|
3218
|
+
{ annotations: { destructive: true, readOnly: false } }
|
|
3219
|
+
);
|
|
3220
|
+
}
|
|
3221
|
+
|
|
3222
|
+
// src/tools/audit-redact.ts
|
|
3223
|
+
var import_node_crypto = require("crypto");
|
|
3224
|
+
var FINGERPRINT_THRESHOLD = 300;
|
|
3225
|
+
var FINGERPRINT_HEX_LEN = 12;
|
|
3226
|
+
var ALWAYS_FINGERPRINT_KEYS = /* @__PURE__ */ new Set(["config_string"]);
|
|
3227
|
+
var CREDENTIAL_PATTERNS = [
|
|
3228
|
+
// password = '...' or password='...'
|
|
3229
|
+
{
|
|
3230
|
+
regex: /(password\s*=\s*)['"][^'"]*['"]/gi,
|
|
3231
|
+
replacement: "$1'[REDACTED]'"
|
|
3232
|
+
},
|
|
3233
|
+
// IDENTIFIED BY '...' (Kinetica CREATE/ALTER USER)
|
|
3234
|
+
{
|
|
3235
|
+
regex: /(identified\s+by\s+)['"][^'"]*['"]/gi,
|
|
3236
|
+
replacement: "$1'[REDACTED]'"
|
|
3237
|
+
},
|
|
3238
|
+
// apiKey=... / api_key: ... / access_token=... / secret = '...' / "access_token": "..."
|
|
3239
|
+
{
|
|
3240
|
+
regex: /(api[_-]?key|access[_-]?token|secret)['"]?(\s*[:=]\s*)['"]?([^\s'"`,;)]+)['"]?/gi,
|
|
3241
|
+
replacement: "$1$2'[REDACTED]'"
|
|
3242
|
+
}
|
|
3243
|
+
];
|
|
3244
|
+
function fingerprint(value) {
|
|
3245
|
+
const sha = (0, import_node_crypto.createHash)("sha256").update(value).digest("hex").slice(0, FINGERPRINT_HEX_LEN);
|
|
3246
|
+
return `<${value.length} bytes, sha256:${sha}\u2026>`;
|
|
3247
|
+
}
|
|
3248
|
+
function scrubCredentialPatterns(value) {
|
|
3249
|
+
return CREDENTIAL_PATTERNS.reduce(
|
|
3250
|
+
(text, { regex, replacement }) => text.replace(regex, replacement),
|
|
3251
|
+
value
|
|
3252
|
+
);
|
|
3253
|
+
}
|
|
3254
|
+
function redactValue(value) {
|
|
3255
|
+
if (typeof value === "string") {
|
|
3256
|
+
const scrubbed = scrubCredentialPatterns(value);
|
|
3257
|
+
return scrubbed.length > FINGERPRINT_THRESHOLD ? fingerprint(scrubbed) : scrubbed;
|
|
3258
|
+
}
|
|
3259
|
+
if (Array.isArray(value)) {
|
|
3260
|
+
return value.map(redactValue);
|
|
3261
|
+
}
|
|
3262
|
+
if (value && typeof value === "object") {
|
|
3263
|
+
return Object.fromEntries(
|
|
3264
|
+
Object.entries(value).map(([k, v]) => [k, redactNamedField(k, v)])
|
|
3265
|
+
);
|
|
3266
|
+
}
|
|
3267
|
+
return value;
|
|
3268
|
+
}
|
|
3269
|
+
function redactNamedField(key, value) {
|
|
3270
|
+
if (ALWAYS_FINGERPRINT_KEYS.has(key) && typeof value === "string") {
|
|
3271
|
+
return fingerprint(value);
|
|
3272
|
+
}
|
|
3273
|
+
return redactValue(value);
|
|
3274
|
+
}
|
|
3275
|
+
function redactAuditInput(input5) {
|
|
3276
|
+
return Object.fromEntries(Object.entries(input5).map(([k, v]) => [k, redactNamedField(k, v)]));
|
|
3277
|
+
}
|
|
3278
|
+
|
|
3279
|
+
// src/tools/index.ts
|
|
3280
|
+
var MUTATION_TOOL_NAMES = [
|
|
3281
|
+
"kinetica_alter_system_properties",
|
|
3282
|
+
"kinetica_execute_mutation_sql",
|
|
3283
|
+
"kinetica_admin_rebalance",
|
|
3284
|
+
"kinetica_alter_configuration"
|
|
3285
|
+
];
|
|
3286
|
+
var ALTER_TABLE_COLUMNS_TOOL_NAME = "kinetica_alter_table_columns";
|
|
3287
|
+
var DIAGNOSTIC_TOOL_NAMES = [
|
|
3288
|
+
"kinetica_health_check",
|
|
3289
|
+
"kinetica_get_metrics",
|
|
3290
|
+
"kinetica_cluster_status",
|
|
3291
|
+
"kinetica_node_details",
|
|
3292
|
+
"kinetica_get_logs",
|
|
3293
|
+
"kinetica_show_configuration",
|
|
3294
|
+
"kinetica_get_system_properties",
|
|
3295
|
+
"kinetica_execute_sql",
|
|
3296
|
+
"kinetica_explain_query",
|
|
3297
|
+
"kinetica_system_timing",
|
|
3298
|
+
"kinetica_resource_groups",
|
|
3299
|
+
"kinetica_verify_db",
|
|
3300
|
+
"kinetica_show_security",
|
|
3301
|
+
"kinetica_show_table",
|
|
3302
|
+
"kinetica_resource_objects",
|
|
3303
|
+
"kinetica_host_manager_status"
|
|
3304
|
+
];
|
|
3305
|
+
function createDiagnosticRegistry() {
|
|
3306
|
+
return DIAGNOSTIC_TOOL_NAMES.reduce(
|
|
3307
|
+
(registry, name) => registry.registerReadOnlyTool(name),
|
|
3308
|
+
createRegistry()
|
|
3309
|
+
);
|
|
3310
|
+
}
|
|
3311
|
+
function applyOutputPipeline(result) {
|
|
3312
|
+
const payload = result.ok ? result.data : result;
|
|
3313
|
+
return truncateOutput(formatOutput(payload));
|
|
3314
|
+
}
|
|
3315
|
+
function logMutationAudit(toolName, result, input5) {
|
|
3316
|
+
const statusLabel = result.ok ? import_picocolors9.default.bold(import_picocolors9.default.green("EXECUTED")) : import_picocolors9.default.bold(import_picocolors9.default.red("FAILED"));
|
|
3317
|
+
const redacted = redactAuditInput(input5);
|
|
3318
|
+
const inputSummary = Object.entries(redacted).map(([k, v]) => `${k}=${typeof v === "string" ? v : JSON.stringify(v)}`).join(", ");
|
|
3319
|
+
const displayName = formatToolName(toolName);
|
|
3320
|
+
process.stderr.write(
|
|
3321
|
+
` ${import_picocolors9.default.dim("MUTATION")} ${statusLabel} ${displayName}
|
|
3322
|
+
${import_picocolors9.default.dim(inputSummary)}
|
|
3323
|
+
|
|
3324
|
+
`
|
|
3325
|
+
);
|
|
3326
|
+
}
|
|
3327
|
+
function makeAlterSystemPropertiesTool(session2) {
|
|
3328
|
+
return (0, import_claude_agent_sdk3.tool)(
|
|
3329
|
+
"kinetica_alter_system_properties",
|
|
3330
|
+
"Apply runtime configuration changes to Kinetica via /alter/system/properties. Accepts a map of property name to new value. Captures current values before applying and verifies changes after. Only the 43 documented properties are accepted \u2014 requests with unsupported property names are rejected before any API call. Blocked for safety: ai_api_key, external_files_directory. Common properties (7.2.x): subtask_concurrency_limit, request_timeout, max_get_records_size, concurrent_kernel_execution, max_concurrent_kernels, tcs_per_tom, tps_per_tom, chunk_size, enable_audit, egress_parquet_compression, background_worker_threads. All property names require 'conf.' prefix in /show/system/properties but NOT in /alter/system/properties.",
|
|
3331
|
+
AlterSystemPropertiesSchema.shape,
|
|
3332
|
+
async (args) => {
|
|
3333
|
+
const parsed = AlterSystemPropertiesSchema.parse(args);
|
|
3334
|
+
const result = await alterSystemProperties(session2, parsed);
|
|
3335
|
+
logMutationAudit("kinetica_alter_system_properties", result, parsed);
|
|
3336
|
+
return { content: [{ type: "text", text: applyOutputPipeline(result) }] };
|
|
3337
|
+
},
|
|
3338
|
+
{ annotations: { destructive: true, readOnly: false } }
|
|
3339
|
+
);
|
|
3340
|
+
}
|
|
3341
|
+
function makeExecuteMutationSqlTool(session2) {
|
|
3342
|
+
return (0, import_claude_agent_sdk3.tool)(
|
|
3343
|
+
"kinetica_execute_mutation_sql",
|
|
3344
|
+
"Execute a SQL mutation statement on Kinetica (CREATE INDEX, ALTER TABLE, ALTER SYSTEM SET, REFRESH MATERIALIZED VIEW, etc.). ANALYZE TABLE is NOT supported by Kinetica \u2014 do not call it. DROP, TRUNCATE, DELETE, and UPDATE are always rejected \u2014 even when wrapped in a CTE (WITH ... DELETE/UPDATE). Requires user approval before execution.",
|
|
3345
|
+
ExecuteMutationSqlSchema.shape,
|
|
3346
|
+
async (args) => {
|
|
3347
|
+
const parsed = ExecuteMutationSqlSchema.parse(args);
|
|
3348
|
+
const result = await executeMutationSql(session2, parsed.statement, parsed.limit);
|
|
3349
|
+
logMutationAudit("kinetica_execute_mutation_sql", result, parsed);
|
|
3350
|
+
return { content: [{ type: "text", text: applyOutputPipeline(result) }] };
|
|
3351
|
+
},
|
|
3352
|
+
{ annotations: { destructive: true, readOnly: false } }
|
|
3353
|
+
);
|
|
3354
|
+
}
|
|
3355
|
+
function makeAdminRebalanceTool(session2) {
|
|
3356
|
+
return (0, import_claude_agent_sdk3.tool)(
|
|
3357
|
+
"kinetica_admin_rebalance",
|
|
3358
|
+
"Trigger shard data rebalancing across Kinetica cluster ranks via /admin/rebalance. Options: rebalance_sharded_data, rebalance_unsharded_data, table_includes, table_excludes, aggressiveness (1-5, capped for safety), compact_after_rebalance, compact_only. Captures before/after shard distribution for verification. WARNING: rebalance causes delayed query responses while running \u2014 use low aggressiveness (1-3) during production hours. NOTE: On single-worker-rank clusters, /admin/rebalance returns 'Database must be offline' because there is only one data rank \u2014 rebalance is only meaningful with 2+ worker ranks.",
|
|
3359
|
+
AdminRebalanceSchema.shape,
|
|
3360
|
+
async (args) => {
|
|
3361
|
+
const parsed = AdminRebalanceSchema.parse(args);
|
|
3362
|
+
const result = await adminRebalance(session2, parsed);
|
|
3363
|
+
logMutationAudit("kinetica_admin_rebalance", result, parsed);
|
|
3364
|
+
return { content: [{ type: "text", text: applyOutputPipeline(result) }] };
|
|
3365
|
+
},
|
|
3366
|
+
{ annotations: { destructive: true, readOnly: false } }
|
|
3367
|
+
);
|
|
3368
|
+
}
|
|
3369
|
+
function makeAlterConfigurationTool(session2) {
|
|
3370
|
+
return (0, import_claude_agent_sdk3.tool)(
|
|
3371
|
+
"kinetica_alter_configuration",
|
|
3372
|
+
"Replace the full gpudb.conf configuration on the Kinetica host manager via /admin/alter/configuration (port 9300). Requires the complete config_string content \u2014 the entire file is replaced. Captures before/after config summaries for verification. WARNING: This replaces the ENTIRE configuration file. Always read the current config via kinetica_show_configuration first, make targeted changes to specific lines, and submit the full modified content. Never compose a config from scratch. Requires host manager connectivity and user approval.",
|
|
3373
|
+
AlterConfigurationSchema.shape,
|
|
3374
|
+
async (args) => {
|
|
3375
|
+
const parsed = AlterConfigurationSchema.parse(args);
|
|
3376
|
+
const result = await alterConfiguration(session2, parsed);
|
|
3377
|
+
logMutationAudit("kinetica_alter_configuration", result, parsed);
|
|
3378
|
+
return { content: [{ type: "text", text: applyOutputPipeline(result) }] };
|
|
3379
|
+
},
|
|
3380
|
+
{ annotations: { destructive: true, readOnly: false } }
|
|
3381
|
+
);
|
|
3382
|
+
}
|
|
3383
|
+
function makeHealthCheckTool(session2) {
|
|
3384
|
+
return (0, import_claude_agent_sdk3.tool)(
|
|
3385
|
+
"kinetica_health_check",
|
|
3386
|
+
"Query Kinetica system health status via /show/system/status. Returns 11 components as rows (component, status): system (cluster status, leader, offline), ranks (per-rank status/mode/accepting_jobs/read_only), hosts (hostname, memory, GPU IDs, sub-service statuses), http_server (connections current/refused, thread capacity), ha_cluster_info/ha_status, graph, text, migrations, triggers, symbols. NOTE: Each status value is a JSON-encoded string \u2014 parse mentally to extract nested fields. Healthy baseline: system.status='running', all ranks rank_status='running' + rank_mode='run', hosts.status='running', http_server.connections.refused=0.",
|
|
3387
|
+
{},
|
|
3388
|
+
async (_args) => {
|
|
3389
|
+
const result = await healthCheck(session2);
|
|
3390
|
+
return { content: [{ type: "text", text: applyOutputPipeline(result) }] };
|
|
3391
|
+
},
|
|
3392
|
+
{ annotations: { readOnly: true } }
|
|
3393
|
+
);
|
|
3394
|
+
}
|
|
3395
|
+
function makeGetMetricsTool(session2) {
|
|
3396
|
+
return (0, import_claude_agent_sdk3.tool)(
|
|
3397
|
+
"kinetica_get_metrics",
|
|
3398
|
+
"Retrieve per-rank storage tier metrics from /show/resource/statistics. Returns rows with: rank, ram_used, ram_limit, ram_percent (computed as 'X.Y%' e.g. '9.6%'), persist_used, disk_used, vram_used (all string values). Rank 0 is the head/coordinator node with minimal RAM (~794MB limit) and empty persist/disk/vram fields. Worker ranks (1+) hold the actual data with ~5.6GB RAM limit. Empty string means tier not configured; '0' means configured but unused. Healthy baseline: ram_percent under 80%. Optional node_id to annotate which node was requested.",
|
|
3399
|
+
{ node_id: import_zod16.z.string().optional() },
|
|
3400
|
+
async (args) => {
|
|
3401
|
+
const result = await getMetrics(session2, args.node_id);
|
|
3402
|
+
return { content: [{ type: "text", text: applyOutputPipeline(result) }] };
|
|
3403
|
+
},
|
|
3404
|
+
{ annotations: { readOnly: true } }
|
|
3405
|
+
);
|
|
3406
|
+
}
|
|
3407
|
+
function makeClusterStatusTool(session2) {
|
|
3408
|
+
return (0, import_claude_agent_sdk3.tool)(
|
|
3409
|
+
"kinetica_cluster_status",
|
|
3410
|
+
"Get full cluster overview via 4 sub-calls: (1) /admin/show/cluster/operations \u2014 in_progress flag, percent_complete, rebalance/add/remove status; (2) /admin/show/shards \u2014 summarized as shard distribution per rank (total_shards, rank_count, per-rank shard_count/percent, balanced flag, shard_array_version); (3) /admin/show/alerts on host manager port \u2014 recent alerts (gracefully degrades if unavailable); (4) /admin/show/jobs \u2014 active async jobs as {job_id, status, endpoint} objects. Healthy baseline: operations.in_progress=false, shards.balanced=true, empty alerts/jobs arrays.",
|
|
3411
|
+
{},
|
|
3412
|
+
async (_args) => {
|
|
3413
|
+
const result = await clusterStatus(session2);
|
|
3414
|
+
return { content: [{ type: "text", text: applyOutputPipeline(result) }] };
|
|
3415
|
+
},
|
|
3416
|
+
{ annotations: { readOnly: true } }
|
|
3417
|
+
);
|
|
3418
|
+
}
|
|
3419
|
+
function makeNodeDetailsTool(session2) {
|
|
3420
|
+
return (0, import_claude_agent_sdk3.tool)(
|
|
3421
|
+
"kinetica_node_details",
|
|
3422
|
+
"Get per-rank resource statistics from /show/resource/statistics (same endpoint as kinetica_get_metrics). Without node_id: returns summary rows for all ranks. With node_id: returns detailed tier + resource-group breakdown for that rank. Per-tier fields: limit, used, free, percent_used, num_evictable_objs, num_unevictable_objs, plus stats: evictions, pins, unpins, watermark_cycles, allocs, reallocs, deallocs (RAM/VRAM tiers) or reads, writes, deletes (PERSIST/DISK tiers). Per-resource-group fields: name, thread_running_count, data (bytes). Rank 0 is the head node \u2014 only RAM tier (no PERSIST/DISK/VRAM), no resource groups.",
|
|
3423
|
+
{ node_id: import_zod16.z.string().optional() },
|
|
3424
|
+
async (args) => {
|
|
3425
|
+
const result = await nodeDetails(session2, args.node_id);
|
|
3426
|
+
return { content: [{ type: "text", text: applyOutputPipeline(result) }] };
|
|
3427
|
+
},
|
|
3428
|
+
{ annotations: { readOnly: true } }
|
|
3429
|
+
);
|
|
3430
|
+
}
|
|
3431
|
+
function makeGetLogsTool(session2) {
|
|
3432
|
+
return (0, import_claude_agent_sdk3.tool)(
|
|
3433
|
+
"kinetica_get_logs",
|
|
3434
|
+
"Retrieve application logs via /admin/show/logs. Sources: kinetica, rank, syslog, gadmin, reveal, workbench. Severity: DEBUG|INFO|WARN|ERROR|FATAL. Time: duration (1h, 30m) or start_time+end_time. WARNING: This endpoint is NOT available on Kinetica 7.2.x \u2014 it always returns a stub response. Use kinetica_execute_sql to query ki_catalog.ki_query_history (for query errors) or ki_catalog.ki_query_span_metrics_all (for operation-level events) instead.",
|
|
3435
|
+
GetLogsSchema.shape,
|
|
3436
|
+
async (args) => {
|
|
3437
|
+
const parsed = GetLogsSchema.parse(args);
|
|
3438
|
+
const result = await getLogs(session2, parsed);
|
|
3439
|
+
return { content: [{ type: "text", text: applyOutputPipeline(result) }] };
|
|
3440
|
+
},
|
|
3441
|
+
{ annotations: { readOnly: true } }
|
|
3442
|
+
);
|
|
3443
|
+
}
|
|
3444
|
+
function makeShowConfigurationTool(session2) {
|
|
3445
|
+
return (0, import_claude_agent_sdk3.tool)(
|
|
3446
|
+
"kinetica_show_configuration",
|
|
3447
|
+
"Retrieve the full gpudb.conf configuration file from the Kinetica host manager via /admin/show/configuration (port 9300). Returns the raw config_string in INI format with all sections and comments. Use this to inspect the complete server configuration for drift detection, misconfiguration diagnosis, or before proposing config changes via kinetica_alter_configuration. Requires host manager connectivity.",
|
|
3448
|
+
ShowConfigurationSchema.shape,
|
|
3449
|
+
async (args) => {
|
|
3450
|
+
const parsed = ShowConfigurationSchema.parse(args);
|
|
3451
|
+
const result = await showConfiguration(session2, parsed);
|
|
3452
|
+
return { content: [{ type: "text", text: applyOutputPipeline(result) }] };
|
|
3453
|
+
},
|
|
3454
|
+
{ annotations: { readOnly: true } }
|
|
3455
|
+
);
|
|
3456
|
+
}
|
|
3457
|
+
function makeGetSystemPropertiesTool(session2) {
|
|
3458
|
+
return (0, import_claude_agent_sdk3.tool)(
|
|
3459
|
+
"kinetica_get_system_properties",
|
|
3460
|
+
"Read Kinetica system configuration properties from /show/system/properties. Returns 260+ property rows as {property, value} pairs (all values are strings). Filter by category prefix (e.g., 'conf.tier', 'conf.sql', 'version') or key_pattern substring. Key property groups: conf.tier.* (RAM limits, watermarks, tier strategy), conf.sql.* (parallel_execution, planner timeout), conf.enable_* (authorization, HA, ML, text_search), version.* (gpudb_core_version, compute_engine). Omit both filters to get the full property_map.",
|
|
3461
|
+
GetSystemPropertiesSchema.shape,
|
|
3462
|
+
async (args) => {
|
|
3463
|
+
const parsed = GetSystemPropertiesSchema.parse(args);
|
|
3464
|
+
const result = await getSystemProperties(session2, parsed);
|
|
3465
|
+
return { content: [{ type: "text", text: applyOutputPipeline(result) }] };
|
|
3466
|
+
},
|
|
3467
|
+
{ annotations: { readOnly: true } }
|
|
3468
|
+
);
|
|
3469
|
+
}
|
|
3470
|
+
function makeExecuteSqlTool(session2, catalogSchemas) {
|
|
3471
|
+
return (0, import_claude_agent_sdk3.tool)(
|
|
3472
|
+
"kinetica_execute_sql",
|
|
3473
|
+
"Execute a read-only SQL query (SELECT, WITH, or EXPLAIN) against Kinetica. Key system tables: ki_catalog.ki_query_history (slow queries \u2014 columns: job_id, query_id, user_name, endpoint, execution_status, error_message, query_text, start_time, stop_time, sql_step_count, refresh_id, resource_group, source_ip), ki_catalog.ki_query_active_all (running queries \u2014 columns: job_id, query_id, user_name, resource_group, source_ip, endpoint, execution_status, error_message, start_time, query_text, sql_step_count, refresh_id, is_mh, is_perpetual, is_cancellable, is_using_timeout, source_rank), ki_catalog.ki_tiered_objects (per-object tier placement \u2014 size, id (string like @schema@oid[col][0] \u2014 NOT a numeric OID, do not join with ki_objects.oid), priority, tier, evictable, locked, pin_count, ram_evictions, persist_evictions, owner_resource_group, source_rank, outer_object; for per-table tier data prefer kinetica_resource_objects with table_names filter), ki_catalog.ki_obj_stat (table sizes \u2014 oid, schema_name, object_name, row_count, bytes_per_row, total_bytes), ki_catalog.ki_columns (column metadata), ki_catalog.ki_objects (object registry with obj_kind R=table/V=view). WARNING: ki_catalog.ki_tables and ki_catalog.ki_version do NOT exist in Kinetica 7.2.x \u2014 use ki_objects and /show/system/status instead.",
|
|
3474
|
+
ExecuteSqlSchema.shape,
|
|
3475
|
+
async (args) => {
|
|
3476
|
+
const result = await executeSql(session2, args.statement, args.limit);
|
|
3477
|
+
if (!result.ok) {
|
|
3478
|
+
const enrichedError = enrichSqlError(result.error, args.statement, catalogSchemas);
|
|
3479
|
+
const enrichedResult = enrichedError !== result.error ? { ok: false, status: result.status, error: enrichedError, raw: result.raw } : result;
|
|
3480
|
+
return { content: [{ type: "text", text: applyOutputPipeline(enrichedResult) }] };
|
|
3481
|
+
}
|
|
3482
|
+
return { content: [{ type: "text", text: applyOutputPipeline(result) }] };
|
|
3483
|
+
},
|
|
3484
|
+
{ annotations: { readOnly: true } }
|
|
3485
|
+
);
|
|
3486
|
+
}
|
|
3487
|
+
function makeExplainQueryTool(session2) {
|
|
3488
|
+
return (0, import_claude_agent_sdk3.tool)(
|
|
3489
|
+
"kinetica_explain_query",
|
|
3490
|
+
"Get the execution plan for a SQL statement. Pass the SELECT statement without the EXPLAIN keyword \u2014 it will be added automatically. Returns rows with columns: ID (step number), ENDPOINT (internal REST endpoint used, e.g., /get/records/bycolumn), INPUT_TABLES, OUTPUT_TABLE, DEPENDENCIES (step IDs this depends on; -1 means none). Use to understand which internal operations a query triggers and verify index usage.",
|
|
3491
|
+
ExplainQuerySchema.shape,
|
|
3492
|
+
async (args) => {
|
|
3493
|
+
const result = await explainQuery(session2, args.statement, args.limit);
|
|
3494
|
+
return { content: [{ type: "text", text: applyOutputPipeline(result) }] };
|
|
3495
|
+
},
|
|
3496
|
+
{ annotations: { readOnly: true } }
|
|
3497
|
+
);
|
|
3498
|
+
}
|
|
3499
|
+
function makeSystemTimingTool(session2) {
|
|
3500
|
+
return (0, import_claude_agent_sdk3.tool)(
|
|
3501
|
+
"kinetica_system_timing",
|
|
3502
|
+
"Show endpoint response timing statistics from /show/system/timing. Returns the last ~100 API calls as {endpoint, time_in_ms, job_id} rows with sub-millisecond precision. job_id='0' means synchronous execution at head node; non-zero means async. Typical baselines: /show/system/properties <4ms, /show/security <1ms, /execute/sql 14-1300ms, /admin/verifydb 500-4500ms. Use to identify slow API endpoints or confirm whether a specific call was abnormally slow.",
|
|
3503
|
+
{},
|
|
3504
|
+
async (_args) => {
|
|
3505
|
+
const result = await systemTiming(session2);
|
|
3506
|
+
return { content: [{ type: "text", text: applyOutputPipeline(result) }] };
|
|
3507
|
+
},
|
|
3508
|
+
{ annotations: { readOnly: true } }
|
|
3509
|
+
);
|
|
3510
|
+
}
|
|
3511
|
+
function makeResourceGroupsTool(session2) {
|
|
3512
|
+
return (0, import_claude_agent_sdk3.tool)(
|
|
3513
|
+
"kinetica_resource_groups",
|
|
3514
|
+
"List resource groups and their configuration from /show/resourcegroups. Returns {groups, rank_usage, info}. Groups include: name, RAM.max_memory, VRAM.GPU0.max_memory, max_cpu_concurrency, max_data, max_scheduling_priority (100=system, 50=default), max_tier_priority. Value '9223372036854775807' (Long.MAX_VALUE) means unlimited. Default groups are kinetica_system_resource_group (priority 100) and kinetica_default_resource_group (priority 50). rank_usage maps rank IDs to JSON-encoded per-group usage (thread_running_count, data bytes, RAM.used, VRAM.GPU0.used). Only worker ranks appear in rank_usage \u2014 rank 0 (head) has no entry. Set show_tier_usage=true for rank-level breakdown.",
|
|
3515
|
+
ResourceGroupsSchema.shape,
|
|
3516
|
+
async (args) => {
|
|
3517
|
+
const parsed = ResourceGroupsSchema.parse(args);
|
|
3518
|
+
const result = await getResourceGroups(session2, parsed);
|
|
3519
|
+
return { content: [{ type: "text", text: applyOutputPipeline(result) }] };
|
|
3520
|
+
},
|
|
3521
|
+
{ annotations: { readOnly: true } }
|
|
3522
|
+
);
|
|
3523
|
+
}
|
|
3524
|
+
function makeVerifyDbTool(session2) {
|
|
3525
|
+
return (0, import_claude_agent_sdk3.tool)(
|
|
3526
|
+
"kinetica_verify_db",
|
|
3527
|
+
"Run a read-only database integrity verification via /admin/verifydb. Checks for null values, persistence issues, and rank0 consistency. Always runs in concurrent_safe mode. Returns {verified_ok: boolean, error_list: [], orphaned_tables_total_size: number}. Healthy: verified_ok=true, empty error_list, orphaned_tables_total_size of -1 (not checked) or 0. WARNING: This is the slowest diagnostic tool \u2014 typically takes 500ms-4500ms. Use sparingly and only when data integrity issues are suspected.",
|
|
3528
|
+
VerifyDbSchema.shape,
|
|
3529
|
+
async (args) => {
|
|
3530
|
+
const parsed = VerifyDbSchema.parse(args);
|
|
3531
|
+
const result = await verifyDb(session2, parsed);
|
|
3532
|
+
return { content: [{ type: "text", text: applyOutputPipeline(result) }] };
|
|
3533
|
+
},
|
|
3534
|
+
{ annotations: { readOnly: true } }
|
|
3535
|
+
);
|
|
3536
|
+
}
|
|
3537
|
+
function makeShowSecurityTool(session2) {
|
|
3538
|
+
return (0, import_claude_agent_sdk3.tool)(
|
|
3539
|
+
"kinetica_show_security",
|
|
3540
|
+
"Show security configuration from /show/security. Returns {types, roles, permissions, resource_groups, info}. When enable_authorization=false (check permissions[''].enable_authorization), types/roles/resource_groups are empty objects \u2014 the tool provides minimal data. When authorization is enabled: types maps usernames to 'internal_user'/'external_user', roles maps role names to member arrays, permissions maps users to permission arrays, resource_groups maps users to group names. Use to audit access control or diagnose authorization failures.",
|
|
3541
|
+
ShowSecuritySchema.shape,
|
|
3542
|
+
async (args) => {
|
|
3543
|
+
const parsed = ShowSecuritySchema.parse(args);
|
|
3544
|
+
const result = await showSecurity(session2, parsed);
|
|
3545
|
+
return { content: [{ type: "text", text: applyOutputPipeline(result) }] };
|
|
3546
|
+
},
|
|
3547
|
+
{ annotations: { readOnly: true } }
|
|
3548
|
+
);
|
|
3549
|
+
}
|
|
3550
|
+
function makeShowTableTool(session2) {
|
|
3551
|
+
return (0, import_claude_agent_sdk3.tool)(
|
|
3552
|
+
"kinetica_show_table",
|
|
3553
|
+
"Show table metadata from /show/table. When a specific table_name is provided: returns table metadata with Kinetica-native column types, per-column properties (DICT, TEXT_SEARCH, COMPRESS, etc.), and index definitions from ki_catalog.ki_indexes (index_type, index_columns) \u2014 this is the preferred method for full table inspection. When table_name is omitted or empty: returns schema-level (collection) entries with sizes, but the processed output may be empty \u2014 use kinetica_execute_sql with 'SELECT * FROM ki_catalog.ki_objects WHERE obj_kind = ''R'' ORDER BY schema_name' for reliable table listing instead.",
|
|
3554
|
+
ShowTableSchema.shape,
|
|
3555
|
+
async (args) => {
|
|
3556
|
+
const parsed = ShowTableSchema.parse(args);
|
|
3557
|
+
const result = await showTable(session2, parsed);
|
|
3558
|
+
return { content: [{ type: "text", text: applyOutputPipeline(result) }] };
|
|
3559
|
+
},
|
|
3560
|
+
{ annotations: { readOnly: true } }
|
|
3561
|
+
);
|
|
3562
|
+
}
|
|
3563
|
+
function makeResourceObjectsTool(session2) {
|
|
3564
|
+
return (0, import_claude_agent_sdk3.tool)(
|
|
3565
|
+
"kinetica_resource_objects",
|
|
3566
|
+
`Show per-rank resource tier usage from /show/resource/objects. Returns {rank_objects: {rank_id: JSON_string_with_objects_array}, info}. Each object has: id (naming convention: @table@oid[column][chunk] for data, AttrIndex[...] for indexes, PKIndex_... for PK hashes), size (bytes), priority (1=system, 5=user, 9=temp), tier ('RAM' or 'PERSIST'), evictable (boolean), locked (boolean), pin_count, ram_evictions, persist_evictions, owner_resource_group. The rank_objects JSON is {"objects": [...]} \u2014 an array nested under an 'objects' key. Only worker ranks have data \u2014 rank 0 (head) has no resource objects. Healthy: zero evictions, zero pin_count at rest.`,
|
|
3567
|
+
ResourceObjectsSchema.shape,
|
|
3568
|
+
async (args) => {
|
|
3569
|
+
const parsed = ResourceObjectsSchema.parse(args);
|
|
3570
|
+
const result = await getResourceObjects(session2, parsed);
|
|
3571
|
+
return { content: [{ type: "text", text: applyOutputPipeline(result) }] };
|
|
3572
|
+
},
|
|
3573
|
+
{ annotations: { readOnly: true } }
|
|
3574
|
+
);
|
|
3575
|
+
}
|
|
3576
|
+
function makeHostManagerStatusTool(session2) {
|
|
3577
|
+
return (0, import_claude_agent_sdk3.tool)(
|
|
3578
|
+
"kinetica_host_manager_status",
|
|
3579
|
+
"Query the Kinetica host manager root endpoint (port 9300) for cluster-wide status. Returns a flat key-value map including: version, hostname, system_mode ('run'/'stop'), system_status ('running'/'stopped'), system_idle_time (seconds), cluster_leader (IP), cluster_operation ('none'/'rebalance'/etc.), license_type/status/expiration, per-host and per-rank mode/status/pid, and service statuses (ml, httpd, query_planner, reveal, stats, graph, text). Healthy baseline: system_mode='run', system_status='running', all rankN_status='running', license_status='ok'. Does NOT require Kinetica DB authentication \u2014 queries the host manager service directly.",
|
|
3580
|
+
{},
|
|
3581
|
+
async (_args) => {
|
|
3582
|
+
const result = await hostManagerStatus(session2);
|
|
3583
|
+
return { content: [{ type: "text", text: applyOutputPipeline(result) }] };
|
|
3584
|
+
},
|
|
3585
|
+
{ annotations: { readOnly: true } }
|
|
3586
|
+
);
|
|
3587
|
+
}
|
|
3588
|
+
function makeDiagnosticTools(session2, catalogSchemas) {
|
|
3589
|
+
return [
|
|
3590
|
+
makeHealthCheckTool(session2),
|
|
3591
|
+
makeGetMetricsTool(session2),
|
|
3592
|
+
makeClusterStatusTool(session2),
|
|
3593
|
+
makeNodeDetailsTool(session2),
|
|
3594
|
+
makeGetLogsTool(session2),
|
|
3595
|
+
makeShowConfigurationTool(session2),
|
|
3596
|
+
makeGetSystemPropertiesTool(session2),
|
|
3597
|
+
makeExecuteSqlTool(session2, catalogSchemas),
|
|
3598
|
+
makeExplainQueryTool(session2),
|
|
3599
|
+
makeSystemTimingTool(session2),
|
|
3600
|
+
makeResourceGroupsTool(session2),
|
|
3601
|
+
makeVerifyDbTool(session2),
|
|
3602
|
+
makeShowSecurityTool(session2),
|
|
3603
|
+
makeShowTableTool(session2),
|
|
3604
|
+
makeResourceObjectsTool(session2),
|
|
3605
|
+
makeHostManagerStatusTool(session2)
|
|
3606
|
+
];
|
|
3607
|
+
}
|
|
3608
|
+
function makeMutationTools(session2) {
|
|
3609
|
+
return [
|
|
3610
|
+
makeAlterSystemPropertiesTool(session2),
|
|
3611
|
+
makeExecuteMutationSqlTool(session2),
|
|
3612
|
+
makeAdminRebalanceTool(session2),
|
|
3613
|
+
makeAlterConfigurationTool(session2)
|
|
3614
|
+
];
|
|
3615
|
+
}
|
|
3616
|
+
function makeAlterTableColumnsToolWithDeps(session2) {
|
|
3617
|
+
return makeAlterTableColumnsTool(session2, {
|
|
3618
|
+
applyOutputPipeline,
|
|
3619
|
+
logMutationAudit
|
|
3620
|
+
});
|
|
3621
|
+
}
|
|
3622
|
+
|
|
3623
|
+
// src/tools/catalog.ts
|
|
3624
|
+
var TOOL_CATALOG = {
|
|
3625
|
+
kinetica_health_check: {
|
|
3626
|
+
reveals: "System health status, version info",
|
|
3627
|
+
whenToUse: "Every investigation (Round 1)"
|
|
3628
|
+
},
|
|
3629
|
+
kinetica_host_manager_status: {
|
|
3630
|
+
reveals: "Host manager overview: version, license, system mode, per-rank process status/PIDs, service statuses (ML, query planner, reveal, graph, text). No auth required.",
|
|
3631
|
+
whenToUse: "Every investigation (Round 1)"
|
|
3632
|
+
},
|
|
3633
|
+
kinetica_get_metrics: {
|
|
3634
|
+
reveals: "CPU, GPU, memory usage per rank",
|
|
3635
|
+
whenToUse: "Performance issues, OOM, high load"
|
|
3636
|
+
},
|
|
3637
|
+
kinetica_cluster_status: {
|
|
3638
|
+
reveals: "Rebalance/add/remove ops, shard mapping, alerts, jobs",
|
|
3639
|
+
whenToUse: "Cluster instability, replication issues"
|
|
3640
|
+
},
|
|
3641
|
+
kinetica_node_details: {
|
|
3642
|
+
reveals: "Per-node resource statistics",
|
|
3643
|
+
whenToUse: "Identifying which node is under pressure"
|
|
3644
|
+
},
|
|
3645
|
+
kinetica_get_logs: {
|
|
3646
|
+
reveals: "Application errors, warnings, system events",
|
|
3647
|
+
whenToUse: "Any error investigation (Round 1)"
|
|
3648
|
+
},
|
|
3649
|
+
kinetica_show_configuration: {
|
|
3650
|
+
reveals: "Full gpudb.conf from host manager (port 9300)",
|
|
3651
|
+
whenToUse: "Config drift, misconfiguration, complete config inspection"
|
|
3652
|
+
},
|
|
3653
|
+
kinetica_get_system_properties: {
|
|
3654
|
+
reveals: "Runtime config properties (260+ rows); filter by category or key pattern",
|
|
3655
|
+
whenToUse: "Read current property values before ALTER, version lookup, feature-flag audit"
|
|
3656
|
+
},
|
|
3657
|
+
kinetica_execute_sql: {
|
|
3658
|
+
reveals: "Query history, active queries, table stats, schema",
|
|
3659
|
+
whenToUse: "Slow queries, contention, data issues"
|
|
3660
|
+
},
|
|
3661
|
+
kinetica_explain_query: {
|
|
3662
|
+
reveals: "Query execution plan with operator tree",
|
|
3663
|
+
whenToUse: "Query performance optimization"
|
|
3664
|
+
},
|
|
3665
|
+
kinetica_system_timing: {
|
|
3666
|
+
reveals: "Per-endpoint response times, slow API detection",
|
|
3667
|
+
whenToUse: "Performance issues, slow endpoint response"
|
|
3668
|
+
},
|
|
3669
|
+
kinetica_resource_groups: {
|
|
3670
|
+
reveals: "Resource group config, tier usage per rank",
|
|
3671
|
+
whenToUse: "Resource allocation, tier capacity issues"
|
|
3672
|
+
},
|
|
3673
|
+
kinetica_verify_db: {
|
|
3674
|
+
reveals: "Database integrity: nulls, persistence, rank0 (healthy: verified_ok=true, orphaned_size=-1)",
|
|
3675
|
+
whenToUse: "Data corruption, integrity verification"
|
|
3676
|
+
},
|
|
3677
|
+
kinetica_show_security: {
|
|
3678
|
+
reveals: "User types, roles, permissions, resource groups",
|
|
3679
|
+
whenToUse: "Access control audit, authorization failures"
|
|
3680
|
+
},
|
|
3681
|
+
kinetica_show_table: {
|
|
3682
|
+
reveals: "Table names, sizes, properties, column types",
|
|
3683
|
+
whenToUse: "Schema inspection, column type inspection, table size analysis"
|
|
3684
|
+
},
|
|
3685
|
+
kinetica_resource_objects: {
|
|
3686
|
+
reveals: "Per-rank object placement across storage tiers",
|
|
3687
|
+
whenToUse: "Tier capacity, eviction, data placement"
|
|
3688
|
+
},
|
|
3689
|
+
kinetica_alter_system_properties: {
|
|
3690
|
+
reveals: "Apply runtime config changes (before/after/verify)",
|
|
3691
|
+
whenToUse: "Config drift remediation, thread pool tuning"
|
|
3692
|
+
},
|
|
3693
|
+
kinetica_execute_mutation_sql: {
|
|
3694
|
+
reveals: "Execute approved DDL/DML (CREATE INDEX, ALTER TABLE, etc.) \u2014 ANALYZE TABLE is NOT supported by Kinetica",
|
|
3695
|
+
whenToUse: "Query optimization, index creation"
|
|
3696
|
+
},
|
|
3697
|
+
kinetica_admin_rebalance: {
|
|
3698
|
+
reveals: "Trigger shard rebalancing (requires 2+ worker ranks)",
|
|
3699
|
+
whenToUse: "Shard imbalance, uneven data distribution"
|
|
3700
|
+
},
|
|
3701
|
+
kinetica_alter_configuration: {
|
|
3702
|
+
reveals: "Replace gpudb.conf on host manager (before/after/verify)",
|
|
3703
|
+
whenToUse: "Config remediation, targeted config file changes"
|
|
3704
|
+
},
|
|
3705
|
+
kinetica_alter_table_columns: {
|
|
3706
|
+
reveals: "Batch column type/property changes (DICT, TEXT_SEARCH, etc.) via checklist",
|
|
3707
|
+
whenToUse: "When recommending 2+ column changes on one table"
|
|
3708
|
+
}
|
|
3709
|
+
};
|
|
3710
|
+
function buildEvidenceChecklist() {
|
|
3711
|
+
const ordered = [
|
|
3712
|
+
...DIAGNOSTIC_TOOL_NAMES,
|
|
3713
|
+
...MUTATION_TOOL_NAMES,
|
|
3714
|
+
ALTER_TABLE_COLUMNS_TOOL_NAME
|
|
3715
|
+
];
|
|
3716
|
+
const rows = ordered.map((name) => {
|
|
3717
|
+
const entry = TOOL_CATALOG[name];
|
|
3718
|
+
return `| ${name} | ${entry.reveals} | ${entry.whenToUse} |`;
|
|
3719
|
+
});
|
|
3720
|
+
return [
|
|
3721
|
+
"| Tool | What it reveals | When to use |",
|
|
3722
|
+
"|------|----------------|-------------|",
|
|
3723
|
+
...rows
|
|
3724
|
+
].join("\n");
|
|
3725
|
+
}
|
|
3726
|
+
|
|
3727
|
+
// src/agent/report-template.ts
|
|
3728
|
+
var import_node_fs2 = require("fs");
|
|
3729
|
+
var import_node_path3 = require("path");
|
|
3730
|
+
|
|
3731
|
+
// src/agent/load-playbooks.ts
|
|
3732
|
+
var import_promises2 = require("fs/promises");
|
|
3733
|
+
var import_node_path2 = require("path");
|
|
3734
|
+
var import_node_fs = require("fs");
|
|
3735
|
+
function findPackageRoot(startDir) {
|
|
3736
|
+
let dir = startDir;
|
|
3737
|
+
while (dir !== (0, import_node_path2.dirname)(dir)) {
|
|
3738
|
+
if ((0, import_node_fs.existsSync)((0, import_node_path2.join)(dir, "package.json"))) return dir;
|
|
3739
|
+
dir = (0, import_node_path2.dirname)(dir);
|
|
3740
|
+
}
|
|
3741
|
+
return startDir;
|
|
3742
|
+
}
|
|
3743
|
+
function parseFrontmatter(raw) {
|
|
3744
|
+
const match = /^---\r?\n([\s\S]*?)\r?\n---/.exec(raw);
|
|
3745
|
+
if (!match) return null;
|
|
3746
|
+
const yamlBlock = match[1];
|
|
3747
|
+
const fields = {};
|
|
3748
|
+
for (const line of yamlBlock.split(/\r?\n/)) {
|
|
3749
|
+
const colonIdx = line.indexOf(":");
|
|
3750
|
+
if (colonIdx === -1) continue;
|
|
3751
|
+
const key = line.slice(0, colonIdx).trim();
|
|
3752
|
+
const value = line.slice(colonIdx + 1).trim();
|
|
3753
|
+
if (key) fields[key] = value;
|
|
3754
|
+
}
|
|
3755
|
+
const title = fields.title;
|
|
3756
|
+
if (!title) return null;
|
|
3757
|
+
const keywordsRaw = fields.keywords ?? "";
|
|
3758
|
+
const keywords = keywordsRaw.startsWith("[") && keywordsRaw.endsWith("]") ? keywordsRaw.slice(1, -1).split(",").map((s) => s.trim()).filter(Boolean) : [];
|
|
3759
|
+
return {
|
|
3760
|
+
title,
|
|
3761
|
+
category: fields.category ?? "general",
|
|
3762
|
+
severity: fields.severity ?? "info",
|
|
3763
|
+
keywords
|
|
3764
|
+
};
|
|
3765
|
+
}
|
|
3766
|
+
function extractBody(raw) {
|
|
3767
|
+
const match = /^---\r?\n[\s\S]*?\r?\n---\r?\n([\s\S]*)$/.exec(raw);
|
|
3768
|
+
return match ? match[1].trim() : raw.trim();
|
|
3769
|
+
}
|
|
3770
|
+
async function loadPlaybooks(playbooksDir) {
|
|
3771
|
+
try {
|
|
3772
|
+
const dir = playbooksDir ?? (0, import_node_path2.join)(findPackageRoot(__dirname), "knowledge", "playbooks");
|
|
3773
|
+
if (!(0, import_node_fs.existsSync)(dir)) return [];
|
|
3774
|
+
const files = await (0, import_promises2.readdir)(dir);
|
|
3775
|
+
const mdFiles = files.filter((f) => f.endsWith(".md")).sort();
|
|
3776
|
+
const playbooks = [];
|
|
3777
|
+
for (const file of mdFiles) {
|
|
3778
|
+
const raw = await (0, import_promises2.readFile)((0, import_node_path2.join)(dir, file), "utf-8");
|
|
3779
|
+
const frontmatter = parseFrontmatter(raw);
|
|
3780
|
+
if (!frontmatter) continue;
|
|
3781
|
+
playbooks.push({
|
|
3782
|
+
...frontmatter,
|
|
3783
|
+
body: extractBody(raw),
|
|
3784
|
+
filename: file
|
|
3785
|
+
});
|
|
3786
|
+
}
|
|
3787
|
+
return playbooks;
|
|
3788
|
+
} catch {
|
|
3789
|
+
return [];
|
|
3790
|
+
}
|
|
3791
|
+
}
|
|
3792
|
+
|
|
3793
|
+
// src/agent/report-template.ts
|
|
3794
|
+
function loadReportTemplateSync() {
|
|
3795
|
+
try {
|
|
3796
|
+
const root = findPackageRoot(__dirname);
|
|
3797
|
+
const path2 = (0, import_node_path3.join)(root, "knowledge", "templates", "report.md");
|
|
3798
|
+
return (0, import_node_fs2.readFileSync)(path2, "utf-8");
|
|
3799
|
+
} catch (err) {
|
|
3800
|
+
console.warn(`[report-template] failed to load knowledge/templates/report.md: ${String(err)}`);
|
|
3801
|
+
return "";
|
|
3802
|
+
}
|
|
3803
|
+
}
|
|
3804
|
+
var REPORT_TEMPLATE = loadReportTemplateSync();
|
|
3805
|
+
|
|
3806
|
+
// src/agent/system-prompt.ts
|
|
3807
|
+
function buildColumnDirective(schemas) {
|
|
3808
|
+
const lines = [
|
|
3809
|
+
"> **Verified Column Names** \u2014 always use these exact columns in SQL queries:"
|
|
3810
|
+
];
|
|
3811
|
+
for (const [table, columns] of schemas.tables) {
|
|
3812
|
+
lines.push(`> - **${table}**: ${columns.join(", ")}`);
|
|
3813
|
+
}
|
|
3814
|
+
return lines.join("\n") + "\n\n";
|
|
3815
|
+
}
|
|
3816
|
+
function buildDiagnosticSqlSection(schemas) {
|
|
3817
|
+
const getColumns = (table) => schemas?.tables.get(table);
|
|
3818
|
+
const directive = schemas ? buildColumnDirective(schemas) : "";
|
|
3819
|
+
const sectionSqls = /* @__PURE__ */ new Map();
|
|
3820
|
+
for (const entry of BUILDER_REGISTRY) {
|
|
3821
|
+
const cols = getColumns(entry.table);
|
|
3822
|
+
const sql = cols ? entry.build(cols) : entry.fallback;
|
|
3823
|
+
const existing = sectionSqls.get(entry.section) ?? [];
|
|
3824
|
+
sectionSqls.set(entry.section, [...existing, sql]);
|
|
3825
|
+
}
|
|
3826
|
+
let result = directive;
|
|
3827
|
+
for (const [heading, sqls] of sectionSqls) {
|
|
3828
|
+
result += `**${heading}:**
|
|
3829
|
+
\`\`\`sql
|
|
3830
|
+
${sqls.join("\n\n")}
|
|
3831
|
+
\`\`\`
|
|
3832
|
+
|
|
3833
|
+
`;
|
|
3834
|
+
}
|
|
3835
|
+
return result.trimEnd();
|
|
3836
|
+
}
|
|
3837
|
+
function buildFailurePatternsSection(playbooks) {
|
|
3838
|
+
if (!playbooks || playbooks.length === 0) return "";
|
|
3839
|
+
const entries = playbooks.map((p) => `**${p.title}:**
|
|
3840
|
+
|
|
3841
|
+
${p.body}`).join("\n\n");
|
|
3842
|
+
return `### Common Failure Patterns
|
|
3843
|
+
|
|
3844
|
+
${entries}`;
|
|
3845
|
+
}
|
|
3846
|
+
function buildReferenceSection(references) {
|
|
3847
|
+
if (!references || references.length === 0) return "";
|
|
3848
|
+
const entries = references.map((r) => `**${r.title}:**
|
|
3849
|
+
|
|
3850
|
+
${r.body}`).join("\n\n");
|
|
3851
|
+
return `### Reference Knowledge
|
|
3852
|
+
|
|
3853
|
+
${entries}`;
|
|
3854
|
+
}
|
|
3855
|
+
function buildSystemPrompt(kineticaVersion, catalogSchemas, playbooks, references, degraded) {
|
|
3856
|
+
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.";
|
|
3857
|
+
const t = "`";
|
|
3858
|
+
const degradedSection = degraded ? `
|
|
3859
|
+
---
|
|
3860
|
+
|
|
3861
|
+
## DEGRADED MODE \u2014 DB Engine Unreachable
|
|
3862
|
+
|
|
3863
|
+
**CRITICAL:** The Kinetica DB engine on port 9191 is DOWN. You are connected to the host manager (port 9300) only.
|
|
3864
|
+
|
|
3865
|
+
### What works:
|
|
3866
|
+
- ${t}kinetica_host_manager_status${t} \u2014 **USE THIS FIRST** for every investigation. Returns version, license info, system mode, per-rank process status/PIDs, and service statuses (ML, query planner, reveal, graph, text).
|
|
3867
|
+
|
|
3868
|
+
### What will fail:
|
|
3869
|
+
- ALL other diagnostic tools (${t}kinetica_health_check${t}, ${t}kinetica_get_metrics${t}, ${t}kinetica_cluster_status${t}, ${t}kinetica_execute_sql${t}, etc.) \u2014 they target port 9191 and will return errors.
|
|
3870
|
+
- SQL queries against ki_catalog system tables \u2014 the DB engine is required.
|
|
3871
|
+
- Mutation tools \u2014 cannot modify a downed system.
|
|
3872
|
+
|
|
3873
|
+
### Investigation strategy in degraded mode:
|
|
3874
|
+
1. Call ${t}kinetica_host_manager_status${t} to gather all available data
|
|
3875
|
+
2. Analyze rank process statuses \u2014 look for stopped/crashed ranks and their PIDs
|
|
3876
|
+
3. Check ${t}system_mode${t} and ${t}system_status${t} for cluster state
|
|
3877
|
+
4. Check ${t}license_status${t} and ${t}license_expiration${t} for license issues
|
|
3878
|
+
5. Check service statuses: ${t}ml_status${t}, ${t}query_planner_status${t}, ${t}reveal_status${t}, ${t}graph0_status${t}, ${t}text0_status${t}
|
|
3879
|
+
6. Report findings and clearly note that full diagnostics require the DB engine to be running
|
|
3880
|
+
7. Recommend the operator check: process logs (${t}/opt/gpudb/core/logs/${t}), ${t}gadmin status${t}, disk space, and network connectivity
|
|
3881
|
+
|
|
3882
|
+
### Report adjustments for degraded mode:
|
|
3883
|
+
- Evidence Gaps MUST include: "DB engine unreachable (port 9191) \u2014 all DB-dependent diagnostic tools unavailable"
|
|
3884
|
+
- Remediation should prioritize bringing the DB engine back online
|
|
3885
|
+
- Do NOT attempt Round 4 (mutations) or Round 5 (verification) \u2014 the DB engine must be running first
|
|
3886
|
+
|
|
3887
|
+
` : "";
|
|
3888
|
+
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.
|
|
3889
|
+
|
|
3890
|
+
${versionSection}
|
|
3891
|
+
${degradedSection}
|
|
3892
|
+
---
|
|
3893
|
+
|
|
3894
|
+
## Role and Mandate
|
|
3895
|
+
|
|
3896
|
+
You are the operator's expert assistant. When an operator describes a problem, you take ownership of the investigation. You use diagnostic tools to gather evidence, reason over that evidence to identify the most likely root cause, and deliver a clear, specific, actionable report. You never give vague or generic advice.
|
|
3897
|
+
|
|
3898
|
+
---
|
|
3899
|
+
|
|
3900
|
+
## Investigation Protocol
|
|
3901
|
+
|
|
3902
|
+
### Pre-Investigation: Announce Your Plan
|
|
3903
|
+
|
|
3904
|
+
Before gathering any evidence, announce a brief 2-3 line investigation plan:
|
|
3905
|
+
1. Restate the issue in your own words
|
|
3906
|
+
2. List the primary tools you will check first
|
|
3907
|
+
3. Begin immediately \u2014 do not wait for user confirmation
|
|
3908
|
+
|
|
3909
|
+
### 5-Round Investigation Protocol
|
|
3910
|
+
|
|
3911
|
+
You have up to 5 rounds of tool calls:
|
|
3912
|
+
|
|
3913
|
+
**Round 1 \u2014 Initial Sweep:**
|
|
3914
|
+
Run a broad baseline sweep. Use parallel tool calls where possible \u2014 issue health + metrics + logs simultaneously to maximize efficiency. Goal: establish baseline and surface obvious anomalies.
|
|
3915
|
+
|
|
3916
|
+
Recommended Round 1 tools (in parallel):
|
|
3917
|
+
- ${t}kinetica_health_check${t} \u2014 system health status
|
|
3918
|
+
- ${t}kinetica_host_manager_status${t} \u2014 host manager cluster overview (version, license, per-rank/service status)
|
|
3919
|
+
- ${t}kinetica_get_metrics${t} \u2014 CPU/GPU/memory resource usage
|
|
3920
|
+
- ${t}kinetica_get_logs${t} (severity: ERROR, duration: 1h) \u2014 recent errors
|
|
3921
|
+
|
|
3922
|
+
**Round 2 \u2014 Targeted Drill-Down:**
|
|
3923
|
+
Based on Round 1 findings, perform targeted drill-down on specific hypotheses. Use additional tools as needed:
|
|
3924
|
+
- ${t}kinetica_cluster_status${t} \u2014 cluster operations, shard mapping, alerts
|
|
3925
|
+
- ${t}kinetica_node_details${t} \u2014 per-node resource breakdown
|
|
3926
|
+
- ${t}kinetica_execute_sql${t} \u2014 query history, active queries, table stats
|
|
3927
|
+
- ${t}kinetica_show_configuration${t} \u2014 full gpudb.conf from host manager
|
|
3928
|
+
- ${t}kinetica_system_timing${t} \u2014 endpoint timing, slow API detection
|
|
3929
|
+
- ${t}kinetica_show_table${t} \u2014 table sizes, properties, column types
|
|
3930
|
+
|
|
3931
|
+
**Round 3 \u2014 Confirmation Pass:**
|
|
3932
|
+
Confirm your primary hypothesis with additional evidence. Use ${t}kinetica_explain_query${t} for query plan issues. Run targeted SQL queries to validate root cause. Use ${t}kinetica_verify_db${t} when suspecting data integrity issues. Collect any final missing evidence.
|
|
3933
|
+
|
|
3934
|
+
After Round 3, you MUST write the report \u2014 even if uncertainty remains.
|
|
3935
|
+
|
|
3936
|
+
### Round 4 -- Mutation Proposal
|
|
3937
|
+
|
|
3938
|
+
When diagnostic evidence supports a specific remediation:
|
|
3939
|
+
1. Explain your reasoning in the tool call (the approval panel will display it)
|
|
3940
|
+
2. Call the appropriate mutation tool:
|
|
3941
|
+
- ${t}kinetica_alter_table_columns${t} -- for batching 2+ column type/property changes on a SINGLE table
|
|
3942
|
+
into one efficient ALTER TABLE statement. Provide column_name, new_definition (full type def), and
|
|
3943
|
+
description for each column. The operator selects which columns via interactive checklist.
|
|
3944
|
+
- ${t}kinetica_alter_system_properties${t} -- for runtime property changes
|
|
3945
|
+
- ${t}kinetica_execute_mutation_sql${t} -- for index creation, single column change, or other DDL (note: Kinetica does NOT support ANALYZE TABLE)
|
|
3946
|
+
- ${t}kinetica_admin_rebalance${t} -- for shard distribution issues
|
|
3947
|
+
- ${t}kinetica_alter_configuration${t} -- for gpudb.conf file changes (via host manager)
|
|
3948
|
+
3. If the user denies: acknowledge, note in report as denied, move to next recommendation
|
|
3949
|
+
4. If the tool fails after approval: note the failure in report, explain the error, suggest alternatives
|
|
3950
|
+
|
|
3951
|
+
**When to use ${t}kinetica_alter_table_columns${t} vs ${t}kinetica_execute_mutation_sql${t}:**
|
|
3952
|
+
- 2+ column changes on the same table \u2192 use ${t}kinetica_alter_table_columns${t} (single efficient statement)
|
|
3953
|
+
- 1 column change, or non-column DDL (e.g., CREATE INDEX) \u2192 use ${t}kinetica_execute_mutation_sql${t}. Do NOT call ANALYZE TABLE \u2014 it is not supported by Kinetica and there is no equivalent "refresh stats" command.
|
|
3954
|
+
|
|
3955
|
+
### Round 5 -- Post-Mutation Verification
|
|
3956
|
+
|
|
3957
|
+
After all approved mutations:
|
|
3958
|
+
1. Re-run relevant diagnostic tools (${t}kinetica_get_system_properties${t}, ${t}kinetica_cluster_status${t}, ${t}kinetica_get_metrics${t}, ${t}kinetica_host_manager_status${t})
|
|
3959
|
+
2. Confirm changes took effect (compare before/after values from tool results)
|
|
3960
|
+
3. Check for any new issues introduced by the mutations
|
|
3961
|
+
4. Proceed to report generation
|
|
3962
|
+
|
|
3963
|
+
### Parallel Tool Calls
|
|
3964
|
+
|
|
3965
|
+
Issue independent tool calls simultaneously when possible. For example:
|
|
3966
|
+
- In Round 1: issue ${t}kinetica_health_check${t}, ${t}kinetica_host_manager_status${t}, ${t}kinetica_get_metrics${t}, and ${t}kinetica_get_logs${t} together
|
|
3967
|
+
- In Round 2: issue ${t}kinetica_cluster_status${t} and ${t}kinetica_node_details${t} together
|
|
3968
|
+
|
|
3969
|
+
---
|
|
3970
|
+
|
|
3971
|
+
## Evidence Checklist \u2014 Diagnostic Tools
|
|
3972
|
+
|
|
3973
|
+
Each tool provides specific diagnostic value. Use them strategically:
|
|
3974
|
+
|
|
3975
|
+
${buildEvidenceChecklist()}
|
|
3976
|
+
|
|
3977
|
+
---
|
|
3978
|
+
|
|
3979
|
+
## Kinetica Domain Knowledge
|
|
3980
|
+
|
|
3981
|
+
### System Tables for Diagnostics
|
|
3982
|
+
|
|
3983
|
+
Use kinetica_execute_sql to query these system tables:
|
|
3984
|
+
|
|
3985
|
+
` + buildDiagnosticSqlSection(catalogSchemas) + `
|
|
3986
|
+
|
|
3987
|
+
### Tables That May Be Empty (Not an Error)
|
|
3988
|
+
|
|
3989
|
+
These tables return 0 rows when the feature is not configured \u2014 this is normal:
|
|
3990
|
+
- ${t}ki_catalog.ki_periodic_objects${t} \u2014 no scheduled/periodic refresh objects
|
|
3991
|
+
- ${t}ki_catalog.ki_backup_history${t} \u2014 no backups have been performed
|
|
3992
|
+
- ${t}ki_catalog.ki_kafka_lag_info${t} \u2014 no Kafka streaming ingestion configured
|
|
3993
|
+
|
|
3994
|
+
### Response Data Interpretation
|
|
3995
|
+
|
|
3996
|
+
Tool responses have these consistent patterns:
|
|
3997
|
+
- **All values are strings** \u2014 numeric fields like ram_used, sizes, counts are returned as strings, not numbers
|
|
3998
|
+
- **Empty string vs "0":** Empty string ${t}""${t} means a tier/feature is not configured; ${t}"0"${t} means configured but currently unused
|
|
3999
|
+
- **JSON-encoded strings:** Health check status values, resource group rank_usage, and resource object rank_objects contain JSON as strings \u2014 parse mentally to extract nested fields
|
|
4000
|
+
- **Timestamps:** SQL queries return epoch milliseconds (e.g., 1774153326000); compute duration as ${t}(stop_time - start_time)${t} in milliseconds
|
|
4001
|
+
- **Long.MAX_VALUE:** ${t}9223372036854775807${t} in resource group limits means unlimited
|
|
4002
|
+
|
|
4003
|
+
### Column Type Inspection
|
|
4004
|
+
|
|
4005
|
+
**Preferred method:** Use ${t}kinetica_show_table${t} with a specific ${t}table_name${t} to get
|
|
4006
|
+
Kinetica-native column types and per-column properties (DICT, TEXT_SEARCH, COMPRESS, etc.).
|
|
4007
|
+
|
|
4008
|
+
**Avoid for types:** ${t}ki_catalog.ki_columns${t} returns SQL-standard types (e.g., ${t}character(64)${t})
|
|
4009
|
+
not Kinetica-native types. Use ${t}ki_columns${t} only for structural metadata not available from
|
|
4010
|
+
${t}kinetica_show_table${t} (e.g., ${t}is_shard_key${t}, ${t}is_primary_key${t}, disk compression stats).
|
|
4011
|
+
|
|
4012
|
+
${buildFailurePatternsSection(playbooks)}
|
|
4013
|
+
|
|
4014
|
+
${buildReferenceSection(references)}
|
|
4015
|
+
|
|
4016
|
+
---
|
|
4017
|
+
|
|
4018
|
+
## Analysis Instructions
|
|
4019
|
+
|
|
4020
|
+
### Commit to the Best Hypothesis
|
|
4021
|
+
|
|
4022
|
+
After gathering evidence, you MUST name specific root causes. No generic hedging.
|
|
4023
|
+
|
|
4024
|
+
**DO:**
|
|
4025
|
+
- "Root cause: GPU OOM due to query materializing 45GB result set in VRAM on rank 3"
|
|
4026
|
+
- "Root cause: Stale rank \u2014 rank 2 failed to rejoin cluster after network event at 14:23 UTC"
|
|
4027
|
+
- "If uncertain, rank top 2-3 hypotheses by likelihood: Primary (70%): X; Secondary (25%): Y; ranked by likelihood"
|
|
4028
|
+
|
|
4029
|
+
**DO NOT:**
|
|
4030
|
+
- "There could be various reasons for this issue..."
|
|
4031
|
+
- "It might be a performance problem or possibly a configuration issue..."
|
|
4032
|
+
- "Further investigation may be needed..."
|
|
4033
|
+
|
|
4034
|
+
This is not about hedging on uncertainty \u2014 you can say "I don't have enough evidence for the exact version" \u2014 it is about not avoiding a conclusion. Always commit to the most likely root cause with supporting evidence.
|
|
4035
|
+
|
|
4036
|
+
### Tie Evidence to Conclusions
|
|
4037
|
+
|
|
4038
|
+
Every conclusion must reference specific evidence:
|
|
4039
|
+
- Wrong: "The cluster appears to have memory issues"
|
|
4040
|
+
- Right: "GPU memory on rank 3 is at 98% (kinetica_get_metrics) with 3 queries materializing >10GB each in ki_catalog.ki_query_history"
|
|
4041
|
+
|
|
4042
|
+
### Evidence Gap Handling
|
|
4043
|
+
|
|
4044
|
+
**SQL column errors (recoverable):**
|
|
4045
|
+
1. Check the **Verified Column Names** list in this prompt for the correct columns
|
|
4046
|
+
2. Rewrite the query using only verified columns and retry once
|
|
4047
|
+
3. If no verified columns exist for that table, fall back to \`SELECT * FROM ki_catalog.<table> LIMIT 10\`
|
|
4048
|
+
4. If the retry also fails, log the gap and continue
|
|
4049
|
+
|
|
4050
|
+
**Other tool failures (non-recoverable):**
|
|
4051
|
+
- Note the gap and continue. Use this format:
|
|
4052
|
+
- "Cluster status: unavailable (HTTP status 503 \u2014 WMS unreachable)"
|
|
4053
|
+
- "Log retrieval: failed (HTTP status 401 \u2014 authentication issue)"
|
|
4054
|
+
|
|
4055
|
+
Never halt the investigation on a single tool failure.
|
|
4056
|
+
|
|
4057
|
+
---
|
|
4058
|
+
|
|
4059
|
+
## Fix Instructions
|
|
4060
|
+
|
|
4061
|
+
Include specific, actionable remediation steps tied to your findings. Structure your actionable remediation as a numbered list:
|
|
4062
|
+
|
|
4063
|
+
1. Immediate manual actions the operator can take now
|
|
4064
|
+
2. Configuration changes to prevent recurrence
|
|
4065
|
+
3. Monitoring/alerting improvements to add
|
|
4066
|
+
4. Agent-assisted mutations (propose via mutation tools with user approval)
|
|
4067
|
+
|
|
4068
|
+
---
|
|
4069
|
+
|
|
4070
|
+
## Post-Report Behavior
|
|
4071
|
+
|
|
4072
|
+
1. Call the ${t}save_report${t} tool with the complete report markdown content to save it to disk.
|
|
4073
|
+
2. After the report is saved, ask: "Would you like to investigate another issue, or end the session?"
|
|
4074
|
+
3. If the operator wants another investigation, start fresh with the same 5-round protocol.
|
|
4075
|
+
4. On session end: summarize all issues investigated and list the saved report file paths, then exit.
|
|
4076
|
+
|
|
4077
|
+
---
|
|
4078
|
+
|
|
4079
|
+
## Context Window Awareness
|
|
4080
|
+
|
|
4081
|
+
Monitor your context window usage during long investigations:
|
|
4082
|
+
- After many tool calls with verbose results, the context window may be getting full.
|
|
4083
|
+
- If you detect that context is getting full (many rounds, many large tool responses), warn the operator: "The session context is getting long. Consider starting a fresh session after this report to maintain investigation quality. Your reports are saved to disk."
|
|
4084
|
+
- Do NOT continue investigating when context is too full \u2014 write the report with evidence gathered so far.
|
|
4085
|
+
|
|
4086
|
+
---
|
|
4087
|
+
|
|
4088
|
+
## Output Formatting
|
|
4089
|
+
|
|
4090
|
+
When presenting data in your response, use clean, well-structured markdown tables:
|
|
4091
|
+
|
|
4092
|
+
- Use standard markdown table syntax: header row, separator row (with dashes), then data rows
|
|
4093
|
+
- Keep column count low (3\u20136 columns max). If data has more dimensions, split into multiple focused tables
|
|
4094
|
+
- **Bold** key identifiers in the first column for scannability
|
|
4095
|
+
- Use consistent status indicators: \`OK\`, \`WARN\`, \`ERROR\`, \`N/A\`
|
|
4096
|
+
- Do NOT dump raw tool output \u2014 synthesize findings into clean, readable tables
|
|
4097
|
+
- Align numeric columns for easy comparison
|
|
4098
|
+
|
|
4099
|
+
Example:
|
|
4100
|
+
|
|
4101
|
+
| **Node** | CPU | Memory | Status |
|
|
4102
|
+
| ---------- | --- | ------ | ------ |
|
|
4103
|
+
| **node_0** | 45% | 12 GB | OK |
|
|
4104
|
+
| **node_1** | 92% | 15 GB | WARN |
|
|
4105
|
+
|
|
4106
|
+
---
|
|
4107
|
+
|
|
4108
|
+
## REPORT TEMPLATE
|
|
4109
|
+
|
|
4110
|
+
At the end of each investigation, generate a structured markdown report using this EXACT template and section order:
|
|
4111
|
+
|
|
4112
|
+
\`\`\`markdown
|
|
4113
|
+
` + REPORT_TEMPLATE + `\`\`\`
|
|
4114
|
+
|
|
4115
|
+
**CRITICAL:** Use this exact section order. The metadata table comes first. Summary before Remediation. Evidence Collected before Evidence Gaps. Mutations Applied before Post-Remediation Verification. Do NOT reorder sections.
|
|
4116
|
+
|
|
4117
|
+
**Section order:** Metadata -> Summary -> Remediation -> Root Cause Analysis -> Evidence Collected -> Evidence Gaps -> Mutations Applied -> Post-Remediation Verification
|
|
4118
|
+
|
|
4119
|
+
**Evidence Collected guidance:** Include only the key data points that led to your conclusion. No raw JSON dumps. No full log output. Extract the 3-10 most relevant findings.
|
|
4120
|
+
`;
|
|
4121
|
+
}
|
|
4122
|
+
|
|
4123
|
+
// src/agent/discover-schemas.ts
|
|
4124
|
+
var TARGET_TABLES = [
|
|
4125
|
+
"ki_query_history",
|
|
4126
|
+
"ki_query_active_all",
|
|
4127
|
+
"ki_query_span_metrics_all",
|
|
4128
|
+
"ki_query_workers",
|
|
4129
|
+
"ki_tiered_objects",
|
|
4130
|
+
"ki_obj_stat",
|
|
4131
|
+
"ki_partitions",
|
|
4132
|
+
"ki_objects",
|
|
4133
|
+
"ki_indexes",
|
|
4134
|
+
"ki_periodic_objects",
|
|
4135
|
+
"ki_depend",
|
|
4136
|
+
"ki_users_and_roles",
|
|
4137
|
+
"ki_object_permissions",
|
|
4138
|
+
"ki_load_history",
|
|
4139
|
+
"ki_backup_history",
|
|
4140
|
+
"ki_kafka_lag_info",
|
|
4141
|
+
"ki_columns",
|
|
4142
|
+
"ki_datatypes"
|
|
4143
|
+
];
|
|
4144
|
+
async function discoverCatalogSchemas(session2) {
|
|
4145
|
+
try {
|
|
4146
|
+
const tableList = TARGET_TABLES.map((t) => `'${t}'`).join(", ");
|
|
4147
|
+
const statement = `SELECT table_name, column_name FROM ki_catalog.ki_columns WHERE table_name IN (${tableList}) ORDER BY table_name, column_name`;
|
|
4148
|
+
const result = await executeSql(session2, statement, 1e3);
|
|
4149
|
+
if (!result.ok) {
|
|
4150
|
+
return void 0;
|
|
4151
|
+
}
|
|
4152
|
+
const rows = result.data;
|
|
4153
|
+
if (rows.length === 0) {
|
|
4154
|
+
return void 0;
|
|
4155
|
+
}
|
|
4156
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
4157
|
+
for (const row of rows) {
|
|
4158
|
+
const existing = grouped.get(row.table_name) ?? [];
|
|
4159
|
+
grouped.set(row.table_name, [...existing, row.column_name]);
|
|
4160
|
+
}
|
|
4161
|
+
return { tables: grouped };
|
|
4162
|
+
} catch {
|
|
4163
|
+
return void 0;
|
|
4164
|
+
}
|
|
4165
|
+
}
|
|
4166
|
+
|
|
4167
|
+
// src/agent/load-references.ts
|
|
4168
|
+
var import_promises3 = require("fs/promises");
|
|
4169
|
+
var import_node_path4 = require("path");
|
|
4170
|
+
var import_node_fs3 = require("fs");
|
|
4171
|
+
async function loadReferences(refsDir) {
|
|
4172
|
+
try {
|
|
4173
|
+
const dir = refsDir ?? (0, import_node_path4.join)(findPackageRoot(__dirname), "knowledge", "references");
|
|
4174
|
+
if (!(0, import_node_fs3.existsSync)(dir)) return [];
|
|
4175
|
+
const files = await (0, import_promises3.readdir)(dir);
|
|
4176
|
+
const mdFiles = files.filter((f) => f.endsWith(".md")).sort();
|
|
4177
|
+
const references = [];
|
|
4178
|
+
for (const file of mdFiles) {
|
|
4179
|
+
const raw = await (0, import_promises3.readFile)((0, import_node_path4.join)(dir, file), "utf-8");
|
|
4180
|
+
const frontmatter = parseFrontmatter(raw);
|
|
4181
|
+
if (!frontmatter) continue;
|
|
4182
|
+
references.push({
|
|
4183
|
+
title: frontmatter.title,
|
|
4184
|
+
category: frontmatter.category,
|
|
4185
|
+
keywords: frontmatter.keywords,
|
|
4186
|
+
body: extractBody(raw),
|
|
4187
|
+
filename: file
|
|
4188
|
+
});
|
|
4189
|
+
}
|
|
4190
|
+
return references;
|
|
4191
|
+
} catch {
|
|
4192
|
+
return [];
|
|
4193
|
+
}
|
|
4194
|
+
}
|
|
4195
|
+
|
|
4196
|
+
// src/agent/prompt-budget.ts
|
|
4197
|
+
var CHARS_PER_TOKEN = 4;
|
|
4198
|
+
var DEFAULT_PROMPT_BUDGET_TOKENS = 15e3;
|
|
4199
|
+
function estimateTokens(text) {
|
|
4200
|
+
if (!text) return 0;
|
|
4201
|
+
return Math.ceil(text.length / CHARS_PER_TOKEN);
|
|
4202
|
+
}
|
|
4203
|
+
function checkPromptBudget(prompt, opts) {
|
|
4204
|
+
const threshold = opts?.warnAtTokens ?? DEFAULT_PROMPT_BUDGET_TOKENS;
|
|
4205
|
+
const tokens = estimateTokens(prompt);
|
|
4206
|
+
return {
|
|
4207
|
+
tokens,
|
|
4208
|
+
chars: prompt ? prompt.length : 0,
|
|
4209
|
+
threshold,
|
|
4210
|
+
overBudget: tokens > threshold
|
|
4211
|
+
};
|
|
4212
|
+
}
|
|
4213
|
+
|
|
4214
|
+
// src/report/save-report.ts
|
|
4215
|
+
var import_promises4 = require("fs/promises");
|
|
4216
|
+
var import_node_path5 = require("path");
|
|
4217
|
+
var import_claude_agent_sdk4 = require("@anthropic-ai/claude-agent-sdk");
|
|
4218
|
+
var import_zod17 = require("zod");
|
|
4219
|
+
var PARTIAL_MARKER = "(PARTIAL -- investigation interrupted)\n\n";
|
|
4220
|
+
function formatTimestamp(date) {
|
|
4221
|
+
const year = date.getUTCFullYear();
|
|
4222
|
+
const month = String(date.getUTCMonth() + 1).padStart(2, "0");
|
|
4223
|
+
const day = String(date.getUTCDate()).padStart(2, "0");
|
|
4224
|
+
const hours = String(date.getUTCHours()).padStart(2, "0");
|
|
4225
|
+
const minutes = String(date.getUTCMinutes()).padStart(2, "0");
|
|
4226
|
+
const seconds = String(date.getUTCSeconds()).padStart(2, "0");
|
|
4227
|
+
return `${year}-${month}-${day}-${hours}${minutes}${seconds}`;
|
|
4228
|
+
}
|
|
4229
|
+
function makeSaveReportTool() {
|
|
4230
|
+
return (0, import_claude_agent_sdk4.tool)(
|
|
4231
|
+
"save_report",
|
|
4232
|
+
"Save a diagnostic report to disk. Automatically scrubs credentials, creates a timestamped filename in reports/, and auto-creates the directory. Use at the end of each investigation or when interrupted.",
|
|
4233
|
+
{
|
|
4234
|
+
content: import_zod17.z.string().describe("The full markdown diagnostic report content"),
|
|
4235
|
+
partial: import_zod17.z.boolean().optional().describe(
|
|
4236
|
+
"Set to true if the investigation was interrupted (e.g., Ctrl+C). Prepends a PARTIAL marker to the report."
|
|
4237
|
+
)
|
|
4238
|
+
},
|
|
4239
|
+
async (args) => {
|
|
4240
|
+
const rawContent = args.partial ? `${PARTIAL_MARKER}${args.content}` : args.content;
|
|
4241
|
+
const scrubbed = scrubCredentials(rawContent);
|
|
4242
|
+
const timestamp = formatTimestamp(/* @__PURE__ */ new Date());
|
|
4243
|
+
const filename = `kinetica-diag-${timestamp}.md`;
|
|
4244
|
+
const dir = (0, import_node_path5.resolve)(process.cwd(), "reports");
|
|
4245
|
+
await (0, import_promises4.mkdir)(dir, { recursive: true });
|
|
4246
|
+
const filepath = (0, import_node_path5.join)(dir, filename);
|
|
4247
|
+
await (0, import_promises4.writeFile)(filepath, scrubbed, "utf-8");
|
|
4248
|
+
return {
|
|
4249
|
+
content: [{ type: "text", text: `Report saved: ${filepath}` }]
|
|
4250
|
+
};
|
|
4251
|
+
},
|
|
4252
|
+
{ annotations: { readOnly: true } }
|
|
4253
|
+
);
|
|
4254
|
+
}
|
|
4255
|
+
|
|
4256
|
+
// src/approval/gate.ts
|
|
4257
|
+
var import_prompts7 = require("@inquirer/prompts");
|
|
4258
|
+
|
|
4259
|
+
// src/approval/display.ts
|
|
4260
|
+
var import_picocolors10 = __toESM(require("picocolors"));
|
|
4261
|
+
var IMPACT_FALLBACK = "Impact unknown \u2014 review parameters carefully";
|
|
4262
|
+
var DIVIDER2 = import_picocolors10.default.dim("\u2500".repeat(50));
|
|
4263
|
+
var LABEL_WIDTH = 8;
|
|
4264
|
+
function formatLabel(label) {
|
|
4265
|
+
return ` ${label.padEnd(LABEL_WIDTH)}: `;
|
|
4266
|
+
}
|
|
4267
|
+
function renderApprovalPanel(toolName, toolInput, impact, beforeAfter, reasoningSummary) {
|
|
4268
|
+
const header = import_picocolors10.default.bold(import_picocolors10.default.yellow(" Mutation Approval Required"));
|
|
4269
|
+
const action = `${formatLabel("Action")}${import_picocolors10.default.bold(formatToolName(toolName))}`;
|
|
4270
|
+
const paramEntries = Object.entries(toolInput);
|
|
4271
|
+
const paramSection = paramEntries.length === 0 ? " (no parameters)" : paramEntries.map(([key, value]) => {
|
|
4272
|
+
const formatted = typeof value === "string" ? value : JSON.stringify(value, null, 2);
|
|
4273
|
+
return ` ${import_picocolors10.default.dim(key)}: ${formatted}`;
|
|
4274
|
+
}).join("\n");
|
|
4275
|
+
const impactLine = `${formatLabel("Impact")}${impact ?? IMPACT_FALLBACK}`;
|
|
4276
|
+
const prompt = import_picocolors10.default.dim(
|
|
4277
|
+
`${formatLabel("Respond")}y (proceed) | n (abort) | explain (show reasoning)`
|
|
4278
|
+
);
|
|
4279
|
+
const hasBeforeAfter = beforeAfter !== void 0 && beforeAfter.length > 0;
|
|
4280
|
+
const beforeAfterSection = hasBeforeAfter ? beforeAfter.map(
|
|
4281
|
+
(entry) => ` ${import_picocolors10.default.dim(entry.key)}: ${entry.current} ${import_picocolors10.default.yellow("->")} ${entry.proposed}`
|
|
4282
|
+
).join("\n") : null;
|
|
4283
|
+
const hasReasoning = reasoningSummary !== void 0 && reasoningSummary.length > 0;
|
|
4284
|
+
const reasoningSection = hasReasoning ? `${formatLabel("Reason")}${reasoningSummary}` : null;
|
|
4285
|
+
const sections = ["", DIVIDER2, header, "", action, paramSection, ""];
|
|
4286
|
+
if (beforeAfterSection !== null) {
|
|
4287
|
+
sections.push(beforeAfterSection, "");
|
|
4288
|
+
}
|
|
4289
|
+
if (reasoningSection !== null) {
|
|
4290
|
+
sections.push(reasoningSection, "");
|
|
4291
|
+
}
|
|
4292
|
+
sections.push(impactLine, "", prompt, DIVIDER2, "");
|
|
4293
|
+
return sections.join("\n");
|
|
4294
|
+
}
|
|
4295
|
+
|
|
4296
|
+
// src/approval/gate.ts
|
|
4297
|
+
var DENY_MESSAGE = "User denied this mutation. Skip and continue with the investigation.";
|
|
4298
|
+
var REASONING_FALLBACK = "Reasoning not available. Review the action details above before proceeding.";
|
|
4299
|
+
function createApprovalGate(isReadOnly) {
|
|
4300
|
+
return async (toolName, toolInput, options) => {
|
|
4301
|
+
if (isReadOnly(toolName)) {
|
|
4302
|
+
return {
|
|
4303
|
+
behavior: "allow",
|
|
4304
|
+
updatedInput: toolInput,
|
|
4305
|
+
toolUseID: options.toolUseID
|
|
4306
|
+
};
|
|
4307
|
+
}
|
|
4308
|
+
const impact = options.decisionReason;
|
|
4309
|
+
const panel = renderApprovalPanel(toolName, toolInput, impact);
|
|
4310
|
+
console.error(panel);
|
|
4311
|
+
while (true) {
|
|
4312
|
+
try {
|
|
4313
|
+
const raw = await (0, import_prompts7.input)({ message: "Proceed? (y/n/explain):" }, { signal: options.signal });
|
|
4314
|
+
const normalized = raw.trim().toLowerCase();
|
|
4315
|
+
if (normalized === "y") {
|
|
4316
|
+
process.stderr.write("\n");
|
|
4317
|
+
return {
|
|
4318
|
+
behavior: "allow",
|
|
4319
|
+
updatedInput: toolInput,
|
|
4320
|
+
toolUseID: options.toolUseID
|
|
4321
|
+
};
|
|
4322
|
+
}
|
|
4323
|
+
if (normalized === "n") {
|
|
4324
|
+
process.stderr.write("\n");
|
|
4325
|
+
return {
|
|
4326
|
+
behavior: "deny",
|
|
4327
|
+
message: DENY_MESSAGE,
|
|
4328
|
+
toolUseID: options.toolUseID
|
|
4329
|
+
};
|
|
4330
|
+
}
|
|
4331
|
+
if (normalized === "explain") {
|
|
4332
|
+
const reasoning = options.decisionReason;
|
|
4333
|
+
if (reasoning) {
|
|
4334
|
+
console.error(`
|
|
4335
|
+
Agent reasoning: ${reasoning}
|
|
4336
|
+
`);
|
|
4337
|
+
} else {
|
|
4338
|
+
console.error(`
|
|
4339
|
+
${REASONING_FALLBACK}
|
|
4340
|
+
`);
|
|
4341
|
+
}
|
|
4342
|
+
}
|
|
4343
|
+
} catch {
|
|
4344
|
+
return {
|
|
4345
|
+
behavior: "deny",
|
|
4346
|
+
message: DENY_MESSAGE,
|
|
4347
|
+
toolUseID: options.toolUseID
|
|
4348
|
+
};
|
|
4349
|
+
}
|
|
4350
|
+
}
|
|
4351
|
+
};
|
|
4352
|
+
}
|
|
4353
|
+
|
|
4354
|
+
// src/agent/turn-gate.ts
|
|
4355
|
+
function createTurnGate() {
|
|
4356
|
+
let resolve2 = () => {
|
|
4357
|
+
};
|
|
4358
|
+
let promise = new Promise((r) => {
|
|
4359
|
+
resolve2 = r;
|
|
4360
|
+
});
|
|
4361
|
+
return Object.freeze({
|
|
4362
|
+
wait: () => promise,
|
|
4363
|
+
open: () => {
|
|
4364
|
+
resolve2();
|
|
4365
|
+
},
|
|
4366
|
+
close: () => {
|
|
4367
|
+
promise = new Promise((r) => {
|
|
4368
|
+
resolve2 = r;
|
|
4369
|
+
});
|
|
4370
|
+
}
|
|
4371
|
+
});
|
|
4372
|
+
}
|
|
4373
|
+
|
|
4374
|
+
// src/output/render-markdown.ts
|
|
4375
|
+
var import_picocolors11 = __toESM(require("picocolors"));
|
|
4376
|
+
var BOLD_RE = /\*\*(.+?)\*\*/g;
|
|
4377
|
+
var HEADING_RE = /^(#{1,6})\s+(.+)$/;
|
|
4378
|
+
function renderMarkdownLine(line) {
|
|
4379
|
+
const headingMatch = HEADING_RE.exec(line);
|
|
4380
|
+
if (headingMatch) {
|
|
4381
|
+
return import_picocolors11.default.bold(headingMatch[2]);
|
|
4382
|
+
}
|
|
4383
|
+
if (line.includes("**")) {
|
|
4384
|
+
return line.replace(BOLD_RE, (_, text) => import_picocolors11.default.bold(text));
|
|
4385
|
+
}
|
|
4386
|
+
return line;
|
|
4387
|
+
}
|
|
4388
|
+
|
|
4389
|
+
// src/output/reformat-tables.ts
|
|
4390
|
+
var SEPARATOR_CELL_RE = /^:?-+:?$/;
|
|
4391
|
+
var BOLD_MARKERS_RE = /\*\*(.+?)\*\*/g;
|
|
4392
|
+
function visualWidth(text) {
|
|
4393
|
+
return text.replace(BOLD_MARKERS_RE, "$1").length;
|
|
4394
|
+
}
|
|
4395
|
+
function isSeparatorCell(cell) {
|
|
4396
|
+
return SEPARATOR_CELL_RE.test(cell);
|
|
4397
|
+
}
|
|
4398
|
+
function isSeparatorRow(cells) {
|
|
4399
|
+
return cells.length > 0 && cells.every(isSeparatorCell);
|
|
4400
|
+
}
|
|
4401
|
+
function parseCells(line) {
|
|
4402
|
+
return line.split("|").slice(1, -1).map((c) => c.trim());
|
|
4403
|
+
}
|
|
4404
|
+
function reformatTableBlock(lines) {
|
|
4405
|
+
const parsed = lines.map(parseCells);
|
|
4406
|
+
const colCount = Math.max(...parsed.map((row) => row.length));
|
|
4407
|
+
const normalised = parsed.map((row) => {
|
|
4408
|
+
const padded = [...row];
|
|
4409
|
+
while (padded.length < colCount) {
|
|
4410
|
+
padded.push("");
|
|
4411
|
+
}
|
|
4412
|
+
return padded;
|
|
4413
|
+
});
|
|
4414
|
+
const colWidths = Array.from(
|
|
4415
|
+
{ length: colCount },
|
|
4416
|
+
(_, col) => Math.max(
|
|
4417
|
+
3,
|
|
4418
|
+
...normalised.filter((row) => !isSeparatorRow(row)).map((row) => visualWidth(row[col]))
|
|
4419
|
+
)
|
|
4420
|
+
);
|
|
4421
|
+
const borderRow = `+${colWidths.map((w) => "-".repeat(w + 2)).join("+")}+`;
|
|
4422
|
+
const bodyRows = normalised.map((row) => {
|
|
4423
|
+
if (isSeparatorRow(row)) {
|
|
4424
|
+
return borderRow;
|
|
4425
|
+
}
|
|
4426
|
+
const cells = row.map((cell, col) => {
|
|
4427
|
+
const rendered = renderMarkdownLine(cell);
|
|
4428
|
+
const pad = colWidths[col] - visualWidth(cell);
|
|
4429
|
+
return rendered + " ".repeat(Math.max(0, pad));
|
|
4430
|
+
});
|
|
4431
|
+
return `| ${cells.join(" | ")} |`;
|
|
4432
|
+
});
|
|
4433
|
+
return [borderRow, ...bodyRows, borderRow];
|
|
4434
|
+
}
|
|
4435
|
+
|
|
4436
|
+
// src/output/streaming-table-aligner.ts
|
|
4437
|
+
var TABLE_LINE_RE = /^\|.*\|$/;
|
|
4438
|
+
function createStreamingTableAligner() {
|
|
4439
|
+
let lineBuffer = "";
|
|
4440
|
+
let tableLines = [];
|
|
4441
|
+
function flushTable() {
|
|
4442
|
+
if (tableLines.length === 0) return "";
|
|
4443
|
+
const aligned = reformatTableBlock(tableLines);
|
|
4444
|
+
tableLines = [];
|
|
4445
|
+
return aligned.join("\n") + "\n";
|
|
4446
|
+
}
|
|
4447
|
+
function push(text) {
|
|
4448
|
+
if (!text) return "";
|
|
4449
|
+
const combined = lineBuffer + text;
|
|
4450
|
+
const segments = combined.split("\n");
|
|
4451
|
+
lineBuffer = segments[segments.length - 1];
|
|
4452
|
+
const completeLines = segments.slice(0, -1);
|
|
4453
|
+
let output = "";
|
|
4454
|
+
for (const line of completeLines) {
|
|
4455
|
+
const trimmed = line.trim();
|
|
4456
|
+
if (TABLE_LINE_RE.test(trimmed)) {
|
|
4457
|
+
tableLines.push(trimmed);
|
|
4458
|
+
} else {
|
|
4459
|
+
output += flushTable();
|
|
4460
|
+
output += renderMarkdownLine(line) + "\n";
|
|
4461
|
+
}
|
|
4462
|
+
}
|
|
4463
|
+
return output;
|
|
4464
|
+
}
|
|
4465
|
+
function flush() {
|
|
4466
|
+
let output = flushTable();
|
|
4467
|
+
if (lineBuffer) {
|
|
4468
|
+
output += renderMarkdownLine(lineBuffer);
|
|
4469
|
+
lineBuffer = "";
|
|
4470
|
+
}
|
|
4471
|
+
return output;
|
|
4472
|
+
}
|
|
4473
|
+
return Object.freeze({ push, flush });
|
|
4474
|
+
}
|
|
4475
|
+
|
|
4476
|
+
// src/output/spinner.ts
|
|
4477
|
+
var import_picocolors12 = __toESM(require("picocolors"));
|
|
4478
|
+
var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
4479
|
+
var FRAME_INTERVAL_MS = 80;
|
|
4480
|
+
var DEFAULT_LABEL = "Thinking";
|
|
4481
|
+
function createSpinner() {
|
|
4482
|
+
let timer = null;
|
|
4483
|
+
let frameIndex = 0;
|
|
4484
|
+
const start = (label = DEFAULT_LABEL) => {
|
|
4485
|
+
if (timer !== null) return;
|
|
4486
|
+
frameIndex = 0;
|
|
4487
|
+
timer = setInterval(() => {
|
|
4488
|
+
const frame = FRAMES[frameIndex % FRAMES.length];
|
|
4489
|
+
process.stderr.write(`\r${import_picocolors12.default.dim(`${frame} ${label}...`)}`);
|
|
4490
|
+
frameIndex += 1;
|
|
4491
|
+
}, FRAME_INTERVAL_MS);
|
|
4492
|
+
timer.unref();
|
|
4493
|
+
};
|
|
4494
|
+
const stop = () => {
|
|
4495
|
+
if (timer === null) return;
|
|
4496
|
+
clearInterval(timer);
|
|
4497
|
+
timer = null;
|
|
4498
|
+
process.stderr.write("\r\x1B[K");
|
|
4499
|
+
};
|
|
4500
|
+
const isRunning = () => timer !== null;
|
|
4501
|
+
return Object.freeze({ start, stop, isRunning });
|
|
4502
|
+
}
|
|
4503
|
+
|
|
4504
|
+
// src/agent/run-agent.ts
|
|
4505
|
+
var MCP_SERVER_NAME = "kinetica-diagnostics";
|
|
4506
|
+
var EXIT_COMMANDS = /* @__PURE__ */ new Set(["exit", "quit", "end", "q"]);
|
|
4507
|
+
var SUPPORTED_MODELS = ["sonnet", "haiku", "opus"];
|
|
4508
|
+
var DEFAULT_AGENT_MODEL = "sonnet";
|
|
4509
|
+
var DEFAULT_MAX_BUDGET_USD = 5;
|
|
4510
|
+
var ALLOWED_TOOL_NAMES = [
|
|
4511
|
+
...DIAGNOSTIC_TOOL_NAMES.map((name) => `mcp__${MCP_SERVER_NAME}__${name}`),
|
|
4512
|
+
`mcp__${MCP_SERVER_NAME}__save_report`,
|
|
4513
|
+
`mcp__${MCP_SERVER_NAME}__${ALTER_TABLE_COLUMNS_TOOL_NAME}`
|
|
4514
|
+
];
|
|
4515
|
+
var DISALLOWED_TOOLS = ["Bash", "Edit", "Write", "MultiEdit"];
|
|
4516
|
+
var ERROR_LABELS = {
|
|
4517
|
+
authentication_failed: "Authentication failed \u2014 check your API key or re-run with --login",
|
|
4518
|
+
billing_error: "Billing error \u2014 check your Anthropic account",
|
|
4519
|
+
rate_limit: "Rate limit exceeded",
|
|
4520
|
+
server_error: "Anthropic API server error",
|
|
4521
|
+
invalid_request: "Invalid API request",
|
|
4522
|
+
max_output_tokens: "Response exceeded maximum output length",
|
|
4523
|
+
unknown: "Unknown API error"
|
|
4524
|
+
};
|
|
4525
|
+
function isExitCommand(text) {
|
|
4526
|
+
return EXIT_COMMANDS.has(text.trim().toLowerCase());
|
|
4527
|
+
}
|
|
4528
|
+
function makeUserMessage(content) {
|
|
4529
|
+
return {
|
|
4530
|
+
type: "user",
|
|
4531
|
+
message: { role: "user", content },
|
|
4532
|
+
parent_tool_use_id: null,
|
|
4533
|
+
session_id: ""
|
|
4534
|
+
};
|
|
4535
|
+
}
|
|
4536
|
+
async function* makeInteractivePrompt(abortController, turnGate, spinner) {
|
|
4537
|
+
while (!abortController.signal.aborted) {
|
|
4538
|
+
try {
|
|
4539
|
+
process.stderr.write("\n");
|
|
4540
|
+
const issue = await (0, import_prompts8.input)({ message: "Describe the issue to investigate:" });
|
|
4541
|
+
process.stderr.write("\n");
|
|
4542
|
+
const trimmed = issue.trim();
|
|
4543
|
+
if (!trimmed) continue;
|
|
4544
|
+
if (isExitCommand(trimmed)) return;
|
|
4545
|
+
spinner.start();
|
|
4546
|
+
yield makeUserMessage(trimmed);
|
|
4547
|
+
break;
|
|
4548
|
+
} catch {
|
|
4549
|
+
return;
|
|
4550
|
+
}
|
|
4551
|
+
}
|
|
4552
|
+
while (!abortController.signal.aborted) {
|
|
4553
|
+
try {
|
|
4554
|
+
await turnGate.wait();
|
|
4555
|
+
if (abortController.signal.aborted) break;
|
|
4556
|
+
process.stderr.write("\n");
|
|
4557
|
+
const response = await (0, import_prompts8.input)({ message: "You:" });
|
|
4558
|
+
process.stderr.write("\n");
|
|
4559
|
+
const trimmed = response.trim();
|
|
4560
|
+
if (!trimmed) continue;
|
|
4561
|
+
if (isExitCommand(trimmed)) return;
|
|
4562
|
+
turnGate.close();
|
|
4563
|
+
spinner.start();
|
|
4564
|
+
yield makeUserMessage(trimmed);
|
|
4565
|
+
} catch {
|
|
4566
|
+
return;
|
|
4567
|
+
}
|
|
4568
|
+
}
|
|
4569
|
+
}
|
|
4570
|
+
async function displayDegradedStatus(session2) {
|
|
4571
|
+
const [statusResult, alertsResult] = await Promise.all([
|
|
4572
|
+
hostManagerStatus(session2),
|
|
4573
|
+
hostManagerAlerts(session2)
|
|
4574
|
+
]);
|
|
4575
|
+
process.stderr.write(import_picocolors13.default.bold("\u2500\u2500 Host Manager Status \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n"));
|
|
4576
|
+
if (statusResult.ok) {
|
|
4577
|
+
const rows = statusResult.data;
|
|
4578
|
+
const maxKeyLen = rows.reduce((max, r) => Math.max(max, r.key.length), 0);
|
|
4579
|
+
for (const row of rows) {
|
|
4580
|
+
process.stderr.write(` ${import_picocolors13.default.dim(row.key.padEnd(maxKeyLen))} ${row.value}
|
|
4581
|
+
`);
|
|
4582
|
+
}
|
|
4583
|
+
} else {
|
|
4584
|
+
process.stderr.write(` ${import_picocolors13.default.red(`Error: ${statusResult.error}`)}
|
|
4585
|
+
`);
|
|
4586
|
+
}
|
|
4587
|
+
process.stderr.write("\n");
|
|
4588
|
+
process.stderr.write(import_picocolors13.default.bold("\u2500\u2500 Recent Alerts \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n"));
|
|
4589
|
+
if (alertsResult.ok) {
|
|
4590
|
+
const alerts = alertsResult.data;
|
|
4591
|
+
if (alerts.length === 0) {
|
|
4592
|
+
process.stderr.write(` ${import_picocolors13.default.dim("No recent alerts.")}
|
|
4593
|
+
`);
|
|
4594
|
+
} else {
|
|
4595
|
+
for (const alert of alerts) {
|
|
4596
|
+
process.stderr.write(` ${import_picocolors13.default.dim(alert.timestamp)} ${alert.type} ${alert.params}
|
|
4597
|
+
`);
|
|
4598
|
+
}
|
|
4599
|
+
}
|
|
4600
|
+
} else {
|
|
4601
|
+
process.stderr.write(` ${import_picocolors13.default.dim(`Unavailable: ${alertsResult.error}`)}
|
|
4602
|
+
`);
|
|
4603
|
+
}
|
|
4604
|
+
process.stderr.write("\n");
|
|
4605
|
+
}
|
|
4606
|
+
async function runAgent(session2, kineticaVersion, degraded, model) {
|
|
4607
|
+
const [catalogSchemas, playbooks, references] = await Promise.all([
|
|
4608
|
+
degraded ? Promise.resolve(void 0) : discoverCatalogSchemas(session2),
|
|
4609
|
+
loadPlaybooks(),
|
|
4610
|
+
loadReferences()
|
|
4611
|
+
]);
|
|
4612
|
+
const systemPrompt = buildSystemPrompt(
|
|
4613
|
+
kineticaVersion,
|
|
4614
|
+
catalogSchemas,
|
|
4615
|
+
playbooks,
|
|
4616
|
+
references,
|
|
4617
|
+
degraded
|
|
4618
|
+
);
|
|
4619
|
+
const budget = checkPromptBudget(systemPrompt);
|
|
4620
|
+
if (process.env.DEBUG) {
|
|
4621
|
+
process.stderr.write(
|
|
4622
|
+
import_picocolors13.default.dim(`system prompt: ~${budget.tokens} tokens (${budget.chars} chars)
|
|
4623
|
+
`)
|
|
4624
|
+
);
|
|
4625
|
+
}
|
|
4626
|
+
if (budget.overBudget) {
|
|
4627
|
+
process.stderr.write(
|
|
4628
|
+
import_picocolors13.default.yellow(
|
|
4629
|
+
`\u26A0 system prompt is ~${budget.tokens} tokens (threshold ${budget.threshold}) \u2014 knowledge corpus is getting expensive; consider keyword-based playbook selection.
|
|
4630
|
+
`
|
|
4631
|
+
)
|
|
4632
|
+
);
|
|
4633
|
+
}
|
|
4634
|
+
const diagnosticTools = makeDiagnosticTools(session2, catalogSchemas);
|
|
4635
|
+
const mutationTools = makeMutationTools(session2);
|
|
4636
|
+
const saveReportTool = makeSaveReportTool();
|
|
4637
|
+
const alterTableColumnsTool = makeAlterTableColumnsToolWithDeps(session2);
|
|
4638
|
+
const server = (0, import_claude_agent_sdk5.createSdkMcpServer)({
|
|
4639
|
+
name: MCP_SERVER_NAME,
|
|
4640
|
+
version: "1.0.0",
|
|
4641
|
+
tools: [...diagnosticTools, ...mutationTools, saveReportTool, alterTableColumnsTool]
|
|
4642
|
+
});
|
|
4643
|
+
const spinner = createSpinner();
|
|
4644
|
+
const registry = createDiagnosticRegistry();
|
|
4645
|
+
const approvalGate = createApprovalGate(registry.isReadOnlyTool);
|
|
4646
|
+
const canUseTool = async (toolName, toolInput, options2) => {
|
|
4647
|
+
spinner.stop();
|
|
4648
|
+
return approvalGate(toolName, toolInput, options2);
|
|
4649
|
+
};
|
|
4650
|
+
const abortController = new AbortController();
|
|
4651
|
+
const effectiveModel = model ?? DEFAULT_AGENT_MODEL;
|
|
4652
|
+
const options = {
|
|
4653
|
+
mcpServers: { [MCP_SERVER_NAME]: server },
|
|
4654
|
+
allowedTools: ALLOWED_TOOL_NAMES,
|
|
4655
|
+
disallowedTools: [...DISALLOWED_TOOLS],
|
|
4656
|
+
canUseTool,
|
|
4657
|
+
systemPrompt,
|
|
4658
|
+
model: effectiveModel,
|
|
4659
|
+
fallbackModel: "haiku",
|
|
4660
|
+
thinking: { type: "adaptive" },
|
|
4661
|
+
maxTurns: 100,
|
|
4662
|
+
maxBudgetUsd: DEFAULT_MAX_BUDGET_USD,
|
|
4663
|
+
persistSession: false,
|
|
4664
|
+
includePartialMessages: true,
|
|
4665
|
+
abortController,
|
|
4666
|
+
env: { ...process.env, CLAUDE_AGENT_SDK_CLIENT_APP: "admin-agent" }
|
|
4667
|
+
};
|
|
4668
|
+
if (degraded) {
|
|
4669
|
+
process.stderr.write("\nKinetica Diagnostic Session Ready (DEGRADED MODE)\n");
|
|
4670
|
+
process.stderr.write(
|
|
4671
|
+
"DB engine (port 9191) is unreachable. Only host manager tools are available.\n\n"
|
|
4672
|
+
);
|
|
4673
|
+
await displayDegradedStatus(session2);
|
|
4674
|
+
process.stderr.write("Type 'exit' to end the session.\n\n");
|
|
4675
|
+
} else {
|
|
4676
|
+
process.stderr.write("\nKinetica Diagnostic Session Ready\n");
|
|
4677
|
+
process.stderr.write("Type 'exit' to end the session.\n\n");
|
|
4678
|
+
}
|
|
4679
|
+
const turnGate = createTurnGate();
|
|
4680
|
+
const agentQuery = (0, import_claude_agent_sdk5.query)({
|
|
4681
|
+
prompt: makeInteractivePrompt(abortController, turnGate, spinner),
|
|
4682
|
+
options
|
|
4683
|
+
});
|
|
4684
|
+
process.once("SIGINT", () => {
|
|
4685
|
+
spinner.stop();
|
|
4686
|
+
process.stderr.write("\nInterrupted \u2014 aborting investigation...\n");
|
|
4687
|
+
abortController.abort();
|
|
4688
|
+
agentQuery.close();
|
|
4689
|
+
});
|
|
4690
|
+
let numTurns = 0;
|
|
4691
|
+
let totalCostUsd = 0;
|
|
4692
|
+
let durationMs = 0;
|
|
4693
|
+
let durationApiMs = 0;
|
|
4694
|
+
let lastStreamCharWasNewline = true;
|
|
4695
|
+
const tableAligner = createStreamingTableAligner();
|
|
4696
|
+
let hadNonAbortError = false;
|
|
4697
|
+
try {
|
|
4698
|
+
for await (const message of agentQuery) {
|
|
4699
|
+
if (message.type === "stream_event") {
|
|
4700
|
+
const { event: evt } = message;
|
|
4701
|
+
if (evt.type === "content_block_delta" && evt.delta.type === "text_delta") {
|
|
4702
|
+
const text = evt.delta.text ?? "";
|
|
4703
|
+
if (text) {
|
|
4704
|
+
spinner.stop();
|
|
4705
|
+
const output = tableAligner.push(text);
|
|
4706
|
+
if (output) {
|
|
4707
|
+
process.stderr.write(output);
|
|
4708
|
+
lastStreamCharWasNewline = output.endsWith("\n");
|
|
4709
|
+
}
|
|
4710
|
+
}
|
|
4711
|
+
}
|
|
4712
|
+
} else if (message.type === "assistant") {
|
|
4713
|
+
const assistantMsg = message;
|
|
4714
|
+
const remaining = tableAligner.flush();
|
|
4715
|
+
if (remaining) {
|
|
4716
|
+
process.stderr.write(remaining);
|
|
4717
|
+
lastStreamCharWasNewline = remaining.endsWith("\n");
|
|
4718
|
+
}
|
|
4719
|
+
if (!lastStreamCharWasNewline) {
|
|
4720
|
+
process.stderr.write("\n");
|
|
4721
|
+
lastStreamCharWasNewline = true;
|
|
4722
|
+
}
|
|
4723
|
+
if (assistantMsg.message.stop_reason === "end_turn") {
|
|
4724
|
+
spinner.stop();
|
|
4725
|
+
turnGate.open();
|
|
4726
|
+
} else if (assistantMsg.message.stop_reason === "tool_use") {
|
|
4727
|
+
spinner.start("Investigating");
|
|
4728
|
+
} else {
|
|
4729
|
+
spinner.stop();
|
|
4730
|
+
}
|
|
4731
|
+
if (assistantMsg.error) {
|
|
4732
|
+
spinner.stop();
|
|
4733
|
+
const label = ERROR_LABELS[assistantMsg.error] ?? assistantMsg.error;
|
|
4734
|
+
process.stderr.write(import_picocolors13.default.yellow(`
|
|
4735
|
+
API error: ${label}
|
|
4736
|
+
`));
|
|
4737
|
+
turnGate.open();
|
|
4738
|
+
}
|
|
4739
|
+
} else if (message.type === "result") {
|
|
4740
|
+
spinner.stop();
|
|
4741
|
+
const resultMsg = message;
|
|
4742
|
+
numTurns = resultMsg.num_turns;
|
|
4743
|
+
totalCostUsd = resultMsg.total_cost_usd;
|
|
4744
|
+
durationMs = resultMsg.duration_ms;
|
|
4745
|
+
durationApiMs = resultMsg.duration_api_ms;
|
|
4746
|
+
if (resultMsg.subtype === "error_max_turns") {
|
|
4747
|
+
process.stderr.write(
|
|
4748
|
+
"\nInvestigation hit turn limit. Partial report may be available.\n"
|
|
4749
|
+
);
|
|
4750
|
+
} else if (resultMsg.subtype === "error_max_budget_usd") {
|
|
4751
|
+
process.stderr.write("\nBudget limit reached.\n");
|
|
4752
|
+
} else if (resultMsg.subtype === "error_during_execution") {
|
|
4753
|
+
process.stderr.write(
|
|
4754
|
+
"\nExecution error \u2014 the agent encountered an unrecoverable failure.\n"
|
|
4755
|
+
);
|
|
4756
|
+
} else if (resultMsg.subtype !== "success") {
|
|
4757
|
+
process.stderr.write(`
|
|
4758
|
+
Agent session ended with error: ${resultMsg.subtype}
|
|
4759
|
+
`);
|
|
4760
|
+
}
|
|
4761
|
+
if (resultMsg.permission_denials.length > 0) {
|
|
4762
|
+
const denied = resultMsg.permission_denials.map((d) => d.tool_name).join(", ");
|
|
4763
|
+
process.stderr.write(`
|
|
4764
|
+
Permission denials: ${denied}
|
|
4765
|
+
`);
|
|
4766
|
+
}
|
|
4767
|
+
turnGate.open();
|
|
4768
|
+
} else if (message.type === "system") {
|
|
4769
|
+
const sysMsg = message;
|
|
4770
|
+
if (sysMsg.subtype === "init") {
|
|
4771
|
+
const initMsg = message;
|
|
4772
|
+
const failed = (initMsg.mcp_servers ?? []).filter(
|
|
4773
|
+
(s) => s.name === MCP_SERVER_NAME && s.status !== "connected"
|
|
4774
|
+
);
|
|
4775
|
+
for (const s of failed) {
|
|
4776
|
+
process.stderr.write(
|
|
4777
|
+
`
|
|
4778
|
+
Warning: MCP server "${s.name}" failed to connect (${s.status})
|
|
4779
|
+
`
|
|
4780
|
+
);
|
|
4781
|
+
}
|
|
4782
|
+
} else if (sysMsg.subtype === "api_retry") {
|
|
4783
|
+
const retryMsg = message;
|
|
4784
|
+
const statusStr = retryMsg.error_status !== null ? ` (HTTP ${retryMsg.error_status})` : "";
|
|
4785
|
+
const delaySec = Math.round(retryMsg.retry_delay_ms / 1e3);
|
|
4786
|
+
process.stderr.write(
|
|
4787
|
+
import_picocolors13.default.yellow(
|
|
4788
|
+
`
|
|
4789
|
+
API error${statusStr}. Retrying (attempt ${retryMsg.attempt}/${retryMsg.max_retries}) in ${delaySec}s...
|
|
4790
|
+
`
|
|
4791
|
+
)
|
|
4792
|
+
);
|
|
4793
|
+
} else if (sysMsg.subtype === "compact_boundary") {
|
|
4794
|
+
const compactMsg = message;
|
|
4795
|
+
const preTokens = compactMsg.compact_metadata.pre_tokens;
|
|
4796
|
+
process.stderr.write(
|
|
4797
|
+
`
|
|
4798
|
+
[Context compressed (${preTokens} tokens before compaction) \u2014 investigation continues]
|
|
4799
|
+
`
|
|
4800
|
+
);
|
|
4801
|
+
}
|
|
4802
|
+
} else if (message.type === "rate_limit_event") {
|
|
4803
|
+
const rateMsg = message;
|
|
4804
|
+
const { status, resetsAt } = rateMsg.rate_limit_info;
|
|
4805
|
+
if (status === "rejected") {
|
|
4806
|
+
const resetStr = resetsAt ? ` Resets at ${new Date(resetsAt * 1e3).toISOString()}.` : "";
|
|
4807
|
+
process.stderr.write(`
|
|
4808
|
+
Rate limited \u2014 requests rejected.${resetStr}
|
|
4809
|
+
`);
|
|
4810
|
+
} else if (status === "allowed_warning") {
|
|
4811
|
+
process.stderr.write("\nApproaching rate limit \u2014 investigation may slow.\n");
|
|
4812
|
+
}
|
|
4813
|
+
} else if (message.type === "control_request") {
|
|
4814
|
+
const controlMsg = message;
|
|
4815
|
+
if (controlMsg.request.subtype === "claude_authenticate") {
|
|
4816
|
+
process.stderr.write(import_picocolors13.default.yellow("\nRe-authentication requested by SDK...\n"));
|
|
4817
|
+
}
|
|
4818
|
+
}
|
|
4819
|
+
}
|
|
4820
|
+
} catch (error) {
|
|
4821
|
+
spinner.stop();
|
|
4822
|
+
if (error instanceof import_claude_agent_sdk5.AbortError) {
|
|
4823
|
+
hadNonAbortError = false;
|
|
4824
|
+
} else {
|
|
4825
|
+
hadNonAbortError = true;
|
|
4826
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
4827
|
+
process.stderr.write(import_picocolors13.default.red(`
|
|
4828
|
+
Agent error: ${message}
|
|
4829
|
+
`));
|
|
4830
|
+
}
|
|
4831
|
+
} finally {
|
|
4832
|
+
spinner.stop();
|
|
4833
|
+
const remaining = tableAligner.flush();
|
|
4834
|
+
if (remaining) {
|
|
4835
|
+
process.stderr.write(remaining);
|
|
4836
|
+
}
|
|
4837
|
+
turnGate.open();
|
|
4838
|
+
const durationSec = Math.round(durationMs / 1e3);
|
|
4839
|
+
const apiPct = durationMs > 0 ? Math.round(durationApiMs / durationMs * 100) : 0;
|
|
4840
|
+
const costStr = totalCostUsd > 0 ? ` Cost: $${totalCostUsd.toFixed(4)}.` : "";
|
|
4841
|
+
if (hadNonAbortError) {
|
|
4842
|
+
process.stderr.write(`
|
|
4843
|
+
Session ended due to error. Turns: ${numTurns}.${costStr}
|
|
4844
|
+
`);
|
|
4845
|
+
} else {
|
|
4846
|
+
process.stderr.write(
|
|
4847
|
+
`
|
|
4848
|
+
Session ended. Turns: ${numTurns}. Duration: ${durationSec}s (${apiPct}% API).${costStr}
|
|
4849
|
+
`
|
|
4850
|
+
);
|
|
4851
|
+
}
|
|
4852
|
+
}
|
|
4853
|
+
}
|
|
4854
|
+
|
|
4855
|
+
// src/cli/index.ts
|
|
4856
|
+
var verbose = false;
|
|
4857
|
+
var session;
|
|
4858
|
+
function printHelp() {
|
|
4859
|
+
const lines = [
|
|
4860
|
+
"",
|
|
4861
|
+
" admin-agent",
|
|
4862
|
+
"",
|
|
4863
|
+
" Autonomous diagnostic agent for Kinetica databases",
|
|
4864
|
+
"",
|
|
4865
|
+
" Usage:",
|
|
4866
|
+
" admin-agent [flags]",
|
|
4867
|
+
"",
|
|
4868
|
+
" Flags:",
|
|
4869
|
+
" --help Show this help message",
|
|
4870
|
+
" --version Print version and exit",
|
|
4871
|
+
" --verbose Enable verbose output (stack traces on error)",
|
|
4872
|
+
" --login Force OAuth login (even if ANTHROPIC_API_KEY is set)",
|
|
4873
|
+
" --login-method=TYPE Login method: claudeai (Pro/Max) or console",
|
|
4874
|
+
" --login-org=UUID Target organization UUID for OAuth",
|
|
4875
|
+
" --logout Log out from Anthropic account and exit",
|
|
4876
|
+
` --model=NAME Override agent model (${SUPPORTED_MODELS.join(" | ")}); default: sonnet`,
|
|
4877
|
+
"",
|
|
4878
|
+
" Environment variables:",
|
|
4879
|
+
" ANTHROPIC_API_KEY Anthropic API key (if not set, OAuth login via browser is used)",
|
|
4880
|
+
" KINETICA_URL Kinetica endpoint URL",
|
|
4881
|
+
" KINETICA_USER Admin username",
|
|
4882
|
+
" KINETICA_PASS Admin password",
|
|
4883
|
+
""
|
|
4884
|
+
];
|
|
4885
|
+
process.stdout.write(lines.join("\n") + "\n");
|
|
4886
|
+
}
|
|
4887
|
+
async function main() {
|
|
4888
|
+
const args = process.argv.slice(2);
|
|
4889
|
+
if (args.includes("--verbose")) {
|
|
4890
|
+
verbose = true;
|
|
4891
|
+
}
|
|
4892
|
+
if (args.includes("--version")) {
|
|
4893
|
+
process.stdout.write(getVersion() + "\n");
|
|
4894
|
+
return;
|
|
4895
|
+
}
|
|
4896
|
+
if (args.includes("--help")) {
|
|
4897
|
+
printHelp();
|
|
4898
|
+
return;
|
|
4899
|
+
}
|
|
4900
|
+
if (args.includes("--logout")) {
|
|
4901
|
+
const result = await logout();
|
|
4902
|
+
process.stderr.write(result.message + "\n");
|
|
4903
|
+
process.exitCode = result.success ? 0 : 1;
|
|
4904
|
+
return;
|
|
4905
|
+
}
|
|
4906
|
+
const forceLogin = args.includes("--login");
|
|
4907
|
+
const loginMethodArg = args.find((a) => a.startsWith("--login-method="));
|
|
4908
|
+
const loginMethod = loginMethodArg?.split("=")[1];
|
|
4909
|
+
const loginOrgArg = args.find((a) => a.startsWith("--login-org="));
|
|
4910
|
+
const loginOrgUUID = loginOrgArg?.split("=")[1];
|
|
4911
|
+
const modelArg = args.find((a) => a.startsWith("--model="));
|
|
4912
|
+
const modelValue = modelArg?.split("=")[1];
|
|
4913
|
+
let model;
|
|
4914
|
+
if (modelValue !== void 0) {
|
|
4915
|
+
if (SUPPORTED_MODELS.includes(modelValue)) {
|
|
4916
|
+
model = modelValue;
|
|
4917
|
+
} else {
|
|
4918
|
+
const valid = SUPPORTED_MODELS.join(", ");
|
|
4919
|
+
process.stderr.write(
|
|
4920
|
+
import_picocolors14.default.red(`Error: unknown --model value "${modelValue}". Valid models: ${valid}
|
|
4921
|
+
`)
|
|
4922
|
+
);
|
|
4923
|
+
process.exitCode = 1;
|
|
4924
|
+
return;
|
|
4925
|
+
}
|
|
4926
|
+
}
|
|
4927
|
+
loadEnvFile();
|
|
4928
|
+
const effectiveModel = model ?? DEFAULT_AGENT_MODEL;
|
|
4929
|
+
printBanner(effectiveModel);
|
|
4930
|
+
const authResult = await authenticateAnthropic({ forceLogin, loginMethod, loginOrgUUID });
|
|
4931
|
+
if (authResult.method === "oauth") {
|
|
4932
|
+
const acctInfo = authResult.email ? ` (${authResult.email})` : "";
|
|
4933
|
+
process.stderr.write(import_picocolors14.default.dim(`Authenticated via OAuth${acctInfo}
|
|
4934
|
+
`));
|
|
4935
|
+
} else {
|
|
4936
|
+
process.stderr.write(import_picocolors14.default.dim("Authenticated via API key\n"));
|
|
4937
|
+
}
|
|
4938
|
+
const { session: connectedSession, kineticaVersion, degraded } = await connectWithRetry();
|
|
4939
|
+
session = connectedSession;
|
|
4940
|
+
await runAgent(session, kineticaVersion, degraded, model);
|
|
4941
|
+
}
|
|
4942
|
+
function getSession() {
|
|
4943
|
+
return session;
|
|
4944
|
+
}
|
|
4945
|
+
if (process.env.NODE_ENV !== "test") {
|
|
4946
|
+
main().catch((err) => {
|
|
4947
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
4948
|
+
process.stderr.write(import_picocolors14.default.red(`Error: ${message}
|
|
4949
|
+
`));
|
|
4950
|
+
if (verbose && err instanceof Error && err.stack) {
|
|
4951
|
+
process.stderr.write(err.stack + "\n");
|
|
4952
|
+
}
|
|
4953
|
+
process.exit(1);
|
|
4954
|
+
});
|
|
4955
|
+
}
|
|
4956
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
4957
|
+
0 && (module.exports = {
|
|
4958
|
+
getSession,
|
|
4959
|
+
main,
|
|
4960
|
+
verbose
|
|
4961
|
+
});
|