@deepsql/mcp 0.18.1 → 0.19.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 +22 -3
- package/README.md +74 -6
- package/deepsql-phase1-lib.js +551 -0
- package/package.json +1 -1
- package/skills/SKILL_BODY.md +25 -5
- package/src/cli.js +14 -4
- package/src/cli.test.js +42 -0
- package/src/commands/slow-queries.js +1 -1
package/CLAUDE.md
CHANGED
|
@@ -33,9 +33,28 @@ server, and the statement you ran.** Don't be sloppy.
|
|
|
33
33
|
|
|
34
34
|
## The tools you have
|
|
35
35
|
|
|
36
|
-
The MCP server exposes
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
The MCP server exposes **41 tools** as of 0.19.0 — the original 16 +
|
|
37
|
+
0.18.x's 5 slow-query tools + 0.19.0's 20 CLI-parity additions. Most
|
|
38
|
+
take a `connectionId` (UUID returned by `list_connections`); a few
|
|
39
|
+
take server-resolved row ids (`apply_index_recommendation` →
|
|
40
|
+
`recommendationId`, `dismiss_index_recommendation` →
|
|
41
|
+
`recommendationId`, `acknowledge_growth_anomaly` → `anomalyId`,
|
|
42
|
+
`get_digest_by_id` → `digestId`); `get_current_user` and
|
|
43
|
+
`list_connections` take no args.
|
|
44
|
+
|
|
45
|
+
The surface mirrors the `deepsql` CLI for almost every read/diagnostic
|
|
46
|
+
operation. **Three CLI capabilities are intentionally NOT in MCP**:
|
|
47
|
+
|
|
48
|
+
1. **Connection write ops** (`add`, `update`, `remove`) — they take
|
|
49
|
+
plaintext DB credentials, which would land in your conversation
|
|
50
|
+
history. Tell the user to run `deepsql connections add` at a TTY.
|
|
51
|
+
2. **Auth flows** (`login`, `logout`, `setup`, `mcp config --install`) —
|
|
52
|
+
interactive by nature.
|
|
53
|
+
3. **Streaming AI optimization** (`slow-queries optimize`) — SSE
|
|
54
|
+
stream, doesn't fit JSON-RPC tools.
|
|
55
|
+
|
|
56
|
+
Per-user admin ops (`users`, `access`, `permissions`) are also CLI-only
|
|
57
|
+
in this version; ask before mid-session admin work.
|
|
39
58
|
|
|
40
59
|
| Tool | Purpose |
|
|
41
60
|
|---|---|
|
package/README.md
CHANGED
|
@@ -52,31 +52,99 @@ Restart the editor for the entry to load.
|
|
|
52
52
|
|
|
53
53
|
## What the MCP server exposes
|
|
54
54
|
|
|
55
|
-
|
|
56
|
-
at the SQL layer (only `execute_sql`,
|
|
57
|
-
`useAnalyze=true`, and
|
|
55
|
+
**41 tools** as of 0.19.0. Read-only at the schema/retrieval/diagnostics
|
|
56
|
+
layer; policy-gated at the SQL layer (only `execute_sql`,
|
|
57
|
+
`analyze_query_plan` with `useAnalyze=true`, and
|
|
58
|
+
`apply_index_recommendation` can write — the rest are reads or low-stakes
|
|
59
|
+
triggers like `reinit_connection_brain` and `acknowledge_growth_anomaly`).
|
|
60
|
+
|
|
61
|
+
The surface mirrors the `deepsql` CLI for every read/diagnostic
|
|
62
|
+
operation. **What's intentionally NOT in MCP**: connection write ops
|
|
63
|
+
(`add`/`update`/`remove` — they'd require plaintext DB credentials in
|
|
64
|
+
agent conversation history; manage at a TTY via `deepsql connections
|
|
65
|
+
add` instead), interactive auth flows (`login`/`logout`/`setup`/`mcp
|
|
66
|
+
config --install`), the SSE streaming `slow-queries optimize` flow, and
|
|
67
|
+
per-user admin ops (`users`/`access`/`permissions`).
|
|
68
|
+
|
|
69
|
+
### Connections + identity (5)
|
|
58
70
|
|
|
59
71
|
| Tool | Purpose |
|
|
60
72
|
|---|---|
|
|
61
73
|
| `list_connections` | Connections this token has access to |
|
|
74
|
+
| `get_current_user` | Authenticated user + role + bound DeepSQL host |
|
|
75
|
+
| `show_connection` | One connection's saved config, secrets masked |
|
|
76
|
+
| `test_connection` | Run the privilege report (+ SSH check); reuses saved creds — no plaintext crosses the wire |
|
|
77
|
+
| `reinit_connection_brain` | Trigger a fresh schema scan + brain re-embed |
|
|
78
|
+
|
|
79
|
+
### Schema + retrieval brain (5)
|
|
80
|
+
|
|
81
|
+
| Tool | Purpose |
|
|
82
|
+
|---|---|
|
|
62
83
|
| `get_schema` | Cached schema metadata (tables, columns, FKs, types) |
|
|
63
84
|
| `get_database_objects` | Tables, views, functions, procedures |
|
|
64
85
|
| `get_brain_context` | Retrieval brain: tables/columns/FKs/training docs/rules for a question |
|
|
65
86
|
| `list_business_rules` | Active business rules and SQL guardrails for a connection |
|
|
66
87
|
| `get_relationships` | Inferred + validated foreign keys with confidence scores |
|
|
88
|
+
|
|
89
|
+
### Anti-patterns + daily digest (4)
|
|
90
|
+
|
|
91
|
+
| Tool | Purpose |
|
|
92
|
+
|---|---|
|
|
67
93
|
| `get_anti_patterns` | Schema-level or query-level anti-patterns |
|
|
94
|
+
| `get_latest_digest` | Most recent DeepSQL daily digest (slow queries + AI commentary) |
|
|
95
|
+
| `list_digests` | Recent digest metadata (find one by date) |
|
|
96
|
+
| `get_digest_by_id` | Full digest body |
|
|
97
|
+
|
|
98
|
+
### Index advisor — workload-weighted + apply tool (5)
|
|
99
|
+
|
|
100
|
+
| Tool | Purpose |
|
|
101
|
+
|---|---|
|
|
68
102
|
| `get_index_recommendations` | Pre-computed top-N workload-weighted index recommendations |
|
|
69
103
|
| `apply_index_recommendation` | Apply (or dry-run with HypoPG) an index recommendation and measure benefit |
|
|
70
|
-
| `
|
|
71
|
-
| `
|
|
104
|
+
| `list_index_recommendations` | Browse the full recommendation history by status |
|
|
105
|
+
| `refresh_index_recommendations` | Force a fresh accumulation cycle (skip 6-hour scheduler wait) |
|
|
106
|
+
| `dismiss_index_recommendation` | Reject a recommendation explicitly |
|
|
107
|
+
|
|
108
|
+
### Index catalog diagnostics — live pg_stat_* / sys.* probes (5)
|
|
109
|
+
|
|
110
|
+
| Tool | Purpose |
|
|
111
|
+
|---|---|
|
|
112
|
+
| `get_missing_indexes` | Schema-walk view of suspected missing indexes |
|
|
113
|
+
| `get_index_health` | Total/bloated/unused/duplicate/biggest indexes summary |
|
|
114
|
+
| `get_unused_indexes` | Indexes with zero/near-zero scans (drop candidates) |
|
|
115
|
+
| `get_duplicate_indexes` | Redundant prefix-duplicate indexes |
|
|
116
|
+
| `get_table_index_usage` | Per-table scan/read/fetch counts on every index |
|
|
117
|
+
|
|
118
|
+
### Slow queries (9)
|
|
119
|
+
|
|
120
|
+
| Tool | Purpose |
|
|
121
|
+
|---|---|
|
|
122
|
+
| `analyze_slow_queries` | Recent slow queries with fingerprints, durations, examples (triggers fresh collection) |
|
|
123
|
+
| `get_latest_slow_query_analysis` | Read the most recent persisted analysis (no new work) |
|
|
124
|
+
| `list_slow_query_history` | List past analysis runs (compact metadata) |
|
|
125
|
+
| `get_slow_query_timeline` | Day-by-day timeline for one fingerprint from the 30-day store |
|
|
72
126
|
| `get_query_regressions` | Slow queries that regressed on the latest daily run |
|
|
73
127
|
| `list_tracked_queries` | All fingerprints tracked in the 30-day store |
|
|
74
128
|
| `get_slow_query_customers` | Tenants ranked by total slow-query load |
|
|
75
129
|
| `get_query_samples` | Literal SQL samples (with bind values) for one fingerprint |
|
|
76
130
|
| `get_slow_query_insights` | Pre-computed AI insights — hotspots / remediation / tail-risk / plan-drift / skew |
|
|
77
|
-
|
|
131
|
+
|
|
132
|
+
(`optimize_slow_query` is also in MCP — see the slow-query family.)
|
|
133
|
+
|
|
134
|
+
### Growth analytics (5)
|
|
135
|
+
|
|
136
|
+
| Tool | Purpose |
|
|
137
|
+
|---|---|
|
|
78
138
|
| `get_table_growth` | Per-table size/row growth from persistent stats history |
|
|
79
139
|
| `get_growth_anomalies` | DeepSQL-flagged sudden growth spikes with severity and root-cause hints |
|
|
140
|
+
| `acknowledge_growth_anomaly` | Mark an anomaly as expected (silences `unacknowledgedOnly` queries) |
|
|
141
|
+
| `get_growth_config` | Read current alert thresholds and sensitivity |
|
|
142
|
+
| `set_growth_config` | Update alert thresholds (admin-gated server-side) |
|
|
143
|
+
|
|
144
|
+
### Plan + execute (2 — the write-capable pair)
|
|
145
|
+
|
|
146
|
+
| Tool | Purpose |
|
|
147
|
+
|---|---|
|
|
80
148
|
| `execute_sql` | Run any SQL — backend enforces role-based policy (developers read-only, admins can mutate with two-step confirm) |
|
|
81
149
|
| `analyze_query_plan` | AI-enriched plan analysis (parsed plan tree, performance issues, index recommendations, written summary that uses the connection's schema + business rules) |
|
|
82
150
|
|
package/deepsql-phase1-lib.js
CHANGED
|
@@ -518,6 +518,352 @@ const TOOL_DEFINITIONS = [
|
|
|
518
518
|
additionalProperties: false,
|
|
519
519
|
},
|
|
520
520
|
},
|
|
521
|
+
|
|
522
|
+
// ─── Phase A symmetry: tools added to match the `deepsql` CLI surface ───
|
|
523
|
+
//
|
|
524
|
+
// Each tool below mirrors a CLI subcommand that previously had no MCP
|
|
525
|
+
// equivalent. Connection write operations (add/update/remove) are
|
|
526
|
+
// intentionally NOT exposed as MCP tools — they require DB credentials
|
|
527
|
+
// and we don't want secrets crossing the agent's conversation history.
|
|
528
|
+
// Customers manage connections via `deepsql connections add` at a TTY.
|
|
529
|
+
|
|
530
|
+
{
|
|
531
|
+
name: "get_current_user",
|
|
532
|
+
description:
|
|
533
|
+
"Return the authenticated user behind the current MCP token: username, role, "
|
|
534
|
+
+ "and the DeepSQL host this MCP server is bound to. Use this when the agent "
|
|
535
|
+
+ "needs to know whether the caller is admin-capable before suggesting a "
|
|
536
|
+
+ "DDL/DML run, or when explaining role-based restrictions to the user.",
|
|
537
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
538
|
+
},
|
|
539
|
+
{
|
|
540
|
+
name: "test_connection",
|
|
541
|
+
description:
|
|
542
|
+
"Validate a saved connection: contacts the database, runs the privilege "
|
|
543
|
+
+ "report, and returns whether the connection (and SSH tunnel, if any) is "
|
|
544
|
+
+ "reachable. Read-only on the customer's DB. Use when diagnosing a `?:` "
|
|
545
|
+
+ "Connected status from `list_connections` or before suggesting a SQL run "
|
|
546
|
+
+ "against a connection the agent hasn't touched yet. Takes a saved "
|
|
547
|
+
+ "connectionId only — does NOT accept ad-hoc credentials (those go through "
|
|
548
|
+
+ "`deepsql connections add` at a terminal, not chat).",
|
|
549
|
+
inputSchema: {
|
|
550
|
+
type: "object",
|
|
551
|
+
properties: {
|
|
552
|
+
connectionId: { type: "string", description: "DeepSQL connection ID." },
|
|
553
|
+
},
|
|
554
|
+
required: ["connectionId"],
|
|
555
|
+
additionalProperties: false,
|
|
556
|
+
},
|
|
557
|
+
},
|
|
558
|
+
{
|
|
559
|
+
name: "show_connection",
|
|
560
|
+
description:
|
|
561
|
+
"Return the full saved configuration for one connection with all secret "
|
|
562
|
+
+ "fields masked as `(set)`. Useful for diagnosing connection issues "
|
|
563
|
+
+ "(host/port/SSL/SSH config). Will never echo a password back; use "
|
|
564
|
+
+ "`deepsql connections show` at a TTY if you genuinely need to see "
|
|
565
|
+
+ "a secret value.",
|
|
566
|
+
inputSchema: {
|
|
567
|
+
type: "object",
|
|
568
|
+
properties: {
|
|
569
|
+
connectionId: { type: "string", description: "DeepSQL connection ID." },
|
|
570
|
+
},
|
|
571
|
+
required: ["connectionId"],
|
|
572
|
+
additionalProperties: false,
|
|
573
|
+
},
|
|
574
|
+
},
|
|
575
|
+
{
|
|
576
|
+
name: "reinit_connection_brain",
|
|
577
|
+
description:
|
|
578
|
+
"Trigger a fresh brain initialization for a connection: re-scans the "
|
|
579
|
+
+ "schema, re-runs key-column / FK inference, re-embeds training context. "
|
|
580
|
+
+ "Use after the user reports DeepSQL's schema knowledge is stale (e.g., "
|
|
581
|
+
+ "they just ran a migration). Returns immediately with an init-status "
|
|
582
|
+
+ "row; the actual reinit runs in the background — poll connection state "
|
|
583
|
+
+ "via `list_connections` to see when it transitions back to COMPLETED.",
|
|
584
|
+
inputSchema: {
|
|
585
|
+
type: "object",
|
|
586
|
+
properties: {
|
|
587
|
+
connectionId: { type: "string", description: "DeepSQL connection ID." },
|
|
588
|
+
force: {
|
|
589
|
+
type: "boolean",
|
|
590
|
+
description: "Restart even if a previous init is currently RUNNING. Defaults to false.",
|
|
591
|
+
},
|
|
592
|
+
},
|
|
593
|
+
required: ["connectionId"],
|
|
594
|
+
additionalProperties: false,
|
|
595
|
+
},
|
|
596
|
+
},
|
|
597
|
+
{
|
|
598
|
+
name: "get_latest_digest",
|
|
599
|
+
description:
|
|
600
|
+
"Return the most recent DeepSQL daily digest for a connection (or the "
|
|
601
|
+
+ "workspace if no connectionId). Digests are nightly summaries of slow "
|
|
602
|
+
+ "queries + AI commentary written to Slack. Useful for context when the "
|
|
603
|
+
+ "user asks 'what changed in the database recently?' without needing a "
|
|
604
|
+
+ "fresh slow-query analysis.",
|
|
605
|
+
inputSchema: {
|
|
606
|
+
type: "object",
|
|
607
|
+
properties: {
|
|
608
|
+
connectionId: {
|
|
609
|
+
type: "string",
|
|
610
|
+
description: "Optional. Filter to one connection. Omit for workspace-wide.",
|
|
611
|
+
},
|
|
612
|
+
},
|
|
613
|
+
additionalProperties: false,
|
|
614
|
+
},
|
|
615
|
+
},
|
|
616
|
+
{
|
|
617
|
+
name: "list_digests",
|
|
618
|
+
description:
|
|
619
|
+
"Return the N most recent DeepSQL daily digests (compact metadata, not full "
|
|
620
|
+
+ "body). Use to find the digest id for a date the user references "
|
|
621
|
+
+ "('what was in yesterday's digest?'), then fetch the full content via "
|
|
622
|
+
+ "`get_digest_by_id`.",
|
|
623
|
+
inputSchema: {
|
|
624
|
+
type: "object",
|
|
625
|
+
properties: {
|
|
626
|
+
connectionId: { type: "string", description: "Optional. Filter to one connection." },
|
|
627
|
+
count: { type: "integer", minimum: 1, maximum: 100, description: "Defaults to 10." },
|
|
628
|
+
},
|
|
629
|
+
additionalProperties: false,
|
|
630
|
+
},
|
|
631
|
+
},
|
|
632
|
+
{
|
|
633
|
+
name: "get_digest_by_id",
|
|
634
|
+
description:
|
|
635
|
+
"Return the full body of one DeepSQL daily digest by id, including the AI "
|
|
636
|
+
+ "narrative and the slow-query list it was built from.",
|
|
637
|
+
inputSchema: {
|
|
638
|
+
type: "object",
|
|
639
|
+
properties: {
|
|
640
|
+
digestId: { type: "string", description: "Digest id (from list_digests)." },
|
|
641
|
+
connectionId: {
|
|
642
|
+
type: "string",
|
|
643
|
+
description: "Optional. Required only if multiple connections share digest ids.",
|
|
644
|
+
},
|
|
645
|
+
},
|
|
646
|
+
required: ["digestId"],
|
|
647
|
+
additionalProperties: false,
|
|
648
|
+
},
|
|
649
|
+
},
|
|
650
|
+
{
|
|
651
|
+
name: "get_missing_indexes",
|
|
652
|
+
description:
|
|
653
|
+
"Catalog probe: returns indexes the DeepSQL advisor thinks are MISSING "
|
|
654
|
+
+ "based on live workload (joins to unindexed columns, sort/group on big "
|
|
655
|
+
+ "tables, etc.). This is the schema-walk view — for the workload-weighted "
|
|
656
|
+
+ "ROI-ranked recommendations (with HypoPG cost-delta), use "
|
|
657
|
+
+ "`get_index_recommendations` instead.",
|
|
658
|
+
inputSchema: {
|
|
659
|
+
type: "object",
|
|
660
|
+
properties: { connectionId: { type: "string", description: "DeepSQL connection ID." } },
|
|
661
|
+
required: ["connectionId"],
|
|
662
|
+
additionalProperties: false,
|
|
663
|
+
},
|
|
664
|
+
},
|
|
665
|
+
{
|
|
666
|
+
name: "get_index_health",
|
|
667
|
+
description:
|
|
668
|
+
"Comprehensive index health report for a connection: total indexes, "
|
|
669
|
+
+ "bloated indexes, unused indexes, duplicate-prefix indexes, biggest "
|
|
670
|
+
+ "indexes by size. Use as the first read when the user says 'audit my "
|
|
671
|
+
+ "indexes' or 'are my indexes healthy?'.",
|
|
672
|
+
inputSchema: {
|
|
673
|
+
type: "object",
|
|
674
|
+
properties: { connectionId: { type: "string", description: "DeepSQL connection ID." } },
|
|
675
|
+
required: ["connectionId"],
|
|
676
|
+
additionalProperties: false,
|
|
677
|
+
},
|
|
678
|
+
},
|
|
679
|
+
{
|
|
680
|
+
name: "get_unused_indexes",
|
|
681
|
+
description:
|
|
682
|
+
"Catalog probe: indexes the DB has reported zero (or near-zero) scans "
|
|
683
|
+
+ "against since last reset. Each returned row has the table, index name, "
|
|
684
|
+
+ "size, and scan count. Dropping these is a quick storage + write-cost "
|
|
685
|
+
+ "win, but verify scan counters aren't recent before suggesting.",
|
|
686
|
+
inputSchema: {
|
|
687
|
+
type: "object",
|
|
688
|
+
properties: { connectionId: { type: "string", description: "DeepSQL connection ID." } },
|
|
689
|
+
required: ["connectionId"],
|
|
690
|
+
additionalProperties: false,
|
|
691
|
+
},
|
|
692
|
+
},
|
|
693
|
+
{
|
|
694
|
+
name: "get_duplicate_indexes",
|
|
695
|
+
description:
|
|
696
|
+
"Catalog probe: indexes that are redundant prefixes of other indexes on "
|
|
697
|
+
+ "the same table. Returns groups — each group is a set of indexes the "
|
|
698
|
+
+ "optimizer would treat as interchangeable, with the recommendation to "
|
|
699
|
+
+ "keep the longest/widest one.",
|
|
700
|
+
inputSchema: {
|
|
701
|
+
type: "object",
|
|
702
|
+
properties: { connectionId: { type: "string", description: "DeepSQL connection ID." } },
|
|
703
|
+
required: ["connectionId"],
|
|
704
|
+
additionalProperties: false,
|
|
705
|
+
},
|
|
706
|
+
},
|
|
707
|
+
{
|
|
708
|
+
name: "get_table_index_usage",
|
|
709
|
+
description:
|
|
710
|
+
"Per-table index usage statistics: every index on the given table with its "
|
|
711
|
+
+ "scan count, tuples read, and tuples fetched. Use to diagnose 'why isn't "
|
|
712
|
+
+ "my index being used?' or to decide which of several composite indexes "
|
|
713
|
+
+ "to drop.",
|
|
714
|
+
inputSchema: {
|
|
715
|
+
type: "object",
|
|
716
|
+
properties: {
|
|
717
|
+
connectionId: { type: "string", description: "DeepSQL connection ID." },
|
|
718
|
+
tableName: { type: "string", description: "Table name (case-sensitive on Postgres)." },
|
|
719
|
+
},
|
|
720
|
+
required: ["connectionId", "tableName"],
|
|
721
|
+
additionalProperties: false,
|
|
722
|
+
},
|
|
723
|
+
},
|
|
724
|
+
{
|
|
725
|
+
name: "list_index_recommendations",
|
|
726
|
+
description:
|
|
727
|
+
"List ALL index recommendations for a connection, optionally filtered by "
|
|
728
|
+
+ "status (PENDING / APPLIED / DISMISSED). For just the top-N pending ones "
|
|
729
|
+
+ "with full evidence, use `get_index_recommendations` instead — that's the "
|
|
730
|
+
+ "agent-facing entry point. This tool is for browsing the recommendation "
|
|
731
|
+
+ "history (e.g. 'what did we apply last week?').",
|
|
732
|
+
inputSchema: {
|
|
733
|
+
type: "object",
|
|
734
|
+
properties: {
|
|
735
|
+
connectionId: { type: "string", description: "DeepSQL connection ID." },
|
|
736
|
+
status: {
|
|
737
|
+
type: "string",
|
|
738
|
+
enum: ["PENDING", "APPLIED", "DISMISSED"],
|
|
739
|
+
description: "Optional. Defaults to all statuses.",
|
|
740
|
+
},
|
|
741
|
+
},
|
|
742
|
+
required: ["connectionId"],
|
|
743
|
+
additionalProperties: false,
|
|
744
|
+
},
|
|
745
|
+
},
|
|
746
|
+
{
|
|
747
|
+
name: "refresh_index_recommendations",
|
|
748
|
+
description:
|
|
749
|
+
"Force a fresh accumulation cycle: rescans the slow-query log for the "
|
|
750
|
+
+ "lookback window and rebuilds the top-N index recommendations. Use when "
|
|
751
|
+
+ "the user just deployed a new query pattern and wants to see if the "
|
|
752
|
+
+ "advisor picks it up without waiting for the next 6-hour scheduler tick.",
|
|
753
|
+
inputSchema: {
|
|
754
|
+
type: "object",
|
|
755
|
+
properties: { connectionId: { type: "string", description: "DeepSQL connection ID." } },
|
|
756
|
+
required: ["connectionId"],
|
|
757
|
+
additionalProperties: false,
|
|
758
|
+
},
|
|
759
|
+
},
|
|
760
|
+
{
|
|
761
|
+
name: "dismiss_index_recommendation",
|
|
762
|
+
description:
|
|
763
|
+
"Mark a recommendation as DISMISSED so it stops appearing in `top` / "
|
|
764
|
+
+ "default `list` queries. Use when the user explicitly rejects a "
|
|
765
|
+
+ "recommendation (not the same as APPLIED — dismissed means 'we decided "
|
|
766
|
+
+ "not to'). Reversible by an admin editing the row directly.",
|
|
767
|
+
inputSchema: {
|
|
768
|
+
type: "object",
|
|
769
|
+
properties: {
|
|
770
|
+
recommendationId: { type: "string", description: "Recommendation row id." },
|
|
771
|
+
},
|
|
772
|
+
required: ["recommendationId"],
|
|
773
|
+
additionalProperties: false,
|
|
774
|
+
},
|
|
775
|
+
},
|
|
776
|
+
{
|
|
777
|
+
name: "get_latest_slow_query_analysis",
|
|
778
|
+
description:
|
|
779
|
+
"Return the most recently completed slow-query analysis run for a "
|
|
780
|
+
+ "connection (the persisted result, no fresh collection). Faster than "
|
|
781
|
+
+ "`analyze_slow_queries` because it doesn't trigger new work — use as "
|
|
782
|
+
+ "the first read when investigating 'what's slow right now?', then "
|
|
783
|
+
+ "fall back to `analyze_slow_queries` only if the latest is stale.",
|
|
784
|
+
inputSchema: {
|
|
785
|
+
type: "object",
|
|
786
|
+
properties: { connectionId: { type: "string", description: "DeepSQL connection ID." } },
|
|
787
|
+
required: ["connectionId"],
|
|
788
|
+
additionalProperties: false,
|
|
789
|
+
},
|
|
790
|
+
},
|
|
791
|
+
{
|
|
792
|
+
name: "list_slow_query_history",
|
|
793
|
+
description:
|
|
794
|
+
"List past slow-query analysis runs for a connection (compact metadata: "
|
|
795
|
+
+ "id, timestamp, count, severity breakdown, AI-summary length). Use to "
|
|
796
|
+
+ "find an older analysis to compare current state against, or to spot "
|
|
797
|
+
+ "trends in how many slow queries are firing day-over-day.",
|
|
798
|
+
inputSchema: {
|
|
799
|
+
type: "object",
|
|
800
|
+
properties: {
|
|
801
|
+
connectionId: { type: "string", description: "DeepSQL connection ID." },
|
|
802
|
+
limit: {
|
|
803
|
+
type: "integer",
|
|
804
|
+
minimum: 1,
|
|
805
|
+
maximum: 100,
|
|
806
|
+
description: "Number of past analyses to return. Defaults to 10.",
|
|
807
|
+
},
|
|
808
|
+
},
|
|
809
|
+
required: ["connectionId"],
|
|
810
|
+
additionalProperties: false,
|
|
811
|
+
},
|
|
812
|
+
},
|
|
813
|
+
{
|
|
814
|
+
name: "acknowledge_growth_anomaly",
|
|
815
|
+
description:
|
|
816
|
+
"Mark a detected growth anomaly as acknowledged so it stops appearing "
|
|
817
|
+
+ "in `get_growth_anomalies(unacknowledgedOnly=true)`. Use after the user "
|
|
818
|
+
+ "confirms the growth was expected (e.g., 'yes, we did a big backfill "
|
|
819
|
+
+ "yesterday'). Does NOT delete the anomaly — it remains in history for "
|
|
820
|
+
+ "audit/timeline purposes.",
|
|
821
|
+
inputSchema: {
|
|
822
|
+
type: "object",
|
|
823
|
+
properties: {
|
|
824
|
+
anomalyId: { type: "string", description: "Anomaly row id (from get_growth_anomalies)." },
|
|
825
|
+
},
|
|
826
|
+
required: ["anomalyId"],
|
|
827
|
+
additionalProperties: false,
|
|
828
|
+
},
|
|
829
|
+
},
|
|
830
|
+
{
|
|
831
|
+
name: "get_growth_config",
|
|
832
|
+
description:
|
|
833
|
+
"Return the alert thresholds and detection sensitivity currently "
|
|
834
|
+
+ "configured for table-growth monitoring on a connection. Useful to "
|
|
835
|
+
+ "explain to the user why a particular growth event did or didn't fire "
|
|
836
|
+
+ "an anomaly.",
|
|
837
|
+
inputSchema: {
|
|
838
|
+
type: "object",
|
|
839
|
+
properties: {
|
|
840
|
+
connectionId: { type: "string", description: "DeepSQL connection ID." },
|
|
841
|
+
},
|
|
842
|
+
required: ["connectionId"],
|
|
843
|
+
additionalProperties: false,
|
|
844
|
+
},
|
|
845
|
+
},
|
|
846
|
+
{
|
|
847
|
+
name: "set_growth_config",
|
|
848
|
+
description:
|
|
849
|
+
"Update the alert thresholds / sensitivity for growth monitoring on a "
|
|
850
|
+
+ "connection. Admin-gated. The config body shape matches what "
|
|
851
|
+
+ "`get_growth_config` returns — use that first to fetch current values, "
|
|
852
|
+
+ "then submit a modified copy.",
|
|
853
|
+
inputSchema: {
|
|
854
|
+
type: "object",
|
|
855
|
+
properties: {
|
|
856
|
+
connectionId: { type: "string", description: "DeepSQL connection ID." },
|
|
857
|
+
config: {
|
|
858
|
+
type: "object",
|
|
859
|
+
description: "Full config object (see get_growth_config for shape).",
|
|
860
|
+
additionalProperties: true,
|
|
861
|
+
},
|
|
862
|
+
},
|
|
863
|
+
required: ["connectionId", "config"],
|
|
864
|
+
additionalProperties: false,
|
|
865
|
+
},
|
|
866
|
+
},
|
|
521
867
|
];
|
|
522
868
|
|
|
523
869
|
class DeepSqlApiError extends Error {
|
|
@@ -1557,6 +1903,211 @@ async function handleToolCall(config, name, args = {}) {
|
|
|
1557
1903
|
return buildToolResult(name, payload);
|
|
1558
1904
|
}
|
|
1559
1905
|
|
|
1906
|
+
// ─── Phase A symmetry — tools added to match the CLI surface ─────────
|
|
1907
|
+
|
|
1908
|
+
case "get_current_user": {
|
|
1909
|
+
const payload = await callDeepSqlApi(config, "/auth/me");
|
|
1910
|
+
return buildToolResult(name, payload);
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1913
|
+
case "test_connection": {
|
|
1914
|
+
const connectionId = String(args.connectionId || "").trim();
|
|
1915
|
+
if (!connectionId) return buildToolError("connectionId is required.");
|
|
1916
|
+
// POST /connections/test with just { id } reuses the saved encrypted
|
|
1917
|
+
// creds server-side — no secrets cross the wire from the MCP client.
|
|
1918
|
+
const payload = await callDeepSqlApi(config, "/connections/test", {
|
|
1919
|
+
method: "POST",
|
|
1920
|
+
json: { id: connectionId },
|
|
1921
|
+
});
|
|
1922
|
+
return buildToolResult(name, payload);
|
|
1923
|
+
}
|
|
1924
|
+
|
|
1925
|
+
case "show_connection": {
|
|
1926
|
+
const connectionId = String(args.connectionId || "").trim();
|
|
1927
|
+
if (!connectionId) return buildToolError("connectionId is required.");
|
|
1928
|
+
// Backend has no GET /connections/{id}; list + filter mirrors CLI behavior.
|
|
1929
|
+
// Server returns secrets already masked.
|
|
1930
|
+
const all = await callDeepSqlApi(config, "/connections");
|
|
1931
|
+
const list = Array.isArray(all) ? all : [];
|
|
1932
|
+
const found = list.find((c) => (c.id || c.connectionId) === connectionId);
|
|
1933
|
+
if (!found) return buildToolError(`Connection ${connectionId} not found.`);
|
|
1934
|
+
return buildToolResult(name, found);
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1937
|
+
case "reinit_connection_brain": {
|
|
1938
|
+
const connectionId = String(args.connectionId || "").trim();
|
|
1939
|
+
if (!connectionId) return buildToolError("connectionId is required.");
|
|
1940
|
+
const payload = await callDeepSqlApi(
|
|
1941
|
+
config,
|
|
1942
|
+
`/connections/${encodeURIComponent(connectionId)}/reinit`,
|
|
1943
|
+
{ method: "POST", json: { force: args.force === true } },
|
|
1944
|
+
);
|
|
1945
|
+
return buildToolResult(name, payload);
|
|
1946
|
+
}
|
|
1947
|
+
|
|
1948
|
+
case "get_latest_digest": {
|
|
1949
|
+
const params = ["size=1"];
|
|
1950
|
+
if (args.connectionId) params.push(`connectionId=${encodeURIComponent(args.connectionId)}`);
|
|
1951
|
+
const payload = await callDeepSqlApi(config, `/admin/slack/digests?${params.join("&")}`);
|
|
1952
|
+
// Server returns a Page<>; surface the first element so consumers don't
|
|
1953
|
+
// have to know about Spring Data's content array.
|
|
1954
|
+
const first = Array.isArray(payload?.content) ? payload.content[0] : null;
|
|
1955
|
+
return buildToolResult(name, first || null);
|
|
1956
|
+
}
|
|
1957
|
+
|
|
1958
|
+
case "list_digests": {
|
|
1959
|
+
const count = clampInteger(args.count, 1, 100, 10);
|
|
1960
|
+
const params = [`size=${count}`];
|
|
1961
|
+
if (args.connectionId) params.push(`connectionId=${encodeURIComponent(args.connectionId)}`);
|
|
1962
|
+
const payload = await callDeepSqlApi(config, `/admin/slack/digests?${params.join("&")}`);
|
|
1963
|
+
return buildToolResult(name, Array.isArray(payload?.content) ? payload.content : []);
|
|
1964
|
+
}
|
|
1965
|
+
|
|
1966
|
+
case "get_digest_by_id": {
|
|
1967
|
+
const digestId = String(args.digestId || "").trim();
|
|
1968
|
+
if (!digestId) return buildToolError("digestId is required.");
|
|
1969
|
+
// No GET /admin/slack/digests/{id} — pull recent and filter.
|
|
1970
|
+
const params = ["size=100"];
|
|
1971
|
+
if (args.connectionId) params.push(`connectionId=${encodeURIComponent(args.connectionId)}`);
|
|
1972
|
+
const payload = await callDeepSqlApi(config, `/admin/slack/digests?${params.join("&")}`);
|
|
1973
|
+
const list = Array.isArray(payload?.content) ? payload.content : [];
|
|
1974
|
+
const found = list.find((d) => String(d.id) === digestId);
|
|
1975
|
+
if (!found) return buildToolError(`Digest ${digestId} not in the recent 100 digests.`);
|
|
1976
|
+
return buildToolResult(name, found);
|
|
1977
|
+
}
|
|
1978
|
+
|
|
1979
|
+
case "get_missing_indexes": {
|
|
1980
|
+
const connectionId = String(args.connectionId || "").trim();
|
|
1981
|
+
if (!connectionId) return buildToolError("connectionId is required.");
|
|
1982
|
+
const payload = await callDeepSqlApi(
|
|
1983
|
+
config, `/advisor/indexes/${encodeURIComponent(connectionId)}`);
|
|
1984
|
+
return buildToolResult(name, payload);
|
|
1985
|
+
}
|
|
1986
|
+
|
|
1987
|
+
case "get_index_health": {
|
|
1988
|
+
const connectionId = String(args.connectionId || "").trim();
|
|
1989
|
+
if (!connectionId) return buildToolError("connectionId is required.");
|
|
1990
|
+
const payload = await callDeepSqlApi(
|
|
1991
|
+
config, `/index-advisor/${encodeURIComponent(connectionId)}/health-report`);
|
|
1992
|
+
return buildToolResult(name, payload);
|
|
1993
|
+
}
|
|
1994
|
+
|
|
1995
|
+
case "get_unused_indexes": {
|
|
1996
|
+
const connectionId = String(args.connectionId || "").trim();
|
|
1997
|
+
if (!connectionId) return buildToolError("connectionId is required.");
|
|
1998
|
+
const payload = await callDeepSqlApi(
|
|
1999
|
+
config, `/index-advisor/${encodeURIComponent(connectionId)}/unused`);
|
|
2000
|
+
return buildToolResult(name, payload);
|
|
2001
|
+
}
|
|
2002
|
+
|
|
2003
|
+
case "get_duplicate_indexes": {
|
|
2004
|
+
const connectionId = String(args.connectionId || "").trim();
|
|
2005
|
+
if (!connectionId) return buildToolError("connectionId is required.");
|
|
2006
|
+
const payload = await callDeepSqlApi(
|
|
2007
|
+
config, `/index-advisor/${encodeURIComponent(connectionId)}/duplicates`);
|
|
2008
|
+
return buildToolResult(name, payload);
|
|
2009
|
+
}
|
|
2010
|
+
|
|
2011
|
+
case "get_table_index_usage": {
|
|
2012
|
+
const connectionId = String(args.connectionId || "").trim();
|
|
2013
|
+
const tableName = String(args.tableName || "").trim();
|
|
2014
|
+
if (!connectionId) return buildToolError("connectionId is required.");
|
|
2015
|
+
if (!tableName) return buildToolError("tableName is required.");
|
|
2016
|
+
const payload = await callDeepSqlApi(
|
|
2017
|
+
config,
|
|
2018
|
+
`/index-advisor/${encodeURIComponent(connectionId)}/usage/${encodeURIComponent(tableName)}`,
|
|
2019
|
+
);
|
|
2020
|
+
return buildToolResult(name, payload);
|
|
2021
|
+
}
|
|
2022
|
+
|
|
2023
|
+
case "list_index_recommendations": {
|
|
2024
|
+
const connectionId = String(args.connectionId || "").trim();
|
|
2025
|
+
if (!connectionId) return buildToolError("connectionId is required.");
|
|
2026
|
+
// The "list ALL" path returns every status; we filter client-side
|
|
2027
|
+
// for PENDING/APPLIED/DISMISSED to match the CLI semantics.
|
|
2028
|
+
const all = await callDeepSqlApi(
|
|
2029
|
+
config, `/index-recommendations/${encodeURIComponent(connectionId)}`);
|
|
2030
|
+
const list = Array.isArray(all) ? all : [];
|
|
2031
|
+
const filtered = args.status
|
|
2032
|
+
? list.filter((r) => String(r.status || "").toUpperCase() === String(args.status).toUpperCase())
|
|
2033
|
+
: list;
|
|
2034
|
+
return buildToolResult(name, filtered);
|
|
2035
|
+
}
|
|
2036
|
+
|
|
2037
|
+
case "refresh_index_recommendations": {
|
|
2038
|
+
const connectionId = String(args.connectionId || "").trim();
|
|
2039
|
+
if (!connectionId) return buildToolError("connectionId is required.");
|
|
2040
|
+
const payload = await callDeepSqlApi(
|
|
2041
|
+
config,
|
|
2042
|
+
`/index-recommendations/generate/${encodeURIComponent(connectionId)}`,
|
|
2043
|
+
{ method: "POST" },
|
|
2044
|
+
);
|
|
2045
|
+
return buildToolResult(name, payload);
|
|
2046
|
+
}
|
|
2047
|
+
|
|
2048
|
+
case "dismiss_index_recommendation": {
|
|
2049
|
+
const recommendationId = String(args.recommendationId || "").trim();
|
|
2050
|
+
if (!recommendationId) return buildToolError("recommendationId is required.");
|
|
2051
|
+
const payload = await callDeepSqlApi(
|
|
2052
|
+
config,
|
|
2053
|
+
`/index-recommendations/${encodeURIComponent(recommendationId)}/dismiss`,
|
|
2054
|
+
{ method: "PUT" },
|
|
2055
|
+
);
|
|
2056
|
+
return buildToolResult(name, payload);
|
|
2057
|
+
}
|
|
2058
|
+
|
|
2059
|
+
case "get_latest_slow_query_analysis": {
|
|
2060
|
+
const connectionId = String(args.connectionId || "").trim();
|
|
2061
|
+
if (!connectionId) return buildToolError("connectionId is required.");
|
|
2062
|
+
const payload = await callDeepSqlApi(
|
|
2063
|
+
config, `/slow-queries/latest/${encodeURIComponent(connectionId)}`);
|
|
2064
|
+
return buildToolResult(name, payload);
|
|
2065
|
+
}
|
|
2066
|
+
|
|
2067
|
+
case "list_slow_query_history": {
|
|
2068
|
+
const connectionId = String(args.connectionId || "").trim();
|
|
2069
|
+
if (!connectionId) return buildToolError("connectionId is required.");
|
|
2070
|
+
const limit = clampInteger(args.limit, 1, 100, 10);
|
|
2071
|
+
const payload = await callDeepSqlApi(
|
|
2072
|
+
config,
|
|
2073
|
+
`/slow-queries/history/${encodeURIComponent(connectionId)}?limit=${limit}`,
|
|
2074
|
+
);
|
|
2075
|
+
return buildToolResult(name, payload);
|
|
2076
|
+
}
|
|
2077
|
+
|
|
2078
|
+
case "acknowledge_growth_anomaly": {
|
|
2079
|
+
const anomalyId = String(args.anomalyId || "").trim();
|
|
2080
|
+
if (!anomalyId) return buildToolError("anomalyId is required.");
|
|
2081
|
+
const payload = await callDeepSqlApi(
|
|
2082
|
+
config,
|
|
2083
|
+
`/growth-monitoring/anomalies/${encodeURIComponent(anomalyId)}/acknowledge`,
|
|
2084
|
+
{ method: "POST" },
|
|
2085
|
+
);
|
|
2086
|
+
return buildToolResult(name, payload);
|
|
2087
|
+
}
|
|
2088
|
+
|
|
2089
|
+
case "get_growth_config": {
|
|
2090
|
+
const connectionId = String(args.connectionId || "").trim();
|
|
2091
|
+
if (!connectionId) return buildToolError("connectionId is required.");
|
|
2092
|
+
const payload = await callDeepSqlApi(
|
|
2093
|
+
config, `/growth-monitoring/config/${encodeURIComponent(connectionId)}`);
|
|
2094
|
+
return buildToolResult(name, payload);
|
|
2095
|
+
}
|
|
2096
|
+
|
|
2097
|
+
case "set_growth_config": {
|
|
2098
|
+
const connectionId = String(args.connectionId || "").trim();
|
|
2099
|
+
if (!connectionId) return buildToolError("connectionId is required.");
|
|
2100
|
+
if (!args.config || typeof args.config !== "object") {
|
|
2101
|
+
return buildToolError("config object is required (see get_growth_config for shape).");
|
|
2102
|
+
}
|
|
2103
|
+
const payload = await callDeepSqlApi(
|
|
2104
|
+
config,
|
|
2105
|
+
"/growth-monitoring/config",
|
|
2106
|
+
{ method: "POST", json: { ...args.config, connectionId } },
|
|
2107
|
+
);
|
|
2108
|
+
return buildToolResult(name, payload);
|
|
2109
|
+
}
|
|
2110
|
+
|
|
1560
2111
|
default:
|
|
1561
2112
|
return buildToolError(`Unknown tool: ${name}`);
|
|
1562
2113
|
}
|
package/package.json
CHANGED
package/skills/SKILL_BODY.md
CHANGED
|
@@ -2,13 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
You have two DeepSQL surfaces available:
|
|
4
4
|
|
|
5
|
-
1. **MCP tools** — JSON-RPC tools loaded into your session (
|
|
6
|
-
|
|
5
|
+
1. **MCP tools** — JSON-RPC tools loaded into your session (41 of them
|
|
6
|
+
as of 0.19.0). The MCP surface mirrors the CLI for almost every
|
|
7
|
+
read/diagnostic operation, plus the two execute tools and the index-
|
|
8
|
+
apply tool.
|
|
7
9
|
|
|
8
10
|
2. **`deepsql` CLI** — a shell binary on the user's PATH (~19 commands).
|
|
9
|
-
Use this for things
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
Use this for the few things still CLI-only: auth (`login`/`logout`),
|
|
12
|
+
the MCP installer (`deepsql mcp config --install`), connection
|
|
13
|
+
CRUD (anything that takes plaintext DB credentials — those must
|
|
14
|
+
stay out of your conversation history), the `setup` wizard, the
|
|
15
|
+
streaming `slow-queries optimize` SSE flow, and per-user admin ops
|
|
16
|
+
(`users`/`access`/`permissions`). You can shell out to the CLI
|
|
12
17
|
yourself when appropriate; you can also point the user at the
|
|
13
18
|
command if it's interactive.
|
|
14
19
|
|
|
@@ -98,6 +103,21 @@ usually doesn't know about either; that's exactly why DeepSQL exists.
|
|
|
98
103
|
| `get_growth_anomalies(connectionId, tableName?, unacknowledgedOnly?, days?)` | DeepSQL-flagged sudden growth spikes with severity (CRITICAL/WARNING/INFO), anomaly type, before/after sizes, confidence score. Check this BEFORE walking the user through a slow-query plan — a recent growth anomaly is often the real root cause. |
|
|
99
104
|
| `execute_sql(connectionId, query, ...)` | Run any SQL — SELECT for everyone, DML/DDL for admins (two-step confirm). |
|
|
100
105
|
| `analyze_query_plan(connectionId, query, useAnalyze=false)` | AI-enriched plan analysis (issues + index recs + summary). |
|
|
106
|
+
| `get_current_user()` | Authenticated user + role. Use to know whether the caller is admin-capable before suggesting DDL. |
|
|
107
|
+
| `test_connection(connectionId)` | Validates a saved connection (privilege report + SSH tunnel check). Read-only on the customer's DB. |
|
|
108
|
+
| `show_connection(connectionId)` | Full saved config with secrets masked. Diagnose host/port/SSL/SSH issues. |
|
|
109
|
+
| `reinit_connection_brain(connectionId, force?)` | Trigger a fresh schema scan + brain re-embedding. Use after the user reports stale schema knowledge. |
|
|
110
|
+
| `get_latest_digest(connectionId?)` / `list_digests(...)` / `get_digest_by_id(digestId, ...)` | Daily DeepSQL digests (slow queries + AI commentary written nightly to Slack). |
|
|
111
|
+
| `get_missing_indexes(connectionId)` | Schema-walk view of suspected missing indexes (complements the workload-weighted `get_index_recommendations`). |
|
|
112
|
+
| `get_index_health(connectionId)` | Total/bloated/unused/duplicate/biggest indexes. Use as first read on "audit my indexes". |
|
|
113
|
+
| `get_unused_indexes(connectionId)` / `get_duplicate_indexes(connectionId)` | Catalog probes for storage + write-cost wins. |
|
|
114
|
+
| `get_table_index_usage(connectionId, tableName)` | Per-table index scan/read/fetch counts. Diagnose "why isn't my index being used?". |
|
|
115
|
+
| `list_index_recommendations(connectionId, status?)` | Browse the full recommendation history (PENDING/APPLIED/DISMISSED). |
|
|
116
|
+
| `refresh_index_recommendations(connectionId)` | Force a fresh accumulation cycle (skips the 6-hour scheduler wait). |
|
|
117
|
+
| `dismiss_index_recommendation(recommendationId)` | Reject a recommendation. Use only after the user explicitly says no. |
|
|
118
|
+
| `get_latest_slow_query_analysis(connectionId)` / `list_slow_query_history(...)` | Read persisted slow-query analysis runs (faster than `analyze_slow_queries` which triggers new work). |
|
|
119
|
+
| `acknowledge_growth_anomaly(anomalyId)` | Mark a growth alert as expected. Use after the user confirms the growth was intentional. |
|
|
120
|
+
| `get_growth_config(connectionId)` / `set_growth_config(connectionId, config)` | View/edit growth-monitoring alert thresholds. |
|
|
101
121
|
|
|
102
122
|
`EXPLAIN` and `EXPLAIN ANALYZE` are just SQL — type them as the query if
|
|
103
123
|
you want raw plan output. Use `analyze_query_plan` when you want the
|
package/src/cli.js
CHANGED
|
@@ -383,12 +383,22 @@ const COMMAND_HELP = {
|
|
|
383
383
|
["analyze --connection <name> [options]", "Trigger a new analysis"],
|
|
384
384
|
["optimize --connection <name> --query-id <id>", "Stream AI optimization steps live (SSE)"],
|
|
385
385
|
["delete (--history-id <id> | --connection <name>) [--yes]", "Clean up history"],
|
|
386
|
+
["trends --connection <name>", "30-day tracked queries with delta metrics"],
|
|
387
|
+
["regressions --connection <name> [--min-factor <n>]", "Queries that got slower on the latest run"],
|
|
388
|
+
["timeline <fingerprint> --connection <name>", "Day-by-day metrics for one fingerprint"],
|
|
389
|
+
["customers --connection <name>", "Tenants ranked by total slow-query load"],
|
|
390
|
+
["samples <fingerprint> --connection <name> [--limit <n>]", "Literal SQL samples (with bind values) for a fingerprint"],
|
|
391
|
+
["insights --connection <name> [--kind <k>] [--window <w>]", "AI insights: hotspots, remediation, tail-risk, plan-drift, skew"],
|
|
392
|
+
["trigger --connection <name>", "Trigger an immediate analysis run"],
|
|
386
393
|
],
|
|
387
394
|
options: [
|
|
388
|
-
["--time-range LAST_24_HOURS|LAST_HOUR",
|
|
389
|
-
["--threshold-ms <n>",
|
|
390
|
-
["--limit <n>",
|
|
391
|
-
["--
|
|
395
|
+
["--time-range LAST_24_HOURS|LAST_HOUR", "Window for analyze"],
|
|
396
|
+
["--threshold-ms <n>", "Min duration to consider"],
|
|
397
|
+
["--limit <n>", "Cap results"],
|
|
398
|
+
["--min-factor <n>", "Min slowdown multiple for regressions (default 1.5)"],
|
|
399
|
+
["--kind hotspots|remediation|tail-risk|plan-drift|skew", "Insight category (default: all)"],
|
|
400
|
+
["--window LAST_24_HOURS|LAST_7_DAYS|LAST_30_DAYS", "Insights window (default LAST_7_DAYS)"],
|
|
401
|
+
["--json", "Raw JSON output"],
|
|
392
402
|
],
|
|
393
403
|
notes: "`optimize` follows the SSE protocol from /slow-queries/optimize/stream — step events go to stderr, the final result to stdout. Honors SIGINT.",
|
|
394
404
|
},
|
package/src/cli.test.js
CHANGED
|
@@ -116,3 +116,45 @@ test("--no-color suppresses ANSI escapes in help output", async () => {
|
|
|
116
116
|
// No ESC bytes anywhere.
|
|
117
117
|
assert.equal(/\x1b\[/.test(io.out()), false, "help output should be plain when --no-color is set");
|
|
118
118
|
});
|
|
119
|
+
|
|
120
|
+
// ─── help/dispatch drift guard ─────────────────────────────────────────────
|
|
121
|
+
//
|
|
122
|
+
// Every command-module SUBCOMMANDS map must be reflected in COMMAND_HELP so
|
|
123
|
+
// `deepsql <command> -h` actually documents what `deepsql <command> <sub>`
|
|
124
|
+
// will dispatch. A previous regression shipped four new slow-query
|
|
125
|
+
// subcommands that worked but weren't listed in -h — users assumed the
|
|
126
|
+
// install was stale. This test catches that class of bug at PR time.
|
|
127
|
+
//
|
|
128
|
+
// To extend: add a `{ command, modulePath }` entry. The module must export
|
|
129
|
+
// a `SUBCOMMANDS` object whose keys are the dispatchable subcommand names.
|
|
130
|
+
const HELP_DRIFT_TARGETS = [
|
|
131
|
+
{ command: "slow-queries", modulePath: "./commands/slow-queries" },
|
|
132
|
+
];
|
|
133
|
+
|
|
134
|
+
for (const { command, modulePath } of HELP_DRIFT_TARGETS) {
|
|
135
|
+
test(`COMMAND_HELP["${command}"] documents every dispatchable subcommand`, () => {
|
|
136
|
+
const mod = require(modulePath);
|
|
137
|
+
assert.ok(mod.SUBCOMMANDS, `${modulePath} must export SUBCOMMANDS for the drift guard`);
|
|
138
|
+
const help = COMMAND_HELP[command];
|
|
139
|
+
assert.ok(help, `COMMAND_HELP["${command}"] is missing`);
|
|
140
|
+
assert.ok(Array.isArray(help.subcommands), `COMMAND_HELP["${command}"].subcommands must be an array`);
|
|
141
|
+
|
|
142
|
+
const dispatchable = Object.keys(mod.SUBCOMMANDS).sort();
|
|
143
|
+
// Each help row is [usage-string, description]; the first whitespace-
|
|
144
|
+
// separated token of usage-string is the subcommand name.
|
|
145
|
+
const documented = help.subcommands
|
|
146
|
+
.map((row) => String(row[0]).trim().split(/\s+/)[0])
|
|
147
|
+
.sort();
|
|
148
|
+
const missing = dispatchable.filter((s) => !documented.includes(s));
|
|
149
|
+
const stale = documented.filter((s) => !dispatchable.includes(s));
|
|
150
|
+
|
|
151
|
+
assert.deepEqual(
|
|
152
|
+
{ missing, stale },
|
|
153
|
+
{ missing: [], stale: [] },
|
|
154
|
+
`\`deepsql ${command} -h\` is out of sync with src/commands/${command}.js:\n`
|
|
155
|
+
+ ` missing from help (callable but undocumented): ${missing.join(", ") || "none"}\n`
|
|
156
|
+
+ ` stale in help (documented but not callable): ${stale.join(", ") || "none"}\n`
|
|
157
|
+
+ `Fix: update COMMAND_HELP["${command}"].subcommands in src/cli.js.`,
|
|
158
|
+
);
|
|
159
|
+
});
|
|
160
|
+
}
|