@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.
@@ -0,0 +1,149 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * `deepsql permissions` — global role-based permission overrides.
5
+ *
6
+ * deepsql permissions list [--role <r>] [--json]
7
+ * deepsql permissions override --role <r> --permission <p> --grant|--revoke [--reason "..."]
8
+ * deepsql permissions reset --role <r> --permission <p>
9
+ *
10
+ * Backed by /permissions/** (overrides require ROLE_ADMIN).
11
+ */
12
+
13
+ const { ApiError, request } = require("../api/client");
14
+ const { resolveSession } = require("./_session");
15
+
16
+ const SUBCOMMANDS = {
17
+ list: cmdList,
18
+ override: cmdOverride,
19
+ reset: cmdReset,
20
+ };
21
+
22
+ async function run(opts, io = {}) {
23
+ const sub = opts.positional[0];
24
+ if (!sub) throw new Error("Usage: deepsql permissions <list|override|reset> ...");
25
+ const handler = SUBCOMMANDS[sub];
26
+ if (!handler) throw new Error(`Unknown permissions subcommand: ${sub}.`);
27
+ return wrap(handler)(
28
+ { ...opts, positional: opts.positional.slice(1) },
29
+ io,
30
+ );
31
+ }
32
+
33
+ function wrap(handler) {
34
+ return async (opts, io) => {
35
+ try {
36
+ return await handler(opts, io);
37
+ } catch (err) {
38
+ if (err instanceof ApiError && err.status === 403) {
39
+ throw new Error("Access denied — managing permissions requires ADMIN role.");
40
+ }
41
+ throw err;
42
+ }
43
+ };
44
+ }
45
+
46
+ // ─── list ──────────────────────────────────────────────────────────────────
47
+
48
+ async function cmdList(opts, { stdout = process.stdout } = {}) {
49
+ const session = resolveSession(opts);
50
+ const [registry, roles, overrides] = await Promise.all([
51
+ request(session.baseUrl, "/permissions/registry", { token: session.token }),
52
+ request(session.baseUrl, "/permissions/roles", { token: session.token }),
53
+ request(session.baseUrl, "/permissions/overrides", { token: session.token }),
54
+ ]);
55
+
56
+ if (opts.json) {
57
+ stdout.write(`${JSON.stringify({ registry, roles, overrides }, null, 2)}\n`);
58
+ return;
59
+ }
60
+
61
+ const roleList = Array.isArray(roles) ? roles : roles?.roles || [];
62
+ const overrideList = Array.isArray(overrides) ? overrides : overrides?.overrides || [];
63
+
64
+ if (opts.role) {
65
+ const wanted = String(opts.role).toUpperCase();
66
+ const role = roleList.find(
67
+ (r) => (r.code || r.role || r.name || "").toUpperCase() === wanted,
68
+ );
69
+ if (!role) {
70
+ const available = roleList.map((r) => r.code || r.role || r.name).filter(Boolean).join(", ");
71
+ throw new Error(`Role "${opts.role}" not found. Available: ${available}.`);
72
+ }
73
+ const code = role.code || role.role || role.name;
74
+ stdout.write(`${code} permissions (effective):\n`);
75
+ const perms = role.effectivePermissions || role.permissions || [];
76
+ for (const p of perms) {
77
+ stdout.write(` - ${typeof p === "string" ? p : p.code || p.name}\n`);
78
+ }
79
+ return;
80
+ }
81
+
82
+ if (overrideList.length === 0) {
83
+ stdout.write("No active overrides — every role has its default permissions.\n");
84
+ } else {
85
+ stdout.write("Active overrides:\n");
86
+ for (const o of overrideList) {
87
+ const role = o.role || "";
88
+ const perm = o.permissionCode || o.permission || "";
89
+ const granted = o.granted ? "GRANT" : "REVOKE";
90
+ const reason = o.reason ? ` — ${o.reason}` : "";
91
+ stdout.write(` ${role.padEnd(12)} ${granted.padEnd(7)} ${perm}${reason}\n`);
92
+ }
93
+ }
94
+
95
+ if (roleList.length > 0) {
96
+ stdout.write("\nRoles:\n");
97
+ for (const r of roleList) {
98
+ const name = r.code || r.role || r.name || "?";
99
+ const count = (r.effectivePermissions || r.permissions || []).length;
100
+ stdout.write(` ${name.padEnd(12)} ${count} permission(s)\n`);
101
+ }
102
+ stdout.write("\nUse `--role <NAME>` to see one role's full permission list.\n");
103
+ }
104
+ }
105
+
106
+ // ─── override ──────────────────────────────────────────────────────────────
107
+
108
+ async function cmdOverride(opts, { stdout = process.stdout } = {}) {
109
+ if (!opts.role) throw new Error("--role <ROLE> is required.");
110
+ if (!opts.permission) throw new Error("--permission <PERMISSION> is required.");
111
+ if (!opts.grant && !opts.revoke) {
112
+ throw new Error("Pass --grant or --revoke.");
113
+ }
114
+ if (opts.grant && opts.revoke) {
115
+ throw new Error("--grant and --revoke are mutually exclusive.");
116
+ }
117
+ const granted = !!opts.grant;
118
+ const session = resolveSession(opts);
119
+ const result = await request(session.baseUrl, "/permissions/overrides", {
120
+ method: "POST",
121
+ token: session.token,
122
+ json: {
123
+ role: String(opts.role).toUpperCase(),
124
+ permission: String(opts.permission).toUpperCase(),
125
+ granted,
126
+ reason: opts.reason || null,
127
+ },
128
+ });
129
+ stdout.write(`${result?.message || "Override applied."}\n`);
130
+ }
131
+
132
+ // ─── reset ─────────────────────────────────────────────────────────────────
133
+
134
+ async function cmdReset(opts, { stdout = process.stdout } = {}) {
135
+ if (!opts.role) throw new Error("--role <ROLE> is required.");
136
+ if (!opts.permission) throw new Error("--permission <PERMISSION> is required.");
137
+ const session = resolveSession(opts);
138
+ const result = await request(session.baseUrl, "/permissions/overrides", {
139
+ method: "DELETE",
140
+ token: session.token,
141
+ json: {
142
+ role: String(opts.role).toUpperCase(),
143
+ permission: String(opts.permission).toUpperCase(),
144
+ },
145
+ });
146
+ stdout.write(`${result?.message || "Override removed."}\n`);
147
+ }
148
+
149
+ module.exports = { run };
@@ -0,0 +1,56 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * `deepsql relationships --connection <name> [--json]`
5
+ *
6
+ * Returns inferred and validated foreign-key relationships for a connection,
7
+ * with confidence scores. Wraps GET /brain/inferred-relationships/{cid}.
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 response = await request(
21
+ session.baseUrl,
22
+ `/brain/inferred-relationships/${encodeURIComponent(connectionId)}`,
23
+ { token: session.token },
24
+ );
25
+
26
+ if (opts.json) {
27
+ stdout.write(`${JSON.stringify(response, null, 2)}\n`);
28
+ return;
29
+ }
30
+
31
+ const list = Array.isArray(response) ? response : [];
32
+ if (list.length === 0) {
33
+ stdout.write("No relationships inferred for this connection yet.\n");
34
+ return;
35
+ }
36
+
37
+ const high = list.filter((r) => (r.confidence ?? 0) >= 0.8).length;
38
+ const noun = list.length === 1 ? "relationship" : "relationships";
39
+ const header = high > 0
40
+ ? `${list.length} ${noun} (${high} high-confidence):`
41
+ : `${list.length} ${noun}:`;
42
+ stdout.write(`${header}\n`);
43
+
44
+ for (const r of list) {
45
+ const parts = [];
46
+ if (r.confidence != null) parts.push(`conf=${r.confidence.toFixed(2)}`);
47
+ if (r.inferenceMethod) parts.push(`via ${r.inferenceMethod}`);
48
+ const meta = parts.length ? ` (${parts.join(", ")})` : "";
49
+ const status = r.validationStatus ? ` [${r.validationStatus}]` : "";
50
+ stdout.write(
51
+ ` • ${r.sourceTable}.${r.sourceColumn} → ${r.targetTable}.${r.targetColumn}${meta}${status}\n`,
52
+ );
53
+ }
54
+ }
55
+
56
+ module.exports = { run };
@@ -0,0 +1,220 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * `deepsql setup` — post-install admin config wizard.
5
+ *
6
+ * Org name and LLM config are set at install time (env vars / install script /
7
+ * bootstrap link), so the CLI wizard only covers what's left after that:
8
+ *
9
+ * 1. Optionally configure SMTP / email (PUT /admin/settings/email +
10
+ * POST /admin/settings/email/test).
11
+ * 2. Optionally configure Slack — bot token, signing secret, optional app
12
+ * token for Socket Mode (PUT /admin/settings/slack). The backend masks
13
+ * existing tokens (GET only returns *Configured booleans), so leaving a
14
+ * token field blank means "keep current value."
15
+ * 3. Mark setup complete (POST /setup/complete) so the web-UI first-run
16
+ * banner clears. Skipped if already complete or with --skip-complete.
17
+ *
18
+ * The wizard is idempotent — re-running picks up current values where the
19
+ * backend exposes them and prefills the prompts.
20
+ */
21
+
22
+ const { ApiError, request } = require("../api/client");
23
+ const { resolveSession } = require("./_session");
24
+ const ui = require("../ui/prompts");
25
+
26
+ async function run(opts, { stdout = process.stdout, stderr = process.stderr } = {}) {
27
+ const session = resolveSession(opts);
28
+ const log = (msg) => stderr.write(`[deepsql] ${msg}\n`);
29
+
30
+ log("Checking setup status…");
31
+ const status = await request(session.baseUrl, "/setup/status", { token: session.token });
32
+ if (!status?.hasOrganizationInfo || !status?.hasLlmConfig) {
33
+ stderr.write(
34
+ "[deepsql] Note: organization or LLM config is not set. Those are configured at install time " +
35
+ "(env vars, install script, or bootstrap link), not by this wizard.\n",
36
+ );
37
+ }
38
+
39
+ await stepEmail(session, opts, log);
40
+ await stepSlack(session, opts, log);
41
+
42
+ if (!opts.skipComplete && !status?.setupComplete) {
43
+ await request(session.baseUrl, "/setup/complete", {
44
+ method: "POST",
45
+ token: session.token,
46
+ json: {},
47
+ });
48
+ log("Setup marked complete.");
49
+ } else if (status?.setupComplete) {
50
+ log("Setup was already complete.");
51
+ }
52
+ stdout.write("Done.\n");
53
+ }
54
+
55
+ // ─── email ─────────────────────────────────────────────────────────────────
56
+
57
+ async function stepEmail(session, opts, log) {
58
+ if (opts.skipEmail) {
59
+ log("Skipping SMTP setup (--skip-email).");
60
+ return;
61
+ }
62
+ const want = await ui.confirm({
63
+ message: "Configure SMTP / email now?",
64
+ default: true,
65
+ });
66
+ if (!want) {
67
+ log("Skipped SMTP.");
68
+ return;
69
+ }
70
+
71
+ let current = {};
72
+ try {
73
+ current = await request(session.baseUrl, "/admin/settings/email", { token: session.token });
74
+ } catch (err) {
75
+ if (err instanceof ApiError && err.status === 403) {
76
+ log("Cannot configure SMTP: requires ADMIN role on the calling token.");
77
+ return;
78
+ }
79
+ throw err;
80
+ }
81
+
82
+ const host = await ui.input({ message: "SMTP host:", default: current?.host || "smtp.gmail.com" });
83
+ const port = Number(
84
+ await ui.input({ message: "SMTP port:", default: String(current?.port || 587), required: true }),
85
+ );
86
+ const username = await ui.input({ message: "SMTP username:", default: current?.username || "" });
87
+ const password = await ui.password({ message: "SMTP password / app password:" });
88
+ const fromAddress = await ui.input({
89
+ message: "From address:",
90
+ default: current?.fromAddress || username,
91
+ });
92
+ const fromName = await ui.input({
93
+ message: "From name:",
94
+ default: current?.fromName || "DeepSQL",
95
+ required: false,
96
+ });
97
+ const startTls = await ui.confirm({ message: "Use STARTTLS?", default: current?.startTls ?? true });
98
+
99
+ const body = { host, port, username, password, fromAddress, fromName, startTls };
100
+ await request(session.baseUrl, "/admin/settings/email", {
101
+ method: "PUT",
102
+ token: session.token,
103
+ json: body,
104
+ });
105
+ log("SMTP saved.");
106
+
107
+ const sendTest = await ui.confirm({ message: "Send a test email now?", default: true });
108
+ if (sendTest) {
109
+ const recipient = await ui.input({
110
+ message: "Test recipient (your email):",
111
+ default: fromAddress,
112
+ required: true,
113
+ });
114
+ try {
115
+ await request(session.baseUrl, "/admin/settings/email/test", {
116
+ method: "POST",
117
+ token: session.token,
118
+ json: { recipient },
119
+ });
120
+ log(`Test email sent to ${recipient}.`);
121
+ } catch (err) {
122
+ log(`Test send failed: ${err.message}. Settings saved anyway.`);
123
+ }
124
+ }
125
+ }
126
+
127
+ // ─── slack ─────────────────────────────────────────────────────────────────
128
+
129
+ async function stepSlack(session, opts, log) {
130
+ if (opts.skipSlack) {
131
+ log("Skipping Slack setup (--skip-slack).");
132
+ return;
133
+ }
134
+ const want = await ui.confirm({
135
+ message: "Configure Slack (digests + bot replies) now?",
136
+ default: false,
137
+ });
138
+ if (!want) {
139
+ log("Skipped Slack.");
140
+ return;
141
+ }
142
+
143
+ let current = {};
144
+ try {
145
+ current = await request(session.baseUrl, "/admin/settings/slack", { token: session.token });
146
+ } catch (err) {
147
+ if (err instanceof ApiError && err.status === 403) {
148
+ log("Cannot configure Slack: requires ADMIN role on the calling token.");
149
+ return;
150
+ }
151
+ throw err;
152
+ }
153
+
154
+ const enabled = await ui.confirm({
155
+ message: "Enable Slack integration?",
156
+ default: current?.enabled ?? true,
157
+ });
158
+ const socketModeEnabled = await ui.confirm({
159
+ message:
160
+ "Use Socket Mode? (Easier for self-hosted — no public webhook needed; requires an App-level token.)",
161
+ default: current?.socketModeEnabled ?? true,
162
+ });
163
+ const deepsqlBotUsername = await ui.input({
164
+ message: "Bot display name in Slack:",
165
+ default: current?.deepsqlBotUsername || "DeepSQL",
166
+ });
167
+
168
+ // The backend only echoes *Configured booleans for tokens, so we never have
169
+ // the existing values to pre-fill. A blank entry means "keep current."
170
+ const tokenHint = (label, configuredFlag) =>
171
+ configuredFlag ? `${label} (leave blank to keep current):` : `${label}:`;
172
+
173
+ const botToken = await ui.password({
174
+ message: tokenHint("Bot token (xoxb-…)", current?.botTokenConfigured),
175
+ });
176
+ const signingSecret = await ui.password({
177
+ message: tokenHint("Signing secret", current?.signingSecretConfigured),
178
+ });
179
+ const appToken = socketModeEnabled
180
+ ? await ui.password({
181
+ message: tokenHint("App-level token (xapp-…)", current?.appTokenConfigured),
182
+ })
183
+ : "";
184
+
185
+ // Build the PUT body. Empty token strings are intentionally omitted so the
186
+ // service treats them as "keep current"; non-blank values overwrite.
187
+ const body = {
188
+ enabled,
189
+ socketModeEnabled,
190
+ deepsqlBotUsername,
191
+ };
192
+ if (botToken && botToken.trim()) body.botToken = botToken.trim();
193
+ if (signingSecret && signingSecret.trim()) body.signingSecret = signingSecret.trim();
194
+ if (appToken && appToken.trim()) body.appToken = appToken.trim();
195
+
196
+ // Reject the case where the user is enabling Slack for the first time but
197
+ // gave us no tokens — saving would leave the integration broken.
198
+ const noBot = !current?.botTokenConfigured && !body.botToken;
199
+ const noSecret = !current?.signingSecretConfigured && !body.signingSecret;
200
+ const noAppToken = socketModeEnabled && !current?.appTokenConfigured && !body.appToken;
201
+ if (enabled && (noBot || noSecret || noAppToken)) {
202
+ const missing = [
203
+ noBot && "bot token",
204
+ noSecret && "signing secret",
205
+ noAppToken && "app-level token",
206
+ ]
207
+ .filter(Boolean)
208
+ .join(", ");
209
+ throw new Error(`Slack is enabled but missing: ${missing}. Aborting before save.`);
210
+ }
211
+
212
+ await request(session.baseUrl, "/admin/settings/slack", {
213
+ method: "PUT",
214
+ token: session.token,
215
+ json: body,
216
+ });
217
+ log("Slack saved.");
218
+ }
219
+
220
+ module.exports = { run };