@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
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `deepsql access` — per-connection access grants and chat-policy editing.
|
|
5
|
+
*
|
|
6
|
+
* deepsql access list --user <ref> → connections this user can see
|
|
7
|
+
* deepsql access list --connection <name> → users who can see this connection
|
|
8
|
+
* deepsql access grant --user <ref> --connection <name> --level read|write|admin
|
|
9
|
+
* deepsql access revoke --user <ref> --connection <name>
|
|
10
|
+
* deepsql access policy <user> <connection> → opens $EDITOR with the
|
|
11
|
+
* plain-English chat policy; on save, validates via the preview endpoint
|
|
12
|
+
* and PUTs the result.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const { ApiError, request } = require("../api/client");
|
|
16
|
+
const { resolveSession } = require("./_session");
|
|
17
|
+
const { resolveUser } = require("./_users");
|
|
18
|
+
const { resolveConnectionId, listConnections } = require("./_connections");
|
|
19
|
+
const { editText } = require("../ui/editor");
|
|
20
|
+
|
|
21
|
+
const SUBCOMMANDS = {
|
|
22
|
+
list: cmdList,
|
|
23
|
+
grant: cmdGrant,
|
|
24
|
+
revoke: cmdRevoke,
|
|
25
|
+
policy: cmdPolicy,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
async function run(opts, io = {}) {
|
|
29
|
+
const sub = opts.positional[0];
|
|
30
|
+
if (!sub) throw new Error("Usage: deepsql access <list|grant|revoke|policy> ...");
|
|
31
|
+
const handler = SUBCOMMANDS[sub];
|
|
32
|
+
if (!handler) throw new Error(`Unknown access subcommand: ${sub}.`);
|
|
33
|
+
return wrap(handler)(
|
|
34
|
+
{ ...opts, positional: opts.positional.slice(1) },
|
|
35
|
+
io,
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function wrap(handler) {
|
|
40
|
+
return async (opts, io) => {
|
|
41
|
+
try {
|
|
42
|
+
return await handler(opts, io);
|
|
43
|
+
} catch (err) {
|
|
44
|
+
if (err instanceof ApiError && err.status === 403) {
|
|
45
|
+
throw new Error("Access denied — managing access requires ADMIN role.");
|
|
46
|
+
}
|
|
47
|
+
throw err;
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ─── list ──────────────────────────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
async function cmdList(opts, { stdout = process.stdout } = {}) {
|
|
55
|
+
const session = resolveSession(opts);
|
|
56
|
+
if (opts.user) {
|
|
57
|
+
const user = await resolveUser(session, opts.user);
|
|
58
|
+
const grants = await request(session.baseUrl, `/admin/users/${user.id}/connection-access`, {
|
|
59
|
+
token: session.token,
|
|
60
|
+
});
|
|
61
|
+
if (opts.json) {
|
|
62
|
+
stdout.write(`${JSON.stringify(grants, null, 2)}\n`);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (!Array.isArray(grants) || grants.length === 0) {
|
|
66
|
+
stdout.write(`${user.email || user.username} has no connection grants.\n`);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
printGrants(stdout, grants, "user");
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (opts.connection) {
|
|
73
|
+
const connectionId = await resolveConnectionId(session, opts.connection);
|
|
74
|
+
const grants = await request(
|
|
75
|
+
session.baseUrl,
|
|
76
|
+
`/admin/connections/${encodeURIComponent(connectionId)}/connection-access`,
|
|
77
|
+
{ token: session.token },
|
|
78
|
+
);
|
|
79
|
+
if (opts.json) {
|
|
80
|
+
stdout.write(`${JSON.stringify(grants, null, 2)}\n`);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (!Array.isArray(grants) || grants.length === 0) {
|
|
84
|
+
stdout.write(`No grants on ${opts.connection}.\n`);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
printGrants(stdout, grants, "connection");
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
throw new Error("Pass --user <ref> or --connection <name>.");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ─── grant ─────────────────────────────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
async function cmdGrant(opts, { stdout = process.stdout } = {}) {
|
|
96
|
+
if (!opts.user) throw new Error("--user <ref> is required.");
|
|
97
|
+
if (!opts.connection) throw new Error("--connection <name> is required.");
|
|
98
|
+
const level = (opts.level || "READ").toUpperCase();
|
|
99
|
+
if (!["READ", "WRITE", "ADMIN"].includes(level)) {
|
|
100
|
+
throw new Error(`Invalid --level "${level}". Pick read, write, or admin.`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const session = resolveSession(opts);
|
|
104
|
+
const user = await resolveUser(session, opts.user);
|
|
105
|
+
const connectionId = await resolveConnectionId(session, opts.connection);
|
|
106
|
+
|
|
107
|
+
await request(
|
|
108
|
+
session.baseUrl,
|
|
109
|
+
`/admin/users/${user.id}/connection-access/${encodeURIComponent(connectionId)}`,
|
|
110
|
+
{
|
|
111
|
+
method: "PUT",
|
|
112
|
+
token: session.token,
|
|
113
|
+
json: { accessLevel: level },
|
|
114
|
+
},
|
|
115
|
+
);
|
|
116
|
+
stdout.write(
|
|
117
|
+
`Granted ${level} on ${opts.connection} to ${user.email || user.username}.\n`,
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ─── revoke ────────────────────────────────────────────────────────────────
|
|
122
|
+
|
|
123
|
+
async function cmdRevoke(opts, { stdout = process.stdout } = {}) {
|
|
124
|
+
if (!opts.user) throw new Error("--user <ref> is required.");
|
|
125
|
+
if (!opts.connection) throw new Error("--connection <name> is required.");
|
|
126
|
+
|
|
127
|
+
const session = resolveSession(opts);
|
|
128
|
+
const user = await resolveUser(session, opts.user);
|
|
129
|
+
const connectionId = await resolveConnectionId(session, opts.connection);
|
|
130
|
+
|
|
131
|
+
await request(
|
|
132
|
+
session.baseUrl,
|
|
133
|
+
`/admin/users/${user.id}/connection-access/${encodeURIComponent(connectionId)}`,
|
|
134
|
+
{ method: "DELETE", token: session.token },
|
|
135
|
+
);
|
|
136
|
+
stdout.write(`Revoked ${opts.connection} for ${user.email || user.username}.\n`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ─── policy ────────────────────────────────────────────────────────────────
|
|
140
|
+
|
|
141
|
+
const POLICY_HEADER =
|
|
142
|
+
"# Plain-English chat access policy. Lines starting with # are kept as-is —\n" +
|
|
143
|
+
"# DeepSQL doesn't strip them. Save and quit to commit; quit without changes\n" +
|
|
144
|
+
"# (e.g. :cq in vi) to abort.";
|
|
145
|
+
|
|
146
|
+
async function cmdPolicy(opts, { stdout = process.stdout, stderr = process.stderr } = {}) {
|
|
147
|
+
const userRef = opts.positional[0];
|
|
148
|
+
const connRef = opts.positional[1];
|
|
149
|
+
if (!userRef || !connRef) {
|
|
150
|
+
throw new Error("Usage: deepsql access policy <user> <connection>");
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const session = resolveSession(opts);
|
|
154
|
+
const user = await resolveUser(session, userRef);
|
|
155
|
+
const connectionId = await resolveConnectionId(session, connRef);
|
|
156
|
+
|
|
157
|
+
const existing = await request(
|
|
158
|
+
session.baseUrl,
|
|
159
|
+
`/admin/users/${user.id}/connection-access/${encodeURIComponent(connectionId)}/chat-policy`,
|
|
160
|
+
{ token: session.token },
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
const initial = (existing && existing.plainEnglishPolicy) || "";
|
|
164
|
+
|
|
165
|
+
stderr.write(
|
|
166
|
+
`Editing policy for ${user.email || user.username} on ${connRef}…\n`,
|
|
167
|
+
);
|
|
168
|
+
const { content, changed } = await editText(initial, {
|
|
169
|
+
suffix: ".policy.md",
|
|
170
|
+
header: POLICY_HEADER,
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
if (!changed) {
|
|
174
|
+
stderr.write("No changes.\n");
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Validate via preview before committing.
|
|
179
|
+
const preview = await request(session.baseUrl, "/admin/connection-chat-policies/preview", {
|
|
180
|
+
method: "POST",
|
|
181
|
+
token: session.token,
|
|
182
|
+
json: { connectionId, plainEnglishPolicy: content },
|
|
183
|
+
});
|
|
184
|
+
if (preview && preview.error) {
|
|
185
|
+
throw new Error(`Policy preview rejected: ${preview.error}`);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const saved = await request(
|
|
189
|
+
session.baseUrl,
|
|
190
|
+
`/admin/users/${user.id}/connection-access/${encodeURIComponent(connectionId)}/chat-policy`,
|
|
191
|
+
{
|
|
192
|
+
method: "PUT",
|
|
193
|
+
token: session.token,
|
|
194
|
+
json: { plainEnglishPolicy: content, active: true },
|
|
195
|
+
},
|
|
196
|
+
);
|
|
197
|
+
stdout.write(`Saved policy for ${user.email || user.username} on ${connRef}.\n`);
|
|
198
|
+
if (opts.json) {
|
|
199
|
+
stdout.write(`${JSON.stringify(saved, null, 2)}\n`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ─── helpers ───────────────────────────────────────────────────────────────
|
|
204
|
+
|
|
205
|
+
function printGrants(stdout, grants, mode) {
|
|
206
|
+
const rows = grants.map((g) => ({
|
|
207
|
+
a: mode === "user" ? (g.connectionName || g.connectionId || "") : (g.email || g.username || ""),
|
|
208
|
+
level: (g.accessLevel || g.level || "").toUpperCase(),
|
|
209
|
+
grantedBy: g.grantedBy || "",
|
|
210
|
+
}));
|
|
211
|
+
const headerA = mode === "user" ? "CONNECTION" : "USER";
|
|
212
|
+
const widthA = Math.max(headerA.length, ...rows.map((r) => r.a.length));
|
|
213
|
+
const widthLevel = Math.max("LEVEL".length, ...rows.map((r) => r.level.length));
|
|
214
|
+
const widthGrantedBy = Math.max("GRANTED BY".length, ...rows.map((r) => r.grantedBy.length));
|
|
215
|
+
stdout.write(
|
|
216
|
+
`${headerA.padEnd(widthA)} ${"LEVEL".padEnd(widthLevel)} ${"GRANTED BY".padEnd(widthGrantedBy)}\n` +
|
|
217
|
+
`${"-".repeat(widthA)} ${"-".repeat(widthLevel)} ${"-".repeat(widthGrantedBy)}\n`,
|
|
218
|
+
);
|
|
219
|
+
for (const r of rows) {
|
|
220
|
+
stdout.write(`${r.a.padEnd(widthA)} ${r.level.padEnd(widthLevel)} ${r.grantedBy.padEnd(widthGrantedBy)}\n`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// listConnections used in __mocks elsewhere; expose for tests if needed
|
|
225
|
+
module.exports = { run, _internal: { listConnections } };
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const test = require("node:test");
|
|
4
|
+
const assert = require("node:assert/strict");
|
|
5
|
+
|
|
6
|
+
const { parseArgs, buildOpts } = require("../cli");
|
|
7
|
+
|
|
8
|
+
function opts(argv) {
|
|
9
|
+
return buildOpts(parseArgs(argv));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// users -----------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
test("users add positional email + role/name flags", () => {
|
|
15
|
+
const o = opts(["add", "x@y.com", "--role", "DEVELOPER", "--name", "X"]);
|
|
16
|
+
assert.deepEqual(o.positional, ["add", "x@y.com"]);
|
|
17
|
+
assert.equal(o.role, "DEVELOPER");
|
|
18
|
+
assert.equal(o.name, "X");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("users delete --yes flag", () => {
|
|
22
|
+
const o = opts(["delete", "alice@x.com", "--yes"]);
|
|
23
|
+
assert.equal(o.yes, true);
|
|
24
|
+
assert.deepEqual(o.positional, ["delete", "alice@x.com"]);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("users reset-password --password-stdin flag", () => {
|
|
28
|
+
const o = opts(["reset-password", "alice@x.com", "--password-stdin"]);
|
|
29
|
+
assert.equal(o.passwordStdin, true);
|
|
30
|
+
assert.equal(o.password, null); // stays null since it's a separate flag
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// access ----------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
test("access grant requires user/connection/level", () => {
|
|
36
|
+
const o = opts([
|
|
37
|
+
"grant",
|
|
38
|
+
"--user",
|
|
39
|
+
"alice@x.com",
|
|
40
|
+
"--connection",
|
|
41
|
+
"mylocalpg",
|
|
42
|
+
"--level",
|
|
43
|
+
"read",
|
|
44
|
+
]);
|
|
45
|
+
assert.equal(o.user, "alice@x.com");
|
|
46
|
+
assert.equal(o.connection, "mylocalpg");
|
|
47
|
+
assert.equal(o.level, "read");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("access policy passes positional user + connection", () => {
|
|
51
|
+
const o = opts(["policy", "alice@x.com", "mylocalpg"]);
|
|
52
|
+
assert.deepEqual(o.positional, ["policy", "alice@x.com", "mylocalpg"]);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// permissions -----------------------------------------------------------------
|
|
56
|
+
|
|
57
|
+
test("permissions override --grant + --reason", () => {
|
|
58
|
+
const o = opts([
|
|
59
|
+
"override",
|
|
60
|
+
"--role",
|
|
61
|
+
"DEVELOPER",
|
|
62
|
+
"--permission",
|
|
63
|
+
"USE_CHAT",
|
|
64
|
+
"--grant",
|
|
65
|
+
"--reason",
|
|
66
|
+
"Beta access",
|
|
67
|
+
]);
|
|
68
|
+
assert.equal(o.role, "DEVELOPER");
|
|
69
|
+
assert.equal(o.permission, "USE_CHAT");
|
|
70
|
+
assert.equal(o.grant, true);
|
|
71
|
+
assert.equal(o.revoke, false);
|
|
72
|
+
assert.equal(o.reason, "Beta access");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("permissions override --revoke flips to revoke", () => {
|
|
76
|
+
const o = opts(["override", "--role", "DEVELOPER", "--permission", "USE_CHAT", "--revoke"]);
|
|
77
|
+
assert.equal(o.grant, false);
|
|
78
|
+
assert.equal(o.revoke, true);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// slow-queries ----------------------------------------------------------------
|
|
82
|
+
|
|
83
|
+
test("slow-queries history N positional", () => {
|
|
84
|
+
const o = opts(["history", "--connection", "mylocalpg", "5"]);
|
|
85
|
+
assert.deepEqual(o.positional, ["history", "5"]);
|
|
86
|
+
assert.equal(o.connection, "mylocalpg");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("slow-queries analyze flags", () => {
|
|
90
|
+
const o = opts([
|
|
91
|
+
"analyze",
|
|
92
|
+
"--connection",
|
|
93
|
+
"mylocalpg",
|
|
94
|
+
"--time-range",
|
|
95
|
+
"LAST_HOUR",
|
|
96
|
+
"--threshold-ms",
|
|
97
|
+
"200",
|
|
98
|
+
"--limit",
|
|
99
|
+
"20",
|
|
100
|
+
]);
|
|
101
|
+
assert.equal(o.timeRange, "LAST_HOUR");
|
|
102
|
+
assert.equal(o.thresholdMs, "200");
|
|
103
|
+
assert.equal(o.limit, "20");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("slow-queries optimize uses --query-id", () => {
|
|
107
|
+
const o = opts(["optimize", "--connection", "mylocalpg", "--query-id", "q-123"]);
|
|
108
|
+
assert.equal(o.queryId, "q-123");
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("slow-queries delete --history-id with --yes", () => {
|
|
112
|
+
const o = opts(["delete", "--history-id", "42", "--yes"]);
|
|
113
|
+
assert.equal(o.historyId, "42");
|
|
114
|
+
assert.equal(o.yes, true);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// setup -----------------------------------------------------------------------
|
|
118
|
+
|
|
119
|
+
test("setup --force --skip-email", () => {
|
|
120
|
+
const o = opts(["--force", "--skip-email"]);
|
|
121
|
+
assert.equal(o.force, true);
|
|
122
|
+
assert.equal(o.skipEmail, true);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// password flag stays a flag for login + value for users -----------------------
|
|
126
|
+
|
|
127
|
+
test("--password alone (login flow) leaves password truthy", () => {
|
|
128
|
+
const o = opts(["--password"]);
|
|
129
|
+
assert.equal(!!o.password, true);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test("--password=secret keeps the string value (still truthy)", () => {
|
|
133
|
+
const o = opts(["--password=secret"]);
|
|
134
|
+
assert.equal(o.password, "secret");
|
|
135
|
+
});
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `deepsql anti-patterns --connection <name> [--kind table|query] [--limit N] [--json]`
|
|
5
|
+
*
|
|
6
|
+
* Returns DeepSQL-detected anti-patterns. Two flavors:
|
|
7
|
+
* - kind=table (default) → GET /brain/table-anti-patterns/{cid}
|
|
8
|
+
* - kind=query → GET /brain/query-anti-patterns/{cid}?limit=
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const { request } = require("../api/client");
|
|
12
|
+
const { resolveSession } = require("./_session");
|
|
13
|
+
const { resolveConnectionId } = require("./_connections");
|
|
14
|
+
|
|
15
|
+
async function run(opts, { stdout = process.stdout } = {}) {
|
|
16
|
+
if (!opts.connection) throw new Error("--connection <name> is required.");
|
|
17
|
+
|
|
18
|
+
const session = resolveSession(opts);
|
|
19
|
+
const connectionId = await resolveConnectionId(session, opts.connection);
|
|
20
|
+
|
|
21
|
+
const kind = opts.kind === "query" ? "query" : "table";
|
|
22
|
+
const path =
|
|
23
|
+
kind === "query"
|
|
24
|
+
? `/brain/query-anti-patterns/${encodeURIComponent(connectionId)}`
|
|
25
|
+
: `/brain/table-anti-patterns/${encodeURIComponent(connectionId)}`;
|
|
26
|
+
|
|
27
|
+
const query = {};
|
|
28
|
+
if (kind === "query" && opts.limit != null) query.limit = opts.limit;
|
|
29
|
+
|
|
30
|
+
const response = await request(session.baseUrl, path, {
|
|
31
|
+
token: session.token,
|
|
32
|
+
query,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
if (opts.json) {
|
|
36
|
+
stdout.write(`${JSON.stringify(response, null, 2)}\n`);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (kind === "table") {
|
|
41
|
+
const tableMap =
|
|
42
|
+
response && typeof response === "object" && !Array.isArray(response) ? response : {};
|
|
43
|
+
const tables = Object.keys(tableMap);
|
|
44
|
+
if (tables.length === 0) {
|
|
45
|
+
stdout.write("No table-level anti-patterns detected.\n");
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const noun = tables.length === 1 ? "table" : "tables";
|
|
49
|
+
stdout.write(`${tables.length} ${noun} with anti-patterns:\n`);
|
|
50
|
+
for (const t of tables) {
|
|
51
|
+
const entry = tableMap[t] || {};
|
|
52
|
+
const patterns = entry.patterns || entry.antiPatterns || entry || [];
|
|
53
|
+
const n = Array.isArray(patterns) ? patterns.length : 0;
|
|
54
|
+
stdout.write(` • ${t}: ${n} ${n === 1 ? "pattern" : "patterns"}\n`);
|
|
55
|
+
}
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const list = Array.isArray(response) ? response : response?.patterns || [];
|
|
60
|
+
if (list.length === 0) {
|
|
61
|
+
stdout.write("No query anti-patterns detected.\n");
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const sev = list.reduce((acc, p) => {
|
|
65
|
+
const s = p.severity || "UNKNOWN";
|
|
66
|
+
acc[s] = (acc[s] || 0) + 1;
|
|
67
|
+
return acc;
|
|
68
|
+
}, {});
|
|
69
|
+
const sevStr = Object.entries(sev).map(([k, v]) => `${k}=${v}`).join(", ");
|
|
70
|
+
const noun = list.length === 1 ? "anti-pattern" : "anti-patterns";
|
|
71
|
+
stdout.write(`${list.length} query ${noun}${sevStr ? ` (${sevStr})` : ""}:\n`);
|
|
72
|
+
for (const p of list.slice(0, 20)) {
|
|
73
|
+
stdout.write(` • [${p.severity || "?"}] ${p.patternType || p.name || "pattern"}: ${p.description || ""}\n`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
module.exports = { run };
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `deepsql brain-context "<question>" --connection <name> [--top-k N] [--json]`
|
|
5
|
+
*
|
|
6
|
+
* Returns DeepSQL's retrieval context for a question — relevant tables,
|
|
7
|
+
* columns, FK relationships, training docs, business rules, and embedding-
|
|
8
|
+
* ranked snippets — without invoking the chat agent. Coding agents (Claude
|
|
9
|
+
* Code, Cursor, Codex) feed this into their own LLM to generate SQL or prose.
|
|
10
|
+
*
|
|
11
|
+
* - Without --top-k → POST /training/context/{cid} (rich payload)
|
|
12
|
+
* - With --top-k → GET /training/retrieve/{cid}?q=&topK= (ranked snippets)
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const { request } = require("../api/client");
|
|
16
|
+
const { resolveSession } = require("./_session");
|
|
17
|
+
const { resolveConnectionId } = require("./_connections");
|
|
18
|
+
|
|
19
|
+
async function run(opts, { stdout = process.stdout } = {}) {
|
|
20
|
+
const question = opts.positional.join(" ").trim();
|
|
21
|
+
if (!question) {
|
|
22
|
+
throw new Error(
|
|
23
|
+
'Pass a question: `deepsql brain-context "which tables hold customer orders?" --connection <name>`.',
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
if (!opts.connection) throw new Error("--connection <name> is required.");
|
|
27
|
+
|
|
28
|
+
const session = resolveSession(opts);
|
|
29
|
+
const connectionId = await resolveConnectionId(session, opts.connection);
|
|
30
|
+
|
|
31
|
+
const topK = opts.topK == null ? null : Number.parseInt(opts.topK, 10);
|
|
32
|
+
let response;
|
|
33
|
+
if (topK != null && Number.isFinite(topK)) {
|
|
34
|
+
response = await request(
|
|
35
|
+
session.baseUrl,
|
|
36
|
+
`/training/retrieve/${encodeURIComponent(connectionId)}`,
|
|
37
|
+
{ token: session.token, query: { q: question, topK } },
|
|
38
|
+
);
|
|
39
|
+
} else {
|
|
40
|
+
response = await request(
|
|
41
|
+
session.baseUrl,
|
|
42
|
+
`/training/context/${encodeURIComponent(connectionId)}`,
|
|
43
|
+
{ method: "POST", token: session.token, json: { question } },
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (opts.json) {
|
|
48
|
+
stdout.write(`${JSON.stringify(response, null, 2)}\n`);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Default: print the most useful field directly so it can be piped into
|
|
53
|
+
// a coding agent. /context returns trainingContext (text); /retrieve
|
|
54
|
+
// returns ranked results — fall back to JSON for the latter.
|
|
55
|
+
if (response && typeof response === "object" && response.trainingContext) {
|
|
56
|
+
if (response.skipped) {
|
|
57
|
+
stdout.write(`# (skipped: ${response.skipReason || "n/a"})\n`);
|
|
58
|
+
}
|
|
59
|
+
stdout.write(`${response.trainingContext}\n`);
|
|
60
|
+
if (response.companyKnowledgeContext) {
|
|
61
|
+
stdout.write(`\n# Company knowledge\n${response.companyKnowledgeContext}\n`);
|
|
62
|
+
}
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
stdout.write(`${JSON.stringify(response, null, 2)}\n`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
module.exports = { run };
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `deepsql business-rules --connection <name> [--question "..."] [--json]`
|
|
5
|
+
*
|
|
6
|
+
* Lists active business rules and SQL guardrails for a connection. Wraps
|
|
7
|
+
* GET /business-rules/connection/{connectionId}?question=...
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const { request } = require("../api/client");
|
|
11
|
+
const { resolveSession } = require("./_session");
|
|
12
|
+
const { resolveConnectionId } = require("./_connections");
|
|
13
|
+
|
|
14
|
+
async function run(opts, { stdout = process.stdout } = {}) {
|
|
15
|
+
if (!opts.connection) throw new Error("--connection <name> is required.");
|
|
16
|
+
|
|
17
|
+
const session = resolveSession(opts);
|
|
18
|
+
const connectionId = await resolveConnectionId(session, opts.connection);
|
|
19
|
+
|
|
20
|
+
const query = {};
|
|
21
|
+
if (opts.question) query.question = opts.question;
|
|
22
|
+
const response = await request(
|
|
23
|
+
session.baseUrl,
|
|
24
|
+
`/business-rules/connection/${encodeURIComponent(connectionId)}`,
|
|
25
|
+
{ token: session.token, query },
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
if (opts.json) {
|
|
29
|
+
stdout.write(`${JSON.stringify(response, null, 2)}\n`);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const active = response?.activeRules || [];
|
|
34
|
+
const guards = response?.applicableGuardrails || [];
|
|
35
|
+
|
|
36
|
+
if (active.length === 0 && guards.length === 0) {
|
|
37
|
+
stdout.write("No business rules or guardrails configured for this connection.\n");
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
stdout.write(
|
|
42
|
+
`${plural(active.length, "active business rule", "active business rules")}, ` +
|
|
43
|
+
`${plural(guards.length, "applicable guardrail", "applicable guardrails")}.\n`,
|
|
44
|
+
);
|
|
45
|
+
for (const r of active) {
|
|
46
|
+
const name = r.name || r.ruleName || `rule#${r.id}`;
|
|
47
|
+
const desc = r.description || r.ruleText || "";
|
|
48
|
+
stdout.write(` • ${name}${desc ? `: ${desc}` : ""}\n`);
|
|
49
|
+
}
|
|
50
|
+
if (guards.length) {
|
|
51
|
+
stdout.write(`\nGuardrail context:\n${response?.guardrailContext || "(none)"}\n`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function plural(n, singular, many) {
|
|
56
|
+
return `${n} ${n === 1 ? singular : many}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
module.exports = { run };
|