@deepsql/mcp 0.6.0 → 0.10.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/AGENT-SETUP.md +289 -0
- package/CLAUDE.md +330 -0
- package/deepsql-phase1-lib.js +230 -36
- package/deepsql-phase1-server.js +1 -1
- package/package.json +3 -1
- package/src/api/client.js +1 -1
- package/src/auth/store.js +32 -1
- package/src/auth/store.test.js +22 -0
- package/src/cli.js +81 -13
- package/src/commands/_connections.js +26 -3
- package/src/commands/_connections.test.js +21 -4
- package/src/commands/_session.js +4 -1
- package/src/commands/access.js +0 -2
- package/src/commands/admin.test.js +37 -0
- package/src/commands/anti-patterns.js +76 -0
- package/src/commands/brain-context.js +87 -0
- package/src/commands/business-rules.js +58 -0
- package/src/commands/connections.js +559 -9
- package/src/commands/digest.js +3 -5
- package/src/commands/explain.js +0 -1
- package/src/commands/query.js +0 -1
- package/src/commands/relationships.js +55 -0
- package/src/commands/schema.js +0 -1
- package/src/commands/slow-queries.js +2 -3
- package/src/commands/whoami.js +11 -5
- package/src/connections/schema.js +213 -0
- package/src/connections/secrets.js +167 -0
- package/src/connections/secrets.test.js +151 -0
- package/src/commands/ask.js +0 -44
package/deepsql-phase1-lib.js
CHANGED
|
@@ -66,29 +66,104 @@ const TOOL_DEFINITIONS = [
|
|
|
66
66
|
},
|
|
67
67
|
},
|
|
68
68
|
{
|
|
69
|
-
name: "
|
|
70
|
-
description:
|
|
69
|
+
name: "get_brain_context",
|
|
70
|
+
description:
|
|
71
|
+
"Retrieve DeepSQL's brain context for a question: relevant tables, columns, FKs, training docs, business rules, anti-patterns, and embedding-ranked snippets. Use this to give your own coding agent the same retrieval context the DeepSQL agent uses, then have your agent generate the SQL/answer.",
|
|
71
72
|
inputSchema: {
|
|
72
73
|
type: "object",
|
|
73
74
|
properties: {
|
|
74
|
-
connectionId: {
|
|
75
|
+
connectionId: { type: "string", description: "DeepSQL connection ID." },
|
|
76
|
+
question: {
|
|
75
77
|
type: "string",
|
|
76
|
-
description: "
|
|
78
|
+
description: "Natural-language question used for retrieval ranking.",
|
|
79
|
+
},
|
|
80
|
+
topK: {
|
|
81
|
+
type: "integer",
|
|
82
|
+
minimum: 1,
|
|
83
|
+
maximum: 100,
|
|
84
|
+
description:
|
|
85
|
+
"Optional retrieval breadth. When provided, returns ranked diagnostic results from /training/retrieve; otherwise returns the rich /training/context payload.",
|
|
77
86
|
},
|
|
87
|
+
},
|
|
88
|
+
required: ["connectionId", "question"],
|
|
89
|
+
additionalProperties: false,
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
name: "list_business_rules",
|
|
94
|
+
description:
|
|
95
|
+
"List active business rules and SQL guardrails for a connection. Optional `question` filters to rules applicable to that question.",
|
|
96
|
+
inputSchema: {
|
|
97
|
+
type: "object",
|
|
98
|
+
properties: {
|
|
99
|
+
connectionId: { type: "string", description: "DeepSQL connection ID." },
|
|
78
100
|
question: {
|
|
79
101
|
type: "string",
|
|
80
|
-
description: "
|
|
102
|
+
description: "Optional natural-language question to scope rules.",
|
|
81
103
|
},
|
|
82
|
-
|
|
104
|
+
},
|
|
105
|
+
required: ["connectionId"],
|
|
106
|
+
additionalProperties: false,
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
name: "get_relationships",
|
|
111
|
+
description:
|
|
112
|
+
"Get inferred and validated foreign-key relationships for a connection (source/target table+column with confidence and inference method).",
|
|
113
|
+
inputSchema: {
|
|
114
|
+
type: "object",
|
|
115
|
+
properties: {
|
|
116
|
+
connectionId: { type: "string", description: "DeepSQL connection ID." },
|
|
117
|
+
},
|
|
118
|
+
required: ["connectionId"],
|
|
119
|
+
additionalProperties: false,
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
name: "get_anti_patterns",
|
|
124
|
+
description:
|
|
125
|
+
"Get DeepSQL-detected anti-patterns. `kind=table` returns table/schema-level anti-patterns; `kind=query` returns query-level anti-patterns (with optional limit).",
|
|
126
|
+
inputSchema: {
|
|
127
|
+
type: "object",
|
|
128
|
+
properties: {
|
|
129
|
+
connectionId: { type: "string", description: "DeepSQL connection ID." },
|
|
130
|
+
kind: {
|
|
83
131
|
type: "string",
|
|
84
|
-
|
|
132
|
+
enum: ["table", "query"],
|
|
133
|
+
description: "Which anti-pattern catalog to fetch. Defaults to 'table'.",
|
|
85
134
|
},
|
|
86
|
-
|
|
87
|
-
type: "
|
|
88
|
-
|
|
135
|
+
limit: {
|
|
136
|
+
type: "integer",
|
|
137
|
+
minimum: 1,
|
|
138
|
+
maximum: 500,
|
|
139
|
+
description: "Optional row limit (only used for kind='query').",
|
|
89
140
|
},
|
|
90
141
|
},
|
|
91
|
-
required: ["connectionId"
|
|
142
|
+
required: ["connectionId"],
|
|
143
|
+
additionalProperties: false,
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
name: "analyze_slow_queries",
|
|
148
|
+
description:
|
|
149
|
+
"Analyze recent slow queries for a connection over the last 24 hours, returning fingerprints, durations, and example statements.",
|
|
150
|
+
inputSchema: {
|
|
151
|
+
type: "object",
|
|
152
|
+
properties: {
|
|
153
|
+
connectionId: { type: "string", description: "DeepSQL connection ID." },
|
|
154
|
+
thresholdMs: {
|
|
155
|
+
type: "number",
|
|
156
|
+
minimum: 1,
|
|
157
|
+
description: "Minimum query duration in milliseconds. Defaults to 100.",
|
|
158
|
+
},
|
|
159
|
+
limit: {
|
|
160
|
+
type: "integer",
|
|
161
|
+
minimum: 1,
|
|
162
|
+
maximum: 500,
|
|
163
|
+
description: "Maximum queries to return. Defaults to 10.",
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
required: ["connectionId"],
|
|
92
167
|
additionalProperties: false,
|
|
93
168
|
},
|
|
94
169
|
},
|
|
@@ -374,18 +449,64 @@ function summarizeObjects(payload) {
|
|
|
374
449
|
: "No database objects were returned.";
|
|
375
450
|
}
|
|
376
451
|
|
|
377
|
-
function
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
lines.push(`chatId: ${payload.chatId}`);
|
|
452
|
+
function summarizeBrainContext(payload) {
|
|
453
|
+
if (!payload || typeof payload !== "object") {
|
|
454
|
+
return "Brain context unavailable.";
|
|
381
455
|
}
|
|
382
|
-
|
|
383
|
-
|
|
456
|
+
// /training/retrieve diagnostic shape
|
|
457
|
+
if (Array.isArray(payload?.results) || payload?.totalResults != null) {
|
|
458
|
+
const total =
|
|
459
|
+
payload.totalResults ?? (Array.isArray(payload.results) ? payload.results.length : 0);
|
|
460
|
+
const tableCount = Array.isArray(payload.tablesCovered) ? payload.tablesCovered.length : 0;
|
|
461
|
+
return `Retrieved ${total} ranked snippet(s) covering ${tableCount} table(s).`;
|
|
384
462
|
}
|
|
385
|
-
|
|
386
|
-
|
|
463
|
+
// /training/context rich shape (RetrievedContextResult)
|
|
464
|
+
const tables = Array.isArray(payload.ragTableNames)
|
|
465
|
+
? payload.ragTableNames.length
|
|
466
|
+
: payload.ragTableNames
|
|
467
|
+
? Object.keys(payload.ragTableNames).length
|
|
468
|
+
: 0;
|
|
469
|
+
const types = payload.typeCounts
|
|
470
|
+
? Object.entries(payload.typeCounts).map(([k, v]) => `${k}=${v}`).join(", ")
|
|
471
|
+
: "";
|
|
472
|
+
const intent = payload.retrievalIntent || "n/a";
|
|
473
|
+
const skipped = payload.skipped ? ` (skipped: ${payload.skipReason || "?"})` : "";
|
|
474
|
+
return `Brain context: intent=${intent}, topK=${payload.retrievalTopK ?? "?"}, results=${payload.resultCount ?? 0}, tables=${tables}${types ? `, types[${types}]` : ""}${skipped}.`;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function summarizeBusinessRules(payload) {
|
|
478
|
+
const active = payload?.activeRuleCount ?? (payload?.activeRules?.length ?? 0);
|
|
479
|
+
const guards = payload?.applicableGuardrailCount ?? (payload?.applicableGuardrails?.length ?? 0);
|
|
480
|
+
return `Business rules: ${active} active, ${guards} applicable guardrail(s).`;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function summarizeRelationships(payload) {
|
|
484
|
+
const list = Array.isArray(payload) ? payload : payload?.relationships || [];
|
|
485
|
+
const high = list.filter((r) => (r.confidence ?? 0) >= 0.8).length;
|
|
486
|
+
return `${list.length} relationship(s) (${high} high-confidence).`;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function summarizeAntiPatterns(payload, kind) {
|
|
490
|
+
if (kind === "table") {
|
|
491
|
+
const tables = payload && typeof payload === "object" ? Object.keys(payload).length : 0;
|
|
492
|
+
return `Anti-patterns across ${tables} table(s).`;
|
|
387
493
|
}
|
|
388
|
-
|
|
494
|
+
const list = Array.isArray(payload) ? payload : payload?.patterns || [];
|
|
495
|
+
const sev = list.reduce((acc, p) => {
|
|
496
|
+
const s = p.severity || "UNKNOWN";
|
|
497
|
+
acc[s] = (acc[s] || 0) + 1;
|
|
498
|
+
return acc;
|
|
499
|
+
}, {});
|
|
500
|
+
const sevStr = Object.entries(sev).map(([k, v]) => `${k}=${v}`).join(", ");
|
|
501
|
+
return `${list.length} query anti-pattern(s)${sevStr ? ` (${sevStr})` : ""}.`;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function summarizeSlowQueries(payload) {
|
|
505
|
+
const list = Array.isArray(payload?.queries) ? payload.queries : [];
|
|
506
|
+
const total = payload?.totalCount ?? list.length;
|
|
507
|
+
const avg = payload?.avgDurationMs;
|
|
508
|
+
const max = payload?.maxDurationMs;
|
|
509
|
+
return `${total} slow query/queries${avg != null ? `, avg=${avg}ms` : ""}${max != null ? `, max=${max}ms` : ""}.`;
|
|
389
510
|
}
|
|
390
511
|
|
|
391
512
|
function summarizeQueryResult(payload) {
|
|
@@ -405,7 +526,7 @@ function summarizeExplain(payload) {
|
|
|
405
526
|
return `EXPLAIN completed for ${planType}.`;
|
|
406
527
|
}
|
|
407
528
|
|
|
408
|
-
function buildToolResult(name, payload) {
|
|
529
|
+
function buildToolResult(name, payload, extra = {}) {
|
|
409
530
|
let summary;
|
|
410
531
|
|
|
411
532
|
switch (name) {
|
|
@@ -418,8 +539,20 @@ function buildToolResult(name, payload) {
|
|
|
418
539
|
case "get_database_objects":
|
|
419
540
|
summary = summarizeObjects(payload);
|
|
420
541
|
break;
|
|
421
|
-
case "
|
|
422
|
-
summary =
|
|
542
|
+
case "get_brain_context":
|
|
543
|
+
summary = summarizeBrainContext(payload);
|
|
544
|
+
break;
|
|
545
|
+
case "list_business_rules":
|
|
546
|
+
summary = summarizeBusinessRules(payload);
|
|
547
|
+
break;
|
|
548
|
+
case "get_relationships":
|
|
549
|
+
summary = summarizeRelationships(payload);
|
|
550
|
+
break;
|
|
551
|
+
case "get_anti_patterns":
|
|
552
|
+
summary = summarizeAntiPatterns(payload, extra.kind || "table");
|
|
553
|
+
break;
|
|
554
|
+
case "analyze_slow_queries":
|
|
555
|
+
summary = summarizeSlowQueries(payload);
|
|
423
556
|
break;
|
|
424
557
|
case "execute_readonly_sql":
|
|
425
558
|
summary = summarizeQueryResult(payload);
|
|
@@ -484,23 +617,84 @@ async function handleToolCall(config, name, args = {}) {
|
|
|
484
617
|
return buildToolResult(name, payload);
|
|
485
618
|
}
|
|
486
619
|
|
|
487
|
-
case "
|
|
620
|
+
case "get_brain_context": {
|
|
488
621
|
const connectionId = String(args.connectionId || "").trim();
|
|
489
622
|
const question = String(args.question || "").trim();
|
|
490
|
-
|
|
491
|
-
|
|
623
|
+
if (!connectionId) return buildToolError("connectionId is required.");
|
|
624
|
+
if (!question) return buildToolError("question is required.");
|
|
625
|
+
|
|
626
|
+
// Route based on whether the caller wants ranked diagnostics (topK) or
|
|
627
|
+
// the rich training-context payload.
|
|
628
|
+
let payload;
|
|
629
|
+
if (args.topK != null) {
|
|
630
|
+
const topK = clampInteger(args.topK, 1, 100, 20);
|
|
631
|
+
const path =
|
|
632
|
+
`/training/retrieve/${encodeURIComponent(connectionId)}` +
|
|
633
|
+
`?q=${encodeURIComponent(question)}&topK=${topK}`;
|
|
634
|
+
payload = await callDeepSqlApi(config, path);
|
|
635
|
+
} else {
|
|
636
|
+
payload = await callDeepSqlApi(
|
|
637
|
+
config,
|
|
638
|
+
`/training/context/${encodeURIComponent(connectionId)}`,
|
|
639
|
+
{ method: "POST", json: { question } },
|
|
640
|
+
);
|
|
641
|
+
}
|
|
642
|
+
return buildToolResult(name, payload);
|
|
643
|
+
}
|
|
492
644
|
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
645
|
+
case "list_business_rules": {
|
|
646
|
+
const connectionId = String(args.connectionId || "").trim();
|
|
647
|
+
if (!connectionId) return buildToolError("connectionId is required.");
|
|
648
|
+
let path = `/business-rules/connection/${encodeURIComponent(connectionId)}`;
|
|
649
|
+
if (args.question) {
|
|
650
|
+
path += `?question=${encodeURIComponent(String(args.question))}`;
|
|
651
|
+
}
|
|
652
|
+
const payload = await callDeepSqlApi(config, path);
|
|
653
|
+
return buildToolResult(name, payload);
|
|
654
|
+
}
|
|
503
655
|
|
|
656
|
+
case "get_relationships": {
|
|
657
|
+
const connectionId = String(args.connectionId || "").trim();
|
|
658
|
+
if (!connectionId) return buildToolError("connectionId is required.");
|
|
659
|
+
const payload = await callDeepSqlApi(
|
|
660
|
+
config,
|
|
661
|
+
`/brain/inferred-relationships/${encodeURIComponent(connectionId)}`,
|
|
662
|
+
);
|
|
663
|
+
return buildToolResult(name, payload);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
case "get_anti_patterns": {
|
|
667
|
+
const connectionId = String(args.connectionId || "").trim();
|
|
668
|
+
if (!connectionId) return buildToolError("connectionId is required.");
|
|
669
|
+
const kind = args.kind === "query" ? "query" : "table";
|
|
670
|
+
let path;
|
|
671
|
+
if (kind === "query") {
|
|
672
|
+
path = `/brain/query-anti-patterns/${encodeURIComponent(connectionId)}`;
|
|
673
|
+
if (args.limit != null) {
|
|
674
|
+
path += `?limit=${clampInteger(args.limit, 1, 500, 50)}`;
|
|
675
|
+
}
|
|
676
|
+
} else {
|
|
677
|
+
path = `/brain/table-anti-patterns/${encodeURIComponent(connectionId)}`;
|
|
678
|
+
}
|
|
679
|
+
const payload = await callDeepSqlApi(config, path);
|
|
680
|
+
return buildToolResult(name, payload, { kind });
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
case "analyze_slow_queries": {
|
|
684
|
+
const connectionId = String(args.connectionId || "").trim();
|
|
685
|
+
if (!connectionId) return buildToolError("connectionId is required.");
|
|
686
|
+
const params = [];
|
|
687
|
+
if (args.thresholdMs != null) {
|
|
688
|
+
params.push(`threshold=${Number(args.thresholdMs)}`);
|
|
689
|
+
}
|
|
690
|
+
if (args.limit != null) {
|
|
691
|
+
params.push(`limit=${clampInteger(args.limit, 1, 500, 10)}`);
|
|
692
|
+
}
|
|
693
|
+
const qs = params.length ? `?${params.join("&")}` : "";
|
|
694
|
+
const payload = await callDeepSqlApi(
|
|
695
|
+
config,
|
|
696
|
+
`/slow-queries/analyze/${encodeURIComponent(connectionId)}${qs}`,
|
|
697
|
+
);
|
|
504
698
|
return buildToolResult(name, payload);
|
|
505
699
|
}
|
|
506
700
|
|
package/deepsql-phase1-server.js
CHANGED
|
@@ -193,7 +193,7 @@ class DeepSqlPhase1McpServer {
|
|
|
193
193
|
},
|
|
194
194
|
serverInfo: SERVER_INFO,
|
|
195
195
|
instructions:
|
|
196
|
-
"DeepSQL phase 1 MCP exposes read-only database access
|
|
196
|
+
"DeepSQL phase 1 MCP exposes read-only database access plus DeepSQL's brain (retrieved context, business rules, inferred relationships, anti-patterns, slow-query analysis). Prefer get_brain_context to ground SQL/answer generation in retrieved schema knowledge, then call execute_readonly_sql / explain_readonly_sql to validate. Mutating SQL is rejected by both client and backend.",
|
|
197
197
|
});
|
|
198
198
|
return;
|
|
199
199
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deepsql/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "DeepSQL CLI and stdio MCP server for self-hosted deployments",
|
|
5
5
|
"bin": {
|
|
6
6
|
"deepsql": "./bin/deepsql.js",
|
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
"main": "./deepsql-phase1-server.js",
|
|
10
10
|
"files": [
|
|
11
11
|
"README.md",
|
|
12
|
+
"CLAUDE.md",
|
|
13
|
+
"AGENT-SETUP.md",
|
|
12
14
|
"bin",
|
|
13
15
|
"src",
|
|
14
16
|
"deepsql-phase1-server.js",
|
package/src/api/client.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* needs:
|
|
9
9
|
* - profile-based base URL resolution (not env-only)
|
|
10
10
|
* - to talk to the unauthenticated /auth/cli endpoints
|
|
11
|
-
* -
|
|
11
|
+
* - per-call query string composition for brain endpoints
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
class ApiError extends Error {
|
package/src/auth/store.js
CHANGED
|
@@ -11,10 +11,18 @@
|
|
|
11
11
|
* {
|
|
12
12
|
* "default": "http://localhost:8080",
|
|
13
13
|
* "profiles": {
|
|
14
|
-
* "<base-url>": {
|
|
14
|
+
* "<base-url>": {
|
|
15
|
+
* token, username, tokenId, createdAt,
|
|
16
|
+
* defaultConnection?: "<connection-name-or-uuid>"
|
|
17
|
+
* }
|
|
15
18
|
* }
|
|
16
19
|
* }
|
|
17
20
|
*
|
|
21
|
+
* `defaultConnection` is the active connection for commands that need one
|
|
22
|
+
* (query/explain/schema/digest/slow-queries/brain-*). Set via
|
|
23
|
+
* `deepsql connections use <name>`. Resolution order in commands:
|
|
24
|
+
* --connection flag → DEEPSQL_CONNECTION env → profile.defaultConnection
|
|
25
|
+
*
|
|
18
26
|
* The file is written with mode 0600 and the parent dir with 0700. We refuse to
|
|
19
27
|
* read a file with looser perms unless DEEPSQL_INSECURE_AUTH=1 is set, since
|
|
20
28
|
* tokens grant access to the user's databases.
|
|
@@ -133,6 +141,27 @@ function defaultBaseUrl() {
|
|
|
133
141
|
return state.default || null;
|
|
134
142
|
}
|
|
135
143
|
|
|
144
|
+
function getDefaultConnection(baseUrl) {
|
|
145
|
+
const profile = getProfile(baseUrl);
|
|
146
|
+
return profile && profile.defaultConnection ? profile.defaultConnection : null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function setDefaultConnection(baseUrl, connectionName) {
|
|
150
|
+
const state = load();
|
|
151
|
+
const key = normalizeBaseUrl(baseUrl);
|
|
152
|
+
if (!state.profiles[key]) {
|
|
153
|
+
throw new Error(
|
|
154
|
+
`No profile saved for ${key}. Run \`deepsql login --url ${key}\` first.`,
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
if (connectionName == null || connectionName === "") {
|
|
158
|
+
delete state.profiles[key].defaultConnection;
|
|
159
|
+
} else {
|
|
160
|
+
state.profiles[key].defaultConnection = connectionName;
|
|
161
|
+
}
|
|
162
|
+
save(state);
|
|
163
|
+
}
|
|
164
|
+
|
|
136
165
|
function listProfiles() {
|
|
137
166
|
return load();
|
|
138
167
|
}
|
|
@@ -141,6 +170,7 @@ module.exports = {
|
|
|
141
170
|
authFilePath,
|
|
142
171
|
configDir,
|
|
143
172
|
defaultBaseUrl,
|
|
173
|
+
getDefaultConnection,
|
|
144
174
|
getProfile,
|
|
145
175
|
listProfiles,
|
|
146
176
|
load,
|
|
@@ -148,5 +178,6 @@ module.exports = {
|
|
|
148
178
|
removeProfile,
|
|
149
179
|
save,
|
|
150
180
|
setDefault,
|
|
181
|
+
setDefaultConnection,
|
|
151
182
|
setProfile,
|
|
152
183
|
};
|
package/src/auth/store.test.js
CHANGED
|
@@ -63,6 +63,28 @@ test("auth file is written with mode 0600", { skip: process.platform === "win32"
|
|
|
63
63
|
});
|
|
64
64
|
});
|
|
65
65
|
|
|
66
|
+
test("setDefaultConnection round-trips and clearing it removes the field", () => {
|
|
67
|
+
withTempStore((store) => {
|
|
68
|
+
store.setProfile("http://x", { token: "t", username: "a" });
|
|
69
|
+
assert.equal(store.getDefaultConnection("http://x"), null);
|
|
70
|
+
|
|
71
|
+
store.setDefaultConnection("http://x", "prod-replica");
|
|
72
|
+
assert.equal(store.getDefaultConnection("http://x"), "prod-replica");
|
|
73
|
+
|
|
74
|
+
store.setDefaultConnection("http://x", null);
|
|
75
|
+
assert.equal(store.getDefaultConnection("http://x"), null);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("setDefaultConnection refuses to write when the profile doesn't exist", () => {
|
|
80
|
+
withTempStore((store) => {
|
|
81
|
+
assert.throws(
|
|
82
|
+
() => store.setDefaultConnection("http://no-such-profile", "x"),
|
|
83
|
+
/No profile saved/,
|
|
84
|
+
);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
66
88
|
test("rejects loose perms unless DEEPSQL_INSECURE_AUTH=1", { skip: process.platform === "win32" }, () => {
|
|
67
89
|
withTempStore((store, dir) => {
|
|
68
90
|
store.setProfile("http://localhost:8080", { token: "t", username: "a" });
|
package/src/cli.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* - boolean flags: --json, --device, --browser, --no-browser
|
|
10
10
|
* - value flags: --url <url>, --token <t>, --connection <name>, --limit 50
|
|
11
11
|
* - subcommands: deepsql connections list, deepsql config show
|
|
12
|
-
* - positional: deepsql
|
|
12
|
+
* - positional: deepsql brain-context "which tables hold orders?"
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
const COMMANDS = {
|
|
@@ -19,10 +19,15 @@ const COMMANDS = {
|
|
|
19
19
|
config: () => require("./commands/config"),
|
|
20
20
|
mcp: () => require("./commands/mcp"),
|
|
21
21
|
connections: () => require("./commands/connections"),
|
|
22
|
-
ask: () => require("./commands/ask"),
|
|
23
22
|
query: () => require("./commands/query"),
|
|
24
23
|
explain: () => require("./commands/explain"),
|
|
25
24
|
schema: () => require("./commands/schema"),
|
|
25
|
+
// Brain tools — give a coding agent the same retrieval context the chat
|
|
26
|
+
// pipeline uses, then let the agent generate SQL/answers itself.
|
|
27
|
+
"brain-context": () => require("./commands/brain-context"),
|
|
28
|
+
"business-rules": () => require("./commands/business-rules"),
|
|
29
|
+
relationships: () => require("./commands/relationships"),
|
|
30
|
+
"anti-patterns": () => require("./commands/anti-patterns"),
|
|
26
31
|
digest: () => require("./commands/digest"),
|
|
27
32
|
users: () => require("./commands/users"),
|
|
28
33
|
access: () => require("./commands/access"),
|
|
@@ -31,7 +36,11 @@ const COMMANDS = {
|
|
|
31
36
|
setup: () => require("./commands/setup"),
|
|
32
37
|
};
|
|
33
38
|
|
|
34
|
-
const HELP = `deepsql —
|
|
39
|
+
const HELP = `deepsql — ask the database what's wrong. It already knows.
|
|
40
|
+
|
|
41
|
+
Self-hosted AI for database performance: profile slow queries, stream
|
|
42
|
+
live optimization plans, audit access, and run day-to-day admin ops —
|
|
43
|
+
all from the terminal, without leaving your VPC.
|
|
35
44
|
|
|
36
45
|
Usage:
|
|
37
46
|
deepsql <command> [options]
|
|
@@ -51,15 +60,41 @@ Commands:
|
|
|
51
60
|
config set-default <url> Set the default profile.
|
|
52
61
|
config path Print the auth file path.
|
|
53
62
|
mcp Run the stdio MCP server using the saved token.
|
|
54
|
-
connections list [--json] List database connections
|
|
55
|
-
|
|
56
|
-
|
|
63
|
+
connections list [--json] List database connections (active default
|
|
64
|
+
is marked with `*`).
|
|
65
|
+
connections use <name> Pin <name> as the active default; commands
|
|
66
|
+
drop --connection from then on.
|
|
67
|
+
connections current Print the active default (exit 1 if none).
|
|
68
|
+
connections unset Clear the active default for this profile.
|
|
69
|
+
connections schema [--json] Print the JSON Schema for the connection
|
|
70
|
+
config (the input format for \`add\`).
|
|
71
|
+
connections add [--from-file <p>] [--from-stdin] [--upsert] [--no-test]
|
|
72
|
+
[--wait] [--delete-after] [--cloud]
|
|
73
|
+
[--allow-plaintext-secrets]
|
|
74
|
+
Create a connection. Default is interactive
|
|
75
|
+
prompts; use --from-file for AI-agent flows.
|
|
76
|
+
connections update <name> --from-file <p>
|
|
77
|
+
PATCH-style update; omitted secrets are
|
|
78
|
+
preserved.
|
|
79
|
+
connections remove <name> [--yes]
|
|
80
|
+
Delete a connection (DELETE /connections).
|
|
81
|
+
connections test [<name> | --from-file <p>]
|
|
82
|
+
Validate a connection without saving.
|
|
83
|
+
Prints the privilege report.
|
|
84
|
+
connections show <name> [--json]
|
|
85
|
+
Show a connection's config (secrets masked).
|
|
86
|
+
connections init <name> [--force] [--wait]
|
|
87
|
+
Trigger brain re-initialization.
|
|
57
88
|
query "<sql>" --connection <name> [--limit <n>] [--timeout-seconds <n>] [--file <path>] [--json]
|
|
58
|
-
Run a read-only SQL
|
|
89
|
+
Run a read-only SQL statement. Enforced
|
|
90
|
+
read-only at the backend (parser-level) and
|
|
91
|
+
ACL-checked per connection.
|
|
59
92
|
explain "<sql>" --connection <name> [--file <path>] [--json]
|
|
60
|
-
Get an EXPLAIN plan
|
|
93
|
+
Get an EXPLAIN plan (no ANALYZE — also
|
|
94
|
+
read-only enforced).
|
|
61
95
|
schema [tables|objects] --connection <name>
|
|
62
|
-
Dump schema or database objects
|
|
96
|
+
Dump connection schema or database objects
|
|
97
|
+
as JSON.
|
|
63
98
|
digest [N] [--connection <name>] [--json]
|
|
64
99
|
Show the latest DeepSQL digest, or pass a
|
|
65
100
|
number to list the last N (e.g. digest 5).
|
|
@@ -68,6 +103,24 @@ Commands:
|
|
|
68
103
|
digest show <id> [--connection <name>] [--json]
|
|
69
104
|
Show one digest by id.
|
|
70
105
|
|
|
106
|
+
Brain commands (give a coding agent DeepSQL's retrieved context — agentless V1):
|
|
107
|
+
brain-context "<question>" --connection <name> [--top-k <n>] [--json]
|
|
108
|
+
Retrieve embedding-ranked tables/columns/FKs,
|
|
109
|
+
training docs, and business rules for a
|
|
110
|
+
question. With --top-k, returns ranked
|
|
111
|
+
diagnostic snippets; otherwise returns the
|
|
112
|
+
rich training-context payload.
|
|
113
|
+
business-rules --connection <name> [--question "..."] [--json]
|
|
114
|
+
List active business rules and SQL guardrails
|
|
115
|
+
for a connection (optionally scoped by
|
|
116
|
+
question).
|
|
117
|
+
relationships --connection <name> [--json]
|
|
118
|
+
Inferred and validated foreign-key
|
|
119
|
+
relationships with confidence scores.
|
|
120
|
+
anti-patterns --connection <name> [--kind table|query] [--limit <n>] [--json]
|
|
121
|
+
Schema-level (table) or query-level
|
|
122
|
+
anti-patterns detected by the brain.
|
|
123
|
+
|
|
71
124
|
Admin commands (require ADMIN role on the calling token):
|
|
72
125
|
users list | get <ref> | add [<email>] [--role <r>] [--name <n>] [--password-stdin]
|
|
73
126
|
| set-role <ref> <role> | lock|unlock|disable <ref>
|
|
@@ -98,10 +151,12 @@ Admin commands (require ADMIN role on the calling token):
|
|
|
98
151
|
and are NOT touched by this wizard.
|
|
99
152
|
|
|
100
153
|
Global options:
|
|
101
|
-
--url <url>
|
|
102
|
-
--token <tok>
|
|
103
|
-
|
|
104
|
-
|
|
154
|
+
--url <url> Override the DeepSQL base URL.
|
|
155
|
+
--token <tok> Override the auth token (also: DEEPSQL_AUTH_TOKEN).
|
|
156
|
+
--connection <name> Override the active connection (also: DEEPSQL_CONNECTION,
|
|
157
|
+
or pin one with \`deepsql connections use <name>\`).
|
|
158
|
+
-h, --help Show help.
|
|
159
|
+
-v, --version Show version.
|
|
105
160
|
`;
|
|
106
161
|
|
|
107
162
|
function parseArgs(argv) {
|
|
@@ -182,6 +237,10 @@ function buildOpts(parsed) {
|
|
|
182
237
|
grant: !!f.grant,
|
|
183
238
|
revoke: !!f.revoke,
|
|
184
239
|
reason: f.reason || null,
|
|
240
|
+
// Brain tools
|
|
241
|
+
topK: f.topK ?? null,
|
|
242
|
+
kind: f.kind || null,
|
|
243
|
+
question: f.question || null,
|
|
185
244
|
// Slow queries
|
|
186
245
|
timeRange: f.timeRange || null,
|
|
187
246
|
thresholdMs: f.thresholdMs || null,
|
|
@@ -196,6 +255,15 @@ function buildOpts(parsed) {
|
|
|
196
255
|
skipComplete: !!f.skipComplete,
|
|
197
256
|
// Confirmations
|
|
198
257
|
yes: !!f.yes || !!f.y,
|
|
258
|
+
// Connection management
|
|
259
|
+
fromFile: f.fromFile || null,
|
|
260
|
+
fromStdin: !!f.fromStdin,
|
|
261
|
+
upsert: !!f.upsert,
|
|
262
|
+
noTest: !!f.noTest,
|
|
263
|
+
wait: !!f.wait,
|
|
264
|
+
deleteAfter: !!f.deleteAfter,
|
|
265
|
+
cloud: !!f.cloud,
|
|
266
|
+
allowPlaintextSecrets: !!f.allowPlaintextSecrets,
|
|
199
267
|
};
|
|
200
268
|
}
|
|
201
269
|
|
|
@@ -29,11 +29,34 @@ async function listConnections(session) {
|
|
|
29
29
|
return cachedList;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Resolution chain for the connection a command should hit:
|
|
34
|
+
*
|
|
35
|
+
* 1. explicit `input` argument (i.e. opts.connection from --connection flag)
|
|
36
|
+
* 2. DEEPSQL_CONNECTION env var
|
|
37
|
+
* 3. session.defaultConnection (set via `deepsql connections use <name>`)
|
|
38
|
+
*
|
|
39
|
+
* If none of those produce a value, throw a friendly message that points the
|
|
40
|
+
* user at all three escape hatches.
|
|
41
|
+
*/
|
|
32
42
|
async function resolveConnectionId(session, input) {
|
|
33
|
-
|
|
34
|
-
|
|
43
|
+
let raw = input;
|
|
44
|
+
let source = "--connection";
|
|
45
|
+
if (raw == null || raw === "") {
|
|
46
|
+
raw = process.env.DEEPSQL_CONNECTION || null;
|
|
47
|
+
source = "DEEPSQL_CONNECTION";
|
|
48
|
+
}
|
|
49
|
+
if (raw == null || raw === "") {
|
|
50
|
+
raw = session && session.defaultConnection ? session.defaultConnection : null;
|
|
51
|
+
source = "saved default";
|
|
52
|
+
}
|
|
53
|
+
if (!raw || typeof raw !== "string") {
|
|
54
|
+
throw new Error(
|
|
55
|
+
"No connection specified. Pass --connection <name>, set DEEPSQL_CONNECTION, " +
|
|
56
|
+
"or run `deepsql connections use <name>` to pin a default.",
|
|
57
|
+
);
|
|
35
58
|
}
|
|
36
|
-
const trimmed =
|
|
59
|
+
const trimmed = raw.trim();
|
|
37
60
|
if (UUID_RE.test(trimmed)) return trimmed;
|
|
38
61
|
|
|
39
62
|
const connections = await listConnections(session);
|