@deepsql/mcp 0.16.0 → 0.18.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/CLAUDE.md +13 -2
- package/README.md +14 -2
- package/deepsql-phase1-lib.js +542 -0
- package/package.json +1 -1
- package/skills/SKILL_BODY.md +21 -3
- package/src/auth/store.js +3 -3
- package/src/cli.js +30 -0
- package/src/commands/growth.js +458 -0
- package/src/commands/growth.test.js +439 -0
- package/src/commands/mcp.js +9 -9
- package/src/commands/slow-queries.js +260 -1
- package/src/user-home.js +29 -0
|
@@ -11,6 +11,15 @@
|
|
|
11
11
|
* deepsql slow-queries optimize --connection <name> --query-id <id>
|
|
12
12
|
* (streams SSE; use --query-text to skip history lookup)
|
|
13
13
|
* deepsql slow-queries delete --history-id <id> [--yes]
|
|
14
|
+
* deepsql slow-queries trends --connection <name> [--json]
|
|
15
|
+
* deepsql slow-queries regressions --connection <name> [--min-factor <n>] [--json]
|
|
16
|
+
* deepsql slow-queries timeline <fingerprint> --connection <name> [--json]
|
|
17
|
+
* deepsql slow-queries customers --connection <name> [--json]
|
|
18
|
+
* deepsql slow-queries samples <fingerprint> --connection <name> [--limit <n>] [--json]
|
|
19
|
+
* deepsql slow-queries insights --connection <name>
|
|
20
|
+
* [--kind hotspots|remediation|tail-risk|plan-drift|skew]
|
|
21
|
+
* [--window LAST_7_DAYS|LAST_24_HOURS|LAST_30_DAYS] [--limit <n>] [--json]
|
|
22
|
+
* deepsql slow-queries trigger --connection <name> [--json]
|
|
14
23
|
*
|
|
15
24
|
* The optimize subcommand follows the SSE protocol from
|
|
16
25
|
* /slow-queries/optimize/stream — `step` events go to stderr, the final
|
|
@@ -29,12 +38,22 @@ const SUBCOMMANDS = {
|
|
|
29
38
|
analyze: cmdAnalyze,
|
|
30
39
|
optimize: cmdOptimize,
|
|
31
40
|
delete: cmdDelete,
|
|
41
|
+
trends: cmdTrends,
|
|
42
|
+
regressions: cmdRegressions,
|
|
43
|
+
timeline: cmdTimeline,
|
|
44
|
+
customers: cmdCustomers,
|
|
45
|
+
samples: cmdSamples,
|
|
46
|
+
insights: cmdInsights,
|
|
47
|
+
trigger: cmdTrigger,
|
|
32
48
|
};
|
|
33
49
|
|
|
34
50
|
async function run(opts, io = {}) {
|
|
35
51
|
const sub = opts.positional[0];
|
|
36
52
|
if (!sub) {
|
|
37
|
-
throw new Error(
|
|
53
|
+
throw new Error(
|
|
54
|
+
"Usage: deepsql slow-queries "
|
|
55
|
+
+ "<latest|history|analyze|optimize|delete|trends|regressions|timeline"
|
|
56
|
+
+ "|customers|samples|insights|trigger> ...");
|
|
38
57
|
}
|
|
39
58
|
const handler = SUBCOMMANDS[sub];
|
|
40
59
|
if (!handler) throw new Error(`Unknown slow-queries subcommand: ${sub}.`);
|
|
@@ -275,6 +294,246 @@ function safeParse(text) {
|
|
|
275
294
|
}
|
|
276
295
|
}
|
|
277
296
|
|
|
297
|
+
// ─── trends ──────────────────────────────────────────────────────────────
|
|
298
|
+
// 30-day analytics: tracked queries, regressions, and per-query timelines.
|
|
299
|
+
|
|
300
|
+
async function cmdTrends(opts, { stdout = process.stdout } = {}) {
|
|
301
|
+
const session = resolveSession(opts);
|
|
302
|
+
const connectionId = await resolveConnectionId(session, opts.connection);
|
|
303
|
+
const queries = await request(
|
|
304
|
+
session.baseUrl,
|
|
305
|
+
`/slow-query-analytics/${encodeURIComponent(connectionId)}/queries`,
|
|
306
|
+
{ token: session.token },
|
|
307
|
+
);
|
|
308
|
+
const items = Array.isArray(queries) ? queries : [];
|
|
309
|
+
if (opts.json) {
|
|
310
|
+
stdout.write(`${JSON.stringify(items, null, 2)}\n`);
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
if (items.length === 0) {
|
|
314
|
+
stdout.write("No tracked queries yet. The daily analysis runs at 01:30, "
|
|
315
|
+
+ "or trigger one now from the UI / API.\n");
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
printTrendRows(stdout, items);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
async function cmdRegressions(opts, { stdout = process.stdout } = {}) {
|
|
322
|
+
const session = resolveSession(opts);
|
|
323
|
+
const connectionId = await resolveConnectionId(session, opts.connection);
|
|
324
|
+
const minFactor = parseNum(opts["min-factor"]) || 1.5;
|
|
325
|
+
const rows = await request(
|
|
326
|
+
session.baseUrl,
|
|
327
|
+
`/slow-query-analytics/${encodeURIComponent(connectionId)}/regressions?minFactor=${minFactor}`,
|
|
328
|
+
{ token: session.token },
|
|
329
|
+
);
|
|
330
|
+
const items = Array.isArray(rows) ? rows : [];
|
|
331
|
+
if (opts.json) {
|
|
332
|
+
stdout.write(`${JSON.stringify(items, null, 2)}\n`);
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
if (items.length === 0) {
|
|
336
|
+
stdout.write(`No queries regressed by ${minFactor}x or more on the latest run.\n`);
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
printTable(stdout, [
|
|
340
|
+
{ key: "fingerprint", label: "QUERY ID" },
|
|
341
|
+
{ key: "factor", label: "SLOWDOWN" },
|
|
342
|
+
{ key: "meanMs", label: "MEAN MS" },
|
|
343
|
+
{ key: "calls", label: "CALLS" },
|
|
344
|
+
{ key: "sql", label: "QUERY" },
|
|
345
|
+
], items.map((r) => ({
|
|
346
|
+
fingerprint: trim(r.fingerprint || "", 14),
|
|
347
|
+
factor: r.regressionFactor != null ? `${r.regressionFactor.toFixed(2)}x` : "?",
|
|
348
|
+
meanMs: r.meanExecMs != null ? Math.round(r.meanExecMs).toString() : "?",
|
|
349
|
+
calls: String(r.callsDelta ?? "?"),
|
|
350
|
+
sql: trim(r.normalizedSql || "", 56),
|
|
351
|
+
})));
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
async function cmdTimeline(opts, { stdout = process.stdout } = {}) {
|
|
355
|
+
const session = resolveSession(opts);
|
|
356
|
+
const connectionId = await resolveConnectionId(session, opts.connection);
|
|
357
|
+
const fingerprint = opts.positional[0];
|
|
358
|
+
if (!fingerprint) {
|
|
359
|
+
throw new Error("Usage: deepsql slow-queries timeline <fingerprint> --connection <name>");
|
|
360
|
+
}
|
|
361
|
+
const points = await request(
|
|
362
|
+
session.baseUrl,
|
|
363
|
+
`/slow-query-analytics/${encodeURIComponent(connectionId)}`
|
|
364
|
+
+ `/timeline/${encodeURIComponent(fingerprint)}`,
|
|
365
|
+
{ token: session.token },
|
|
366
|
+
);
|
|
367
|
+
const items = Array.isArray(points) ? points : [];
|
|
368
|
+
if (opts.json) {
|
|
369
|
+
stdout.write(`${JSON.stringify(items, null, 2)}\n`);
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
if (items.length === 0) {
|
|
373
|
+
stdout.write("No timeline data for that query fingerprint.\n");
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
printTable(stdout, [
|
|
377
|
+
{ key: "day", label: "DAY" },
|
|
378
|
+
{ key: "calls", label: "CALLS" },
|
|
379
|
+
{ key: "meanMs", label: "MEAN MS" },
|
|
380
|
+
{ key: "maxMs", label: "MAX MS" },
|
|
381
|
+
{ key: "factor", label: "VS PREV" },
|
|
382
|
+
], items.map((p) => ({
|
|
383
|
+
day: String(p.day || ""),
|
|
384
|
+
calls: String(p.callsDelta ?? "?"),
|
|
385
|
+
meanMs: p.meanExecMs != null ? Math.round(p.meanExecMs).toString() : "?",
|
|
386
|
+
maxMs: p.maxExecMs != null ? Math.round(p.maxExecMs).toString() : "?",
|
|
387
|
+
factor: p.regressionFactor != null ? `${p.regressionFactor.toFixed(2)}x` : "—",
|
|
388
|
+
})));
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// ─── customers ─────────────────────────────────────────────────────────────
|
|
392
|
+
|
|
393
|
+
async function cmdCustomers(opts, { stdout = process.stdout } = {}) {
|
|
394
|
+
const session = resolveSession(opts);
|
|
395
|
+
const connectionId = await resolveConnectionId(session, opts.connection);
|
|
396
|
+
const rows = await request(
|
|
397
|
+
session.baseUrl,
|
|
398
|
+
`/slow-query-analytics/${encodeURIComponent(connectionId)}/customers`,
|
|
399
|
+
{ token: session.token },
|
|
400
|
+
);
|
|
401
|
+
const items = Array.isArray(rows) ? rows : [];
|
|
402
|
+
if (opts.json) {
|
|
403
|
+
stdout.write(`${JSON.stringify(items, null, 2)}\n`);
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
if (items.length === 0) {
|
|
407
|
+
stdout.write("No customer attribution data yet. Configure a tenant column in analytics settings.\n");
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
printTable(stdout, [
|
|
411
|
+
{ key: "rank", label: "#" },
|
|
412
|
+
{ key: "customer", label: "CUSTOMER" },
|
|
413
|
+
{ key: "name", label: "NAME" },
|
|
414
|
+
{ key: "queries", label: "QUERIES" },
|
|
415
|
+
{ key: "totalSec", label: "TOTAL (s)" },
|
|
416
|
+
], items.map((c, i) => ({
|
|
417
|
+
rank: String(i + 1),
|
|
418
|
+
customer: trim(c.customerId || "", 22),
|
|
419
|
+
name: trim(c.customerName || "—", 28),
|
|
420
|
+
queries: String(c.queryCount ?? "?"),
|
|
421
|
+
totalSec: c.totalExecMs != null ? (c.totalExecMs / 1000).toFixed(1) : "?",
|
|
422
|
+
})));
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// ─── samples ───────────────────────────────────────────────────────────────
|
|
426
|
+
|
|
427
|
+
async function cmdSamples(opts, { stdout = process.stdout } = {}) {
|
|
428
|
+
const session = resolveSession(opts);
|
|
429
|
+
const connectionId = await resolveConnectionId(session, opts.connection);
|
|
430
|
+
const fingerprint = opts.positional[0];
|
|
431
|
+
if (!fingerprint) {
|
|
432
|
+
throw new Error("Usage: deepsql slow-queries samples <fingerprint> --connection <name>");
|
|
433
|
+
}
|
|
434
|
+
const limit = parseNum(opts.limit) || 10;
|
|
435
|
+
const rows = await request(
|
|
436
|
+
session.baseUrl,
|
|
437
|
+
`/slow-query-analytics/${encodeURIComponent(connectionId)}`
|
|
438
|
+
+ `/query/${encodeURIComponent(fingerprint)}/samples`,
|
|
439
|
+
{ token: session.token },
|
|
440
|
+
);
|
|
441
|
+
const items = Array.isArray(rows) ? rows.slice(0, limit) : [];
|
|
442
|
+
if (opts.json) {
|
|
443
|
+
stdout.write(`${JSON.stringify(items, null, 2)}\n`);
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
if (items.length === 0) {
|
|
447
|
+
stdout.write("No samples found for that fingerprint.\n");
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
printTable(stdout, [
|
|
451
|
+
{ key: "execMs", label: "EXEC MS" },
|
|
452
|
+
{ key: "examined", label: "ROWS EXAM" },
|
|
453
|
+
{ key: "sent", label: "ROWS SENT" },
|
|
454
|
+
{ key: "source", label: "SOURCE" },
|
|
455
|
+
{ key: "captured", label: "CAPTURED AT" },
|
|
456
|
+
{ key: "sql", label: "SQL" },
|
|
457
|
+
], items.map((s) => ({
|
|
458
|
+
execMs: s.execMs != null ? String(Math.round(s.execMs)) : "?",
|
|
459
|
+
examined: String(s.rowsExamined ?? "?"),
|
|
460
|
+
sent: String(s.rowsSent ?? "?"),
|
|
461
|
+
source: trim(s.source || "?", 14),
|
|
462
|
+
captured: formatTime(s.capturedAt),
|
|
463
|
+
sql: trim(s.rawSql || "", 60),
|
|
464
|
+
})));
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// ─── insights ──────────────────────────────────────────────────────────────
|
|
468
|
+
|
|
469
|
+
async function cmdInsights(opts, { stdout = process.stdout } = {}) {
|
|
470
|
+
const session = resolveSession(opts);
|
|
471
|
+
const connectionId = await resolveConnectionId(session, opts.connection);
|
|
472
|
+
const kind = (opts.kind || "").toLowerCase();
|
|
473
|
+
const window = (opts.window || "LAST_7_DAYS").toUpperCase();
|
|
474
|
+
const limit = parseNum(opts.limit) || 10;
|
|
475
|
+
const subPath = kind && kind !== "all" ? `/${encodeURIComponent(kind)}` : "";
|
|
476
|
+
const rows = await request(
|
|
477
|
+
session.baseUrl,
|
|
478
|
+
`/slow-queries/insights/${encodeURIComponent(connectionId)}${subPath}`
|
|
479
|
+
+ `?window=${encodeURIComponent(window)}&limit=${limit}`,
|
|
480
|
+
{ token: session.token },
|
|
481
|
+
);
|
|
482
|
+
const items = Array.isArray(rows) ? rows : [];
|
|
483
|
+
if (opts.json) {
|
|
484
|
+
stdout.write(`${JSON.stringify(items, null, 2)}\n`);
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
if (items.length === 0) {
|
|
488
|
+
stdout.write(`No insights for window=${window}${kind ? ` kind=${kind}` : ""}.\n`);
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
for (let i = 0; i < items.length; i++) {
|
|
492
|
+
const ins = items[i];
|
|
493
|
+
const sev = ins.severity ? `[${ins.severity}] ` : "";
|
|
494
|
+
const title = ins.title || ins.insightType || "insight";
|
|
495
|
+
const desc = String(ins.description || ins.message || "").replace(/\s+/g, " ").trim();
|
|
496
|
+
stdout.write(`${i + 1}. ${sev}${title}\n`);
|
|
497
|
+
if (desc) stdout.write(` ${desc}\n`);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// ─── trigger ───────────────────────────────────────────────────────────────
|
|
502
|
+
|
|
503
|
+
async function cmdTrigger(opts, { stdout = process.stdout, stderr = process.stderr } = {}) {
|
|
504
|
+
const session = resolveSession(opts);
|
|
505
|
+
const connectionId = await resolveConnectionId(session, opts.connection);
|
|
506
|
+
stderr.write(`Triggering slow-query analysis for ${opts.connection}…\n`);
|
|
507
|
+
const result = await request(
|
|
508
|
+
session.baseUrl,
|
|
509
|
+
`/slow-query-analytics/${encodeURIComponent(connectionId)}/analyze-now`,
|
|
510
|
+
{ method: "POST", token: session.token, timeoutMs: 300000 },
|
|
511
|
+
);
|
|
512
|
+
if (opts.json) {
|
|
513
|
+
stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
const runId = result?.analysisRunId || result?.runId || "?";
|
|
517
|
+
const health = result?.overallHealth || result?.health || "";
|
|
518
|
+
stdout.write(`Analysis started. Run ID: ${runId}${health ? ` Health: ${health}` : ""}\n`);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function printTrendRows(stdout, items) {
|
|
522
|
+
printTable(stdout, [
|
|
523
|
+
{ key: "fingerprint", label: "QUERY ID" },
|
|
524
|
+
{ key: "meanMs", label: "MEAN MS" },
|
|
525
|
+
{ key: "calls", label: "CALLS" },
|
|
526
|
+
{ key: "factor", label: "VS PREV" },
|
|
527
|
+
{ key: "sql", label: "QUERY" },
|
|
528
|
+
], items.map((q) => ({
|
|
529
|
+
fingerprint: trim(q.fingerprint || "", 14),
|
|
530
|
+
meanMs: q.meanExecMs != null ? Math.round(q.meanExecMs).toString() : "?",
|
|
531
|
+
calls: String(q.callsDelta ?? "?"),
|
|
532
|
+
factor: q.regressionFactor != null ? `${q.regressionFactor.toFixed(2)}x` : "—",
|
|
533
|
+
sql: trim(q.normalizedSql || "", 54),
|
|
534
|
+
})));
|
|
535
|
+
}
|
|
536
|
+
|
|
278
537
|
function printHistory(stdout, items) {
|
|
279
538
|
const rows = items.map((h) => ({
|
|
280
539
|
id: String(h.id ?? h.historyId ?? ""),
|
package/src/user-home.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Resolves the real user's home directory, even when the CLI is run via sudo.
|
|
5
|
+
*
|
|
6
|
+
* Priority: HOME env var → SUDO_USER /etc/passwd lookup → os.homedir().
|
|
7
|
+
* The "/root" guard on HOME handles `sudo -H` (which sets HOME=/root) while
|
|
8
|
+
* still respecting `sudo -E` (which preserves the original HOME).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const fs = require("node:fs");
|
|
12
|
+
const os = require("node:os");
|
|
13
|
+
|
|
14
|
+
function userHome() {
|
|
15
|
+
if (process.env.HOME && process.env.HOME !== "/root") return process.env.HOME;
|
|
16
|
+
if (process.env.SUDO_USER) {
|
|
17
|
+
try {
|
|
18
|
+
const passwd = fs.readFileSync("/etc/passwd", "utf8");
|
|
19
|
+
const line = passwd.split("\n").find(l => l.startsWith(process.env.SUDO_USER + ":"));
|
|
20
|
+
if (line) {
|
|
21
|
+
const home = line.split(":")[5];
|
|
22
|
+
if (home) return home;
|
|
23
|
+
}
|
|
24
|
+
} catch (_) { /* fall through */ }
|
|
25
|
+
}
|
|
26
|
+
return os.homedir();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
module.exports = { userHome };
|