@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,60 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Thin wrapper over @inquirer/prompts so commands import from one place.
|
|
5
|
+
*
|
|
6
|
+
* Why a wrapper?
|
|
7
|
+
* - Keeps the import surface stable if we ever swap libs.
|
|
8
|
+
* - Lets us short-circuit to the existing `prompt`/`promptPassword` helpers
|
|
9
|
+
* when stdin isn't a TTY (CI / piped input) — @inquirer requires a TTY
|
|
10
|
+
* and aborts otherwise, which is the wrong UX for scripted use.
|
|
11
|
+
* - Lazy-loads inquirer so plain CLI commands don't pay the load cost.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const { prompt: tinyPrompt, promptPassword: tinyPromptPassword } = require("../auth/prompt");
|
|
15
|
+
|
|
16
|
+
let inquirer;
|
|
17
|
+
function loadInquirer() {
|
|
18
|
+
if (!inquirer) inquirer = require("@inquirer/prompts");
|
|
19
|
+
return inquirer;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function input({ message, default: dflt, required = true, validate } = {}) {
|
|
23
|
+
if (!process.stdin.isTTY) {
|
|
24
|
+
const value = await tinyPrompt(`${message} `);
|
|
25
|
+
if (required && !value) throw new Error(`${message} is required.`);
|
|
26
|
+
return value;
|
|
27
|
+
}
|
|
28
|
+
return loadInquirer().input({ message, default: dflt, required, validate });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function password({ message, mask = "*", validate } = {}) {
|
|
32
|
+
if (!process.stdin.isTTY) {
|
|
33
|
+
return tinyPromptPassword(`${message} `);
|
|
34
|
+
}
|
|
35
|
+
return loadInquirer().password({ message, mask, validate });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function select({ message, choices, default: dflt } = {}) {
|
|
39
|
+
if (!process.stdin.isTTY) {
|
|
40
|
+
// Non-interactive: print the available choices and read one as a line.
|
|
41
|
+
const labels = choices.map((c) => c.value).join(", ");
|
|
42
|
+
const value = (await tinyPrompt(`${message} (${labels}): `)).trim();
|
|
43
|
+
if (!choices.some((c) => c.value === value)) {
|
|
44
|
+
throw new Error(`Invalid choice: ${value}. Pick one of: ${labels}`);
|
|
45
|
+
}
|
|
46
|
+
return value;
|
|
47
|
+
}
|
|
48
|
+
return loadInquirer().select({ message, choices, default: dflt });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function confirm({ message, default: dflt = false } = {}) {
|
|
52
|
+
if (!process.stdin.isTTY) {
|
|
53
|
+
const raw = (await tinyPrompt(`${message} (y/N): `)).trim().toLowerCase();
|
|
54
|
+
if (!raw) return dflt;
|
|
55
|
+
return raw === "y" || raw === "yes";
|
|
56
|
+
}
|
|
57
|
+
return loadInquirer().confirm({ message, default: dflt });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
module.exports = { input, password, select, confirm };
|
package/src/ui/sse.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Minimal Server-Sent Events consumer using node:fetch's ReadableStream.
|
|
5
|
+
*
|
|
6
|
+
* Yields { event, data } objects. `data` is the raw string — caller decides
|
|
7
|
+
* whether to JSON.parse(). Honours SIGINT by aborting the underlying request
|
|
8
|
+
* and resolving the iterator cleanly.
|
|
9
|
+
*
|
|
10
|
+
* SSE wire format we parse (RFC):
|
|
11
|
+
* event: <name>\n
|
|
12
|
+
* data: <line1>\n
|
|
13
|
+
* data: <line2>\n
|
|
14
|
+
* \n ← message boundary
|
|
15
|
+
*
|
|
16
|
+
* We don't implement reconnection, last-event-id, or comment lines (`:` prefix)
|
|
17
|
+
* because the optimize stream is short-lived and stateless.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const { resolveUrl, normalizeBaseUrl } = require("../api/client");
|
|
21
|
+
|
|
22
|
+
async function* streamSse(baseUrl, path, { token, query, signal } = {}) {
|
|
23
|
+
let url;
|
|
24
|
+
if (typeof path === "string" && /^https?:\/\//i.test(path)) {
|
|
25
|
+
url = path;
|
|
26
|
+
} else {
|
|
27
|
+
url = resolveUrl(baseUrl, path);
|
|
28
|
+
}
|
|
29
|
+
if (query && typeof query === "object") {
|
|
30
|
+
const u = new URL(url);
|
|
31
|
+
for (const [k, v] of Object.entries(query)) {
|
|
32
|
+
if (v == null) continue;
|
|
33
|
+
u.searchParams.set(k, String(v));
|
|
34
|
+
}
|
|
35
|
+
url = u.toString();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const headers = { Accept: "text/event-stream" };
|
|
39
|
+
if (token) headers.Authorization = `Bearer ${token}`;
|
|
40
|
+
|
|
41
|
+
const response = await fetch(url, { headers, signal });
|
|
42
|
+
if (!response.ok) {
|
|
43
|
+
const text = await response.text().catch(() => "");
|
|
44
|
+
const err = new Error(`SSE ${response.status}: ${text || response.statusText}`);
|
|
45
|
+
err.status = response.status;
|
|
46
|
+
throw err;
|
|
47
|
+
}
|
|
48
|
+
if (!response.body) {
|
|
49
|
+
throw new Error("SSE response has no body");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const decoder = new TextDecoder();
|
|
53
|
+
let buffer = "";
|
|
54
|
+
|
|
55
|
+
for await (const chunk of response.body) {
|
|
56
|
+
buffer += decoder.decode(chunk, { stream: true });
|
|
57
|
+
let idx;
|
|
58
|
+
// Each SSE message ends with a blank line (\n\n or \r\n\r\n).
|
|
59
|
+
while ((idx = nextMessageEnd(buffer)) !== -1) {
|
|
60
|
+
const raw = buffer.slice(0, idx);
|
|
61
|
+
buffer = buffer.slice(idx).replace(/^(\r?\n){2}/, "");
|
|
62
|
+
const message = parseMessage(raw);
|
|
63
|
+
if (message) yield message;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Drain any remaining message that didn't end with a blank line (server
|
|
68
|
+
// close after final event).
|
|
69
|
+
const final = parseMessage(buffer);
|
|
70
|
+
if (final) yield final;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function nextMessageEnd(buffer) {
|
|
74
|
+
const lf = buffer.indexOf("\n\n");
|
|
75
|
+
const crlf = buffer.indexOf("\r\n\r\n");
|
|
76
|
+
if (lf === -1) return crlf;
|
|
77
|
+
if (crlf === -1) return lf;
|
|
78
|
+
return Math.min(lf, crlf);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function parseMessage(raw) {
|
|
82
|
+
if (!raw || !raw.trim()) return null;
|
|
83
|
+
let event = "message";
|
|
84
|
+
const dataLines = [];
|
|
85
|
+
for (const rawLine of raw.split(/\r?\n/)) {
|
|
86
|
+
if (!rawLine || rawLine.startsWith(":")) continue;
|
|
87
|
+
const colon = rawLine.indexOf(":");
|
|
88
|
+
const field = colon === -1 ? rawLine : rawLine.slice(0, colon);
|
|
89
|
+
const value = colon === -1 ? "" : rawLine.slice(colon + 1).replace(/^\s/, "");
|
|
90
|
+
if (field === "event") event = value;
|
|
91
|
+
else if (field === "data") dataLines.push(value);
|
|
92
|
+
}
|
|
93
|
+
if (dataLines.length === 0) return null;
|
|
94
|
+
return { event, data: dataLines.join("\n") };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
module.exports = { streamSse };
|
package/src/commands/ask.js
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
const os = require("node:os");
|
|
4
|
-
const { request } = require("../api/client");
|
|
5
|
-
const { resolveSession } = require("./_session");
|
|
6
|
-
const { resolveConnectionId } = require("./_connections");
|
|
7
|
-
|
|
8
|
-
async function run(opts, { stdout = process.stdout } = {}) {
|
|
9
|
-
const question = opts.positional.join(" ").trim();
|
|
10
|
-
if (!question) throw new Error("Pass a question: `deepsql ask \"why is this query slow?\" --connection <name>`.");
|
|
11
|
-
if (!opts.connection) throw new Error("--connection <name> is required.");
|
|
12
|
-
|
|
13
|
-
const session = resolveSession(opts);
|
|
14
|
-
const connectionId = await resolveConnectionId(session, opts.connection);
|
|
15
|
-
const userId = opts.user || `cli-${os.userInfo().username}`;
|
|
16
|
-
const projectId = opts.project || "deepsql-cli";
|
|
17
|
-
|
|
18
|
-
const response = await request(session.baseUrl, "/chat", {
|
|
19
|
-
method: "POST",
|
|
20
|
-
token: session.token,
|
|
21
|
-
timeoutMs: 240000,
|
|
22
|
-
json: {
|
|
23
|
-
connectionId,
|
|
24
|
-
message: question,
|
|
25
|
-
chatId: opts.chat || null,
|
|
26
|
-
userId,
|
|
27
|
-
projectId,
|
|
28
|
-
},
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
if (opts.json) {
|
|
32
|
-
stdout.write(`${JSON.stringify(response, null, 2)}\n`);
|
|
33
|
-
return;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const answer = response?.answer || response?.message || response?.content || null;
|
|
37
|
-
if (answer) {
|
|
38
|
-
stdout.write(`${answer}\n`);
|
|
39
|
-
} else {
|
|
40
|
-
stdout.write(`${JSON.stringify(response, null, 2)}\n`);
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
module.exports = { run };
|