@deepsql/mcp 0.5.0 → 0.8.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/deepsql-phase1-lib.js +230 -36
- package/deepsql-phase1-server.js +1 -1
- package/package.json +5 -2
- package/src/api/client.js +1 -1
- package/src/cli.js +105 -9
- package/src/commands/_users.js +55 -0
- package/src/commands/access.js +225 -0
- package/src/commands/admin.test.js +135 -0
- package/src/commands/anti-patterns.js +77 -0
- package/src/commands/brain-context.js +68 -0
- package/src/commands/business-rules.js +59 -0
- package/src/commands/permissions.js +149 -0
- package/src/commands/relationships.js +56 -0
- package/src/commands/setup.js +220 -0
- package/src/commands/slow-queries.js +340 -0
- package/src/commands/users.js +276 -0
- package/src/ui/editor.js +61 -0
- package/src/ui/prompts.js +60 -0
- package/src/ui/sse.js +97 -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.8.0",
|
|
4
4
|
"description": "DeepSQL CLI and stdio MCP server for self-hosted deployments",
|
|
5
5
|
"bin": {
|
|
6
6
|
"deepsql": "./bin/deepsql.js",
|
|
@@ -22,5 +22,8 @@
|
|
|
22
22
|
"engines": {
|
|
23
23
|
"node": ">=20"
|
|
24
24
|
},
|
|
25
|
-
"license": "UNLICENSED"
|
|
25
|
+
"license": "UNLICENSED",
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@inquirer/prompts": "^8.4.2"
|
|
28
|
+
}
|
|
26
29
|
}
|
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/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,14 +19,28 @@ 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"),
|
|
32
|
+
users: () => require("./commands/users"),
|
|
33
|
+
access: () => require("./commands/access"),
|
|
34
|
+
permissions: () => require("./commands/permissions"),
|
|
35
|
+
"slow-queries": () => require("./commands/slow-queries"),
|
|
36
|
+
setup: () => require("./commands/setup"),
|
|
27
37
|
};
|
|
28
38
|
|
|
29
|
-
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.
|
|
30
44
|
|
|
31
45
|
Usage:
|
|
32
46
|
deepsql <command> [options]
|
|
@@ -47,14 +61,16 @@ Commands:
|
|
|
47
61
|
config path Print the auth file path.
|
|
48
62
|
mcp Run the stdio MCP server using the saved token.
|
|
49
63
|
connections list [--json] List database connections.
|
|
50
|
-
ask "<question>" --connection <name> [--chat <id>] [--json]
|
|
51
|
-
Ask DeepSQL a question.
|
|
52
64
|
query "<sql>" --connection <name> [--limit <n>] [--timeout-seconds <n>] [--file <path>] [--json]
|
|
53
|
-
Run a read-only SQL
|
|
65
|
+
Run a read-only SQL statement. Enforced
|
|
66
|
+
read-only at the backend (parser-level) and
|
|
67
|
+
ACL-checked per connection.
|
|
54
68
|
explain "<sql>" --connection <name> [--file <path>] [--json]
|
|
55
|
-
Get an EXPLAIN plan
|
|
69
|
+
Get an EXPLAIN plan (no ANALYZE — also
|
|
70
|
+
read-only enforced).
|
|
56
71
|
schema [tables|objects] --connection <name>
|
|
57
|
-
Dump schema or database objects
|
|
72
|
+
Dump connection schema or database objects
|
|
73
|
+
as JSON.
|
|
58
74
|
digest [N] [--connection <name>] [--json]
|
|
59
75
|
Show the latest DeepSQL digest, or pass a
|
|
60
76
|
number to list the last N (e.g. digest 5).
|
|
@@ -63,6 +79,53 @@ Commands:
|
|
|
63
79
|
digest show <id> [--connection <name>] [--json]
|
|
64
80
|
Show one digest by id.
|
|
65
81
|
|
|
82
|
+
Brain commands (give a coding agent DeepSQL's retrieved context — agentless V1):
|
|
83
|
+
brain-context "<question>" --connection <name> [--top-k <n>] [--json]
|
|
84
|
+
Retrieve embedding-ranked tables/columns/FKs,
|
|
85
|
+
training docs, and business rules for a
|
|
86
|
+
question. With --top-k, returns ranked
|
|
87
|
+
diagnostic snippets; otherwise returns the
|
|
88
|
+
rich training-context payload.
|
|
89
|
+
business-rules --connection <name> [--question "..."] [--json]
|
|
90
|
+
List active business rules and SQL guardrails
|
|
91
|
+
for a connection (optionally scoped by
|
|
92
|
+
question).
|
|
93
|
+
relationships --connection <name> [--json]
|
|
94
|
+
Inferred and validated foreign-key
|
|
95
|
+
relationships with confidence scores.
|
|
96
|
+
anti-patterns --connection <name> [--kind table|query] [--limit <n>] [--json]
|
|
97
|
+
Schema-level (table) or query-level
|
|
98
|
+
anti-patterns detected by the brain.
|
|
99
|
+
|
|
100
|
+
Admin commands (require ADMIN role on the calling token):
|
|
101
|
+
users list | get <ref> | add [<email>] [--role <r>] [--name <n>] [--password-stdin]
|
|
102
|
+
| set-role <ref> <role> | lock|unlock|disable <ref>
|
|
103
|
+
| resend-invite <ref> | reset-password <ref> [--password-stdin]
|
|
104
|
+
| delete <ref> [--yes]
|
|
105
|
+
Manage workspace users.
|
|
106
|
+
access list --user <ref> | --connection <name>
|
|
107
|
+
| grant --user <ref> --connection <name> --level read|write|admin
|
|
108
|
+
| revoke --user <ref> --connection <name>
|
|
109
|
+
| policy <user> <connection> (opens $EDITOR)
|
|
110
|
+
Per-connection access grants and chat policies.
|
|
111
|
+
permissions list [--role <ROLE>] [--json]
|
|
112
|
+
| override --role <ROLE> --permission <PERM> --grant|--revoke [--reason "..."]
|
|
113
|
+
| reset --role <ROLE> --permission <PERM>
|
|
114
|
+
Role-based permission overrides.
|
|
115
|
+
slow-queries latest --connection <name>
|
|
116
|
+
| history --connection <name> [N]
|
|
117
|
+
| analyze --connection <name> [--time-range LAST_24_HOURS|LAST_HOUR]
|
|
118
|
+
[--threshold-ms <n>] [--limit <n>]
|
|
119
|
+
| optimize --connection <name> --query-id <id>
|
|
120
|
+
(streams AI optimization steps to stderr; result to stdout)
|
|
121
|
+
| delete (--history-id <id> | --connection <name>) [--yes]
|
|
122
|
+
Read, trigger, and clean up slow-query analyses.
|
|
123
|
+
setup [--skip-email] [--skip-slack] [--skip-complete]
|
|
124
|
+
Post-install wizard: SMTP/email, Slack
|
|
125
|
+
(digests + bot), then mark setup complete.
|
|
126
|
+
Org and LLM config are set at install time
|
|
127
|
+
and are NOT touched by this wizard.
|
|
128
|
+
|
|
66
129
|
Global options:
|
|
67
130
|
--url <url> Override the DeepSQL base URL.
|
|
68
131
|
--token <tok> Override the auth token (also: DEEPSQL_AUTH_TOKEN).
|
|
@@ -118,21 +181,54 @@ function buildOpts(parsed) {
|
|
|
118
181
|
url: f.url || null,
|
|
119
182
|
token: f.token || null,
|
|
120
183
|
json: !!f.json,
|
|
184
|
+
// Login-flow selectors
|
|
121
185
|
device: !!f.device,
|
|
122
186
|
browser: !!f.browser,
|
|
123
187
|
noBrowser: !!f.noBrowser,
|
|
124
|
-
password
|
|
188
|
+
// password may be `true` (login flow flag) OR a string value (`users add
|
|
189
|
+
// --password secret`, `users add --password=secret`). Each command
|
|
190
|
+
// interprets whichever shape it expects.
|
|
191
|
+
password: f.password ?? null,
|
|
125
192
|
passwordStdin: !!f.passwordStdin,
|
|
126
193
|
email: f.email || null,
|
|
127
194
|
label: f.label || null,
|
|
195
|
+
// Connection / users / chat
|
|
128
196
|
connection: f.connection || f.c || null,
|
|
129
197
|
chat: f.chat || null,
|
|
130
198
|
user: f.user || null,
|
|
131
199
|
project: f.project || null,
|
|
200
|
+
name: f.name || null,
|
|
201
|
+
username: f.username || null,
|
|
202
|
+
role: f.role || null,
|
|
203
|
+
// List / pagination
|
|
132
204
|
limit: f.limit,
|
|
133
205
|
count: f.count || f.n || null,
|
|
134
206
|
timeoutSeconds: f.timeoutSeconds,
|
|
135
207
|
file: f.file || null,
|
|
208
|
+
// RBAC / access
|
|
209
|
+
level: f.level || null,
|
|
210
|
+
permission: f.permission || null,
|
|
211
|
+
grant: !!f.grant,
|
|
212
|
+
revoke: !!f.revoke,
|
|
213
|
+
reason: f.reason || null,
|
|
214
|
+
// Brain tools
|
|
215
|
+
topK: f.topK ?? null,
|
|
216
|
+
kind: f.kind || null,
|
|
217
|
+
question: f.question || null,
|
|
218
|
+
// Slow queries
|
|
219
|
+
timeRange: f.timeRange || null,
|
|
220
|
+
thresholdMs: f.thresholdMs || null,
|
|
221
|
+
queryId: f.queryId || null,
|
|
222
|
+
queryText: f.queryText || null,
|
|
223
|
+
sampleQuery: f.sampleQuery || null,
|
|
224
|
+
historyId: f.historyId || null,
|
|
225
|
+
// Setup wizard
|
|
226
|
+
force: !!f.force,
|
|
227
|
+
skipEmail: !!f.skipEmail,
|
|
228
|
+
skipSlack: !!f.skipSlack,
|
|
229
|
+
skipComplete: !!f.skipComplete,
|
|
230
|
+
// Confirmations
|
|
231
|
+
yes: !!f.yes || !!f.y,
|
|
136
232
|
};
|
|
137
233
|
}
|
|
138
234
|
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Resolve a user reference (numeric id, email, or username) to a {id, email,
|
|
5
|
+
* username, role, ...} record.
|
|
6
|
+
*
|
|
7
|
+
* Backend `GET /admin/users` returns the full list, so we fetch once per
|
|
8
|
+
* invocation and match locally. Cheap for typical org sizes.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const { request } = require("../api/client");
|
|
12
|
+
|
|
13
|
+
let cachedUsers = null;
|
|
14
|
+
|
|
15
|
+
async function listUsers(session) {
|
|
16
|
+
if (cachedUsers) return cachedUsers;
|
|
17
|
+
cachedUsers = await request(session.baseUrl, "/admin/users", { token: session.token });
|
|
18
|
+
if (!Array.isArray(cachedUsers)) cachedUsers = [];
|
|
19
|
+
return cachedUsers;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function clearUserCache() {
|
|
23
|
+
cachedUsers = null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function resolveUser(session, ref) {
|
|
27
|
+
if (ref == null || String(ref).trim() === "") {
|
|
28
|
+
throw new Error("Pass a user email, username, or numeric id.");
|
|
29
|
+
}
|
|
30
|
+
const trimmed = String(ref).trim();
|
|
31
|
+
|
|
32
|
+
// Numeric id — short-circuit if list isn't already cached.
|
|
33
|
+
if (/^\d+$/.test(trimmed)) {
|
|
34
|
+
const users = await listUsers(session);
|
|
35
|
+
const hit = users.find((u) => String(u.id) === trimmed);
|
|
36
|
+
if (hit) return hit;
|
|
37
|
+
throw new Error(`User id ${trimmed} not found.`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const users = await listUsers(session);
|
|
41
|
+
const lower = trimmed.toLowerCase();
|
|
42
|
+
const exactEmail = users.find((u) => (u.email || "").toLowerCase() === lower);
|
|
43
|
+
if (exactEmail) return exactEmail;
|
|
44
|
+
const exactUsername = users.find((u) => (u.username || "").toLowerCase() === lower);
|
|
45
|
+
if (exactUsername) return exactUsername;
|
|
46
|
+
|
|
47
|
+
const available = users
|
|
48
|
+
.map((u) => u.email || u.username)
|
|
49
|
+
.filter(Boolean)
|
|
50
|
+
.slice(0, 20);
|
|
51
|
+
const hint = available.length ? ` Available: ${available.join(", ")}.` : "";
|
|
52
|
+
throw new Error(`User "${trimmed}" not found.${hint}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
module.exports = { listUsers, resolveUser, clearUserCache };
|