@deepsql/mcp 0.21.0 → 0.23.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/package.json +2 -3
- package/scripts/postinstall.js +16 -46
- package/src/cli.js +4 -4
- package/src/commands/_agent_intro.js +178 -0
- package/src/commands/agent.js +130 -22
- package/agent-profile/SOUL.md +0 -28
- package/agent-profile/distribution.yaml +0 -41
- package/agent-profile/skills/bi-query/SKILL.md +0 -40
- package/agent-profile/skills/index-advisor/SKILL.md +0 -34
- package/agent-profile/skills/schema-exploration/SKILL.md +0 -32
- package/agent-profile/skills/slow-query-optimize/SKILL.md +0 -31
- package/agent-profile/skills/workload-analysis/SKILL.md +0 -36
- package/agent-profile/skins/deepsql.yaml +0 -46
- package/agent-profile/tui/HERMES_VERSION +0 -1
- package/agent-profile/tui/dist/entry.js +0 -92903
- package/src/agent/bootstrap.js +0 -293
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deepsql/mcp",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "DeepSQL CLI, DBA Agent
|
|
3
|
+
"version": "0.23.0",
|
|
4
|
+
"description": "DeepSQL CLI, DBA Agent (thin client), and stdio MCP server for self-hosted deployments",
|
|
5
5
|
"bin": {
|
|
6
6
|
"deepsql": "bin/deepsql.js",
|
|
7
7
|
"deepsql-mcp": "deepsql-phase1-server.js"
|
|
@@ -15,7 +15,6 @@
|
|
|
15
15
|
"skills",
|
|
16
16
|
"src",
|
|
17
17
|
"scripts",
|
|
18
|
-
"agent-profile",
|
|
19
18
|
"deepsql-phase1-server.js",
|
|
20
19
|
"deepsql-phase1-lib.js",
|
|
21
20
|
"claude_desktop_config.customer.example.json",
|
package/scripts/postinstall.js
CHANGED
|
@@ -1,57 +1,27 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
"use strict";
|
|
3
3
|
|
|
4
|
-
// Postinstall:
|
|
5
|
-
// time so the first `deepsql` launch is instant instead of triggering a heavy
|
|
6
|
-
// one-time download. Best-effort and NON-FATAL — it never fails the npm install.
|
|
4
|
+
// Postinstall: intentionally a no-op heavy-wise.
|
|
7
5
|
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
6
|
+
// The DeepSQL Agent is now a THIN client — `deepsql` / `deepsql agent` talk to
|
|
7
|
+
// the server-side agent over the backend's brokered /agent/chat endpoint, so
|
|
8
|
+
// there is NO local agent runtime to download. `npm install -g @deepsql/mcp` is
|
|
9
|
+
// fast and quiet; nothing to pre-build.
|
|
10
10
|
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
// - non-global installs (local dev `npm install`, dependency installs)
|
|
14
|
-
// The profile itself is provisioned later, on first `deepsql` after login,
|
|
15
|
-
// because it needs the user's token.
|
|
16
|
-
|
|
17
|
-
function log(msg) {
|
|
18
|
-
process.stdout.write(`${msg}\n`);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function skip(reason) {
|
|
22
|
-
// Silent for the common "not global / CI" cases; only note an explicit opt-out.
|
|
23
|
-
if (reason) log(`DeepSQL Agent: ${reason}`);
|
|
24
|
-
process.exit(0);
|
|
25
|
-
}
|
|
11
|
+
// We only print a short next-steps hint on a global install (and stay silent for
|
|
12
|
+
// CI / local dependency installs so we never clutter other projects' logs).
|
|
26
13
|
|
|
27
14
|
try {
|
|
28
|
-
if (process.env.
|
|
29
|
-
skip("agent runtime setup skipped (DEEPSQL_SKIP_AGENT_SETUP set).");
|
|
30
|
-
}
|
|
31
|
-
if (process.env.CI) skip();
|
|
32
|
-
// Only pre-install on an explicit global install (or a forced run). Local /
|
|
33
|
-
// dependency installs shouldn't pull a Python+Node runtime.
|
|
15
|
+
if (process.env.CI) process.exit(0);
|
|
34
16
|
const isGlobal = String(process.env.npm_config_global || "") === "true";
|
|
35
|
-
if (!isGlobal
|
|
36
|
-
|
|
37
|
-
const { ensureHermes, hermesInstalled } = require("../src/agent/bootstrap");
|
|
38
|
-
|
|
39
|
-
if (hermesInstalled()) {
|
|
40
|
-
log("✓ DeepSQL Agent runtime already present.");
|
|
41
|
-
process.exit(0);
|
|
42
|
-
}
|
|
17
|
+
if (!isGlobal) process.exit(0);
|
|
43
18
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
log("DeepSQL Agent: runtime not pre-installed; it'll be set up on first `deepsql` run.");
|
|
52
|
-
}
|
|
53
|
-
} catch (e) {
|
|
54
|
-
// Never break the npm install over agent pre-setup.
|
|
55
|
-
log(`DeepSQL Agent: skipped runtime pre-install (${e && e.message ? e.message : e}).`);
|
|
19
|
+
process.stdout.write(
|
|
20
|
+
"\nDeepSQL installed. Next:\n" +
|
|
21
|
+
" deepsql login --url <your-deepsql-url>\n" +
|
|
22
|
+
" deepsql # chat with the DeepSQL Agent (connects to your server)\n\n"
|
|
23
|
+
);
|
|
24
|
+
} catch {
|
|
25
|
+
// Never let a cosmetic hint fail the install.
|
|
56
26
|
}
|
|
57
27
|
process.exit(0);
|
package/src/cli.js
CHANGED
|
@@ -90,13 +90,13 @@ const GLOBAL_OPTIONS = [
|
|
|
90
90
|
|
|
91
91
|
const COMMAND_HELP = {
|
|
92
92
|
agent: {
|
|
93
|
-
description: "
|
|
94
|
-
usage:
|
|
93
|
+
description: "Chat with the DeepSQL Agent — DBA work, BI questions, and database guardrails. A thin client: it connects to your server's agent (the same one behind the web Agent tab and Slack), nothing installs locally. Bare `deepsql` in a terminal opens interactive chat; pass a question for a one-shot.",
|
|
94
|
+
usage: 'deepsql agent ["<question>"] [options] (or just: deepsql)',
|
|
95
95
|
options: [
|
|
96
96
|
["--url <url>", "DeepSQL instance to use (default: saved login)"],
|
|
97
|
-
["--connection <name>", "
|
|
97
|
+
["--connection <name>", "Connection to ground answers on (default: saved active connection)"],
|
|
98
98
|
],
|
|
99
|
-
notes: "
|
|
99
|
+
notes: "No local runtime or LLM key needed — the server runs the agent and proxies the model. Interactive chat resumes your most recent conversation (shared with the web Agent tab).",
|
|
100
100
|
},
|
|
101
101
|
|
|
102
102
|
login: {
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// Branded one-time intro for the interactive DeepSQL Agent REPL.
|
|
4
|
+
//
|
|
5
|
+
// Compact by design (the REPL is a scrolling buffer, not a full-screen TUI):
|
|
6
|
+
// wordmark + tagline + a rotating sample prompt per category + how to drive it.
|
|
7
|
+
//
|
|
8
|
+
// Colors use 256-color codes (\x1b[38;5;Nm) — macOS Terminal.app does NOT
|
|
9
|
+
// support 24-bit truecolor and garbles \x1b[38;2;r;g;bm into wrong colors, so
|
|
10
|
+
// we stick to the 256 palette which renders correctly everywhere. Body text is
|
|
11
|
+
// left at the terminal's default foreground so it's readable on light AND dark
|
|
12
|
+
// backgrounds. Suppress the whole banner with DEEPSQL_NO_BANNER=1.
|
|
13
|
+
|
|
14
|
+
const https = require("node:https");
|
|
15
|
+
|
|
16
|
+
// Installed package version (this file ships inside the package).
|
|
17
|
+
let PKG_VERSION = "";
|
|
18
|
+
try {
|
|
19
|
+
PKG_VERSION = require("../../package.json").version || "";
|
|
20
|
+
} catch {
|
|
21
|
+
/* ignore */
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Solid-block "DEEPSQL" wordmark.
|
|
25
|
+
const LOGO = [
|
|
26
|
+
"████ ████ ████ ████ ████ ███ █ ",
|
|
27
|
+
"█ █ █ █ █ █ █ █ █ █ ",
|
|
28
|
+
"█ █ ███ ███ ████ ███ █ █ █ ",
|
|
29
|
+
"█ █ █ █ █ █ █ ██ █ ",
|
|
30
|
+
"████ ████ ████ █ ████ ███ █████ ",
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
// Welcoming purple palette (256-color cube). Light→deep down the wordmark; one
|
|
34
|
+
// clean hue, no grey, so it reads as a single wordmark.
|
|
35
|
+
const PURPLE_LIGHT = 141; // #af87ff
|
|
36
|
+
const PURPLE = 99; // #875fff
|
|
37
|
+
const PURPLE_DEEP = 98; // #875fd7
|
|
38
|
+
const LOGO_ROWS = [PURPLE_LIGHT, PURPLE_LIGHT, PURPLE, PURPLE, PURPLE_DEEP];
|
|
39
|
+
|
|
40
|
+
function c(n, s, on) {
|
|
41
|
+
return on ? `\x1b[38;5;${n}m${s}\x1b[0m` : s;
|
|
42
|
+
}
|
|
43
|
+
function dim(s, on) {
|
|
44
|
+
return on ? `\x1b[2m${s}\x1b[0m` : s;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Several sample prompts per category; one is picked at random each launch.
|
|
48
|
+
const PROMPTS = {
|
|
49
|
+
DBA: [
|
|
50
|
+
"why is the orders query slow?",
|
|
51
|
+
"what indexes should I add?",
|
|
52
|
+
"is this query using the right index?",
|
|
53
|
+
"what's my slowest query today?",
|
|
54
|
+
],
|
|
55
|
+
BI: [
|
|
56
|
+
"revenue by city this month",
|
|
57
|
+
"how many bookings last week?",
|
|
58
|
+
"top 10 customers by spend",
|
|
59
|
+
"signups per day this month",
|
|
60
|
+
],
|
|
61
|
+
Guardian: [
|
|
62
|
+
"is this migration safe?",
|
|
63
|
+
"who’s driving DB load?",
|
|
64
|
+
"any tables bloating?",
|
|
65
|
+
"what schema changed recently?",
|
|
66
|
+
],
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
function pick(arr) {
|
|
70
|
+
return arr[Math.floor(Math.random() * arr.length)];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Prompt label for the readline input line (purple, brand-forward).
|
|
74
|
+
function promptLabel(useColor) {
|
|
75
|
+
return "\n" + c(PURPLE, "deepsql ›", useColor) + " ";
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function renderIntro(stdout, { useColor = true, connectionLabel = null, versionLine = null } = {}) {
|
|
79
|
+
const out = [""];
|
|
80
|
+
LOGO.forEach((row, i) => out.push(" " + c(LOGO_ROWS[i], row, useColor)));
|
|
81
|
+
out.push("");
|
|
82
|
+
out.push(" " + dim("I am your DBA and Data agent. DeepSQL is the brain for your database", useColor));
|
|
83
|
+
out.push("");
|
|
84
|
+
out.push(" " + c(PURPLE, "Try these prompts", useColor));
|
|
85
|
+
for (const cat of ["DBA", "BI", "Guardian"]) {
|
|
86
|
+
// Label reads "<cat> prompt" (not a bare "DBA" that looks like a command);
|
|
87
|
+
// purple label, prompt text stays default-fg so it's always readable.
|
|
88
|
+
const label = `${cat} prompt`;
|
|
89
|
+
out.push(" " + c(PURPLE_LIGHT, label.padEnd(16), useColor) + `"${pick(PROMPTS[cat])}"`);
|
|
90
|
+
}
|
|
91
|
+
out.push("");
|
|
92
|
+
if (connectionLabel) {
|
|
93
|
+
out.push(" " + dim("Grounded on ", useColor) + connectionLabel);
|
|
94
|
+
}
|
|
95
|
+
out.push(
|
|
96
|
+
" " +
|
|
97
|
+
dim("Ask a question · ", useColor) +
|
|
98
|
+
"exit" +
|
|
99
|
+
dim(" to quit · ", useColor) +
|
|
100
|
+
"deepsql --help" +
|
|
101
|
+
dim(" for CLI commands", useColor)
|
|
102
|
+
);
|
|
103
|
+
if (versionLine) out.push(" " + versionLine);
|
|
104
|
+
out.push("");
|
|
105
|
+
stdout.write(out.join("\n") + "\n");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function compareSemver(a, b) {
|
|
109
|
+
const pa = String(a).split(".").map((n) => parseInt(n, 10) || 0);
|
|
110
|
+
const pb = String(b).split(".").map((n) => parseInt(n, 10) || 0);
|
|
111
|
+
for (let i = 0; i < 3; i++) {
|
|
112
|
+
if ((pa[i] || 0) > (pb[i] || 0)) return 1;
|
|
113
|
+
if ((pa[i] || 0) < (pb[i] || 0)) return -1;
|
|
114
|
+
}
|
|
115
|
+
return 0;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Best-effort latest-version lookup from the npm registry, with a tight timeout
|
|
119
|
+
// so it never noticeably delays startup. Resolves null on any failure.
|
|
120
|
+
function fetchLatest(timeoutMs = 1200) {
|
|
121
|
+
return new Promise((resolve) => {
|
|
122
|
+
let done = false;
|
|
123
|
+
const finish = (v) => {
|
|
124
|
+
if (!done) {
|
|
125
|
+
done = true;
|
|
126
|
+
resolve(v);
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
try {
|
|
130
|
+
const req = https.get(
|
|
131
|
+
"https://registry.npmjs.org/@deepsql/mcp/latest",
|
|
132
|
+
{ timeout: timeoutMs, headers: { accept: "application/json" } },
|
|
133
|
+
(res) => {
|
|
134
|
+
if (res.statusCode !== 200) {
|
|
135
|
+
res.resume();
|
|
136
|
+
return finish(null);
|
|
137
|
+
}
|
|
138
|
+
let d = "";
|
|
139
|
+
res.on("data", (chunk) => (d += chunk));
|
|
140
|
+
res.on("end", () => {
|
|
141
|
+
try {
|
|
142
|
+
finish(JSON.parse(d).version || null);
|
|
143
|
+
} catch {
|
|
144
|
+
finish(null);
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
);
|
|
149
|
+
req.on("timeout", () => {
|
|
150
|
+
req.destroy();
|
|
151
|
+
finish(null);
|
|
152
|
+
});
|
|
153
|
+
req.on("error", () => finish(null));
|
|
154
|
+
} catch {
|
|
155
|
+
finish(null);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// "v0.22.0 · latest" / "v0.22.0 · update available → v0.23.0 npm i -g …".
|
|
161
|
+
// Falls back to a plain dim version when the registry can't be reached.
|
|
162
|
+
async function getVersionLine(useColor) {
|
|
163
|
+
const cur = PKG_VERSION;
|
|
164
|
+
if (!cur) return null;
|
|
165
|
+
const latest = await fetchLatest();
|
|
166
|
+
if (!latest) return dim(`v${cur}`, useColor);
|
|
167
|
+
if (compareSemver(latest, cur) > 0) {
|
|
168
|
+
return (
|
|
169
|
+
c(PURPLE_LIGHT, `v${cur}`, useColor) +
|
|
170
|
+
dim(" · update available → ", useColor) +
|
|
171
|
+
c(PURPLE, `v${latest}`, useColor) +
|
|
172
|
+
dim(" npm i -g @deepsql/mcp@latest", useColor)
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
return dim(`v${cur} · latest`, useColor);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
module.exports = { renderIntro, getVersionLine, promptLabel };
|
package/src/commands/agent.js
CHANGED
|
@@ -1,15 +1,70 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
// `deepsql agent` (and bare `deepsql` in a terminal):
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
3
|
+
// `deepsql agent` (and bare `deepsql` in a terminal): the DeepSQL Agent, as a
|
|
4
|
+
// THIN client. It does not run an agent runtime locally — it talks to the
|
|
5
|
+
// server-side agent through the backend's brokered, authenticated
|
|
6
|
+
// `/agent/chat` endpoint (the same agent that powers the web Agent tab and
|
|
7
|
+
// Slack). Identity, profile, connection scope, and conversation history are all
|
|
8
|
+
// resolved server-side from your saved `deepsql login` — nothing heavy installs,
|
|
9
|
+
// and your terminal shares the same conversations as the web app.
|
|
10
|
+
//
|
|
11
|
+
// deepsql agent "how many active hotels?" one-shot
|
|
12
|
+
// deepsql interactive chat (resumes latest)
|
|
7
13
|
|
|
8
|
-
const
|
|
14
|
+
const readline = require("node:readline");
|
|
15
|
+
const { request } = require("../api/client");
|
|
9
16
|
const { resolveSession } = require("./_session");
|
|
10
|
-
const {
|
|
17
|
+
const { resolveConnectionId } = require("./_connections");
|
|
18
|
+
const { renderIntro, getVersionLine, promptLabel } = require("./_agent_intro");
|
|
19
|
+
|
|
20
|
+
// The brokered turn can run a full multi-tool agent loop server-side; allow well
|
|
21
|
+
// past the backend's own per-turn ceiling so we don't abandon a valid answer.
|
|
22
|
+
const TURN_TIMEOUT_MS = 320000;
|
|
23
|
+
|
|
24
|
+
async function runTurn(session, connectionId, message, conversationId) {
|
|
25
|
+
return request(session.baseUrl, "/agent/chat", {
|
|
26
|
+
method: "POST",
|
|
27
|
+
token: session.token,
|
|
28
|
+
json: { message, connectionId, conversationId },
|
|
29
|
+
timeoutMs: TURN_TIMEOUT_MS,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// A server-side turn can run a multi-tool agent loop for a minute or more with
|
|
34
|
+
// no streamed output, so show a live spinner + elapsed timer instead of a
|
|
35
|
+
// static "…thinking" that reads as frozen. No-op (returns the promise) when not
|
|
36
|
+
// a TTY, so piped/one-shot output stays clean.
|
|
37
|
+
function awaitWithSpinner(stdout, promise) {
|
|
38
|
+
if (!stdout.isTTY) return promise;
|
|
39
|
+
const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
40
|
+
const start = Date.now();
|
|
41
|
+
let i = 0;
|
|
42
|
+
const tick = () => {
|
|
43
|
+
const s = Math.floor((Date.now() - start) / 1000);
|
|
44
|
+
stdout.write(`\r\x1b[2m${frames[i++ % frames.length]} thinking… ${s}s\x1b[0m\x1b[K`);
|
|
45
|
+
};
|
|
46
|
+
tick();
|
|
47
|
+
const timer = setInterval(tick, 120);
|
|
48
|
+
const clear = () => {
|
|
49
|
+
clearInterval(timer);
|
|
50
|
+
stdout.write("\r\x1b[K");
|
|
51
|
+
};
|
|
52
|
+
return promise.then(
|
|
53
|
+
(v) => { clear(); return v; },
|
|
54
|
+
(e) => { clear(); throw e; }
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function renderReply(stdout, resp) {
|
|
59
|
+
if (resp && resp.ok && resp.answer && String(resp.answer).trim()) {
|
|
60
|
+
stdout.write(`\n${String(resp.answer).trim()}\n`);
|
|
61
|
+
} else {
|
|
62
|
+
stdout.write(`\n⚠ ${(resp && resp.error) || "the agent run ended early"}\n`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
11
65
|
|
|
12
66
|
async function run(opts, io = {}) {
|
|
67
|
+
const stdout = io.stdout || process.stdout;
|
|
13
68
|
const stderr = io.stderr || process.stderr;
|
|
14
69
|
|
|
15
70
|
let session;
|
|
@@ -20,26 +75,79 @@ async function run(opts, io = {}) {
|
|
|
20
75
|
return 1;
|
|
21
76
|
}
|
|
22
77
|
|
|
23
|
-
if
|
|
24
|
-
|
|
25
|
-
|
|
78
|
+
// Connection is optional — the agent can chat without one and will ask if a
|
|
79
|
+
// question needs a database. Resolve the active default when present.
|
|
80
|
+
let connectionId = null;
|
|
81
|
+
try {
|
|
82
|
+
connectionId = await resolveConnectionId(session, opts.connection);
|
|
83
|
+
} catch {
|
|
84
|
+
/* no default connection set — fine */
|
|
26
85
|
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
86
|
+
|
|
87
|
+
const message = (opts.positional || []).join(" ").trim();
|
|
88
|
+
|
|
89
|
+
// One-shot: a question on the command line.
|
|
90
|
+
if (message) {
|
|
91
|
+
try {
|
|
92
|
+
const resp = await awaitWithSpinner(stdout, runTurn(session, connectionId, message, null));
|
|
93
|
+
renderReply(stdout, resp);
|
|
94
|
+
return resp && resp.ok ? 0 : 1;
|
|
95
|
+
} catch (e) {
|
|
96
|
+
stderr.write(`Agent request failed: ${e.message}\n`);
|
|
97
|
+
return 1;
|
|
98
|
+
}
|
|
30
99
|
}
|
|
31
|
-
// Branding is delivered via HERMES_TUI_DIR in launchEnv (Hermes runs our
|
|
32
|
-
// prebuilt branded TUI directly — no esbuild), set just below.
|
|
33
100
|
|
|
34
|
-
//
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
101
|
+
// Interactive: a line REPL against the server agent. Requires a TTY.
|
|
102
|
+
if (!stdout.isTTY) {
|
|
103
|
+
stderr.write('Usage: deepsql agent "<question>" (or run in a terminal for interactive chat)\n');
|
|
104
|
+
return 2;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Branded one-time intro (compact). Suppress with DEEPSQL_NO_BANNER=1; colors
|
|
108
|
+
// honor --no-color / NO_COLOR (this branch is already TTY-only).
|
|
109
|
+
const useColor = !opts.noColor && !process.env.NO_COLOR;
|
|
110
|
+
if (process.env.DEEPSQL_NO_BANNER) {
|
|
111
|
+
stdout.write("DeepSQL Agent — connected to your server agent. Type a question; `exit` or Ctrl-C to quit.\n");
|
|
112
|
+
} else {
|
|
113
|
+
// Only surface a connection when one was explicitly named; a default/absent
|
|
114
|
+
// connection shows nothing (the agent asks if a question needs one).
|
|
115
|
+
const connectionLabel = opts.connection || null;
|
|
116
|
+
const versionLine = await getVersionLine(useColor); // best-effort, tight timeout
|
|
117
|
+
renderIntro(stdout, { useColor, connectionLabel, versionLine });
|
|
118
|
+
}
|
|
119
|
+
// conversationId stays null on the first turn so the server resumes your most
|
|
120
|
+
// recent conversation for this connection (shared with the web Agent tab);
|
|
121
|
+
// subsequent turns reuse the id it returns.
|
|
122
|
+
let conversationId = null;
|
|
123
|
+
const rl = readline.createInterface({ input: process.stdin, output: stdout, prompt: promptLabel(useColor) });
|
|
124
|
+
rl.prompt();
|
|
125
|
+
|
|
38
126
|
return await new Promise((resolve) => {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
127
|
+
rl.on("line", async (line) => {
|
|
128
|
+
const text = line.trim();
|
|
129
|
+
if (!text) {
|
|
130
|
+
rl.prompt();
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (text === "exit" || text === "quit") {
|
|
134
|
+
rl.close();
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
rl.pause();
|
|
138
|
+
try {
|
|
139
|
+
const resp = await awaitWithSpinner(stdout, runTurn(session, connectionId, text, conversationId));
|
|
140
|
+
if (resp && resp.conversationId) conversationId = resp.conversationId;
|
|
141
|
+
renderReply(stdout, resp);
|
|
142
|
+
} catch (e) {
|
|
143
|
+
stdout.write(`\n⚠ ${e.message}\n`);
|
|
144
|
+
}
|
|
145
|
+
rl.resume();
|
|
146
|
+
rl.prompt();
|
|
147
|
+
});
|
|
148
|
+
rl.on("close", () => {
|
|
149
|
+
stdout.write("\nBye.\n");
|
|
150
|
+
resolve(0);
|
|
43
151
|
});
|
|
44
152
|
});
|
|
45
153
|
}
|
package/agent-profile/SOUL.md
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
You are **DeepSQL DBA**, an AI database performance assistant. You answer questions about the user's databases and help them build features against an existing schema. You operate exclusively through the **DeepSQL MCP tools** (server `deepsql`) — they are your source of truth, not your training data and not the user's codebase.
|
|
2
|
-
|
|
3
|
-
Be direct, concrete, and grounded. Cite the tables and rules you used. Admit uncertainty instead of guessing. Prefer one correct, grounded answer over a verbose survey.
|
|
4
|
-
|
|
5
|
-
## Non-negotiable rules
|
|
6
|
-
|
|
7
|
-
1. **Connections are UUIDs.** Everything except `get_current_user` needs a `connectionId`. Get it from `list_connections` once and reuse it. Pass the UUID, never the human name.
|
|
8
|
-
|
|
9
|
-
2. **Ground before you generate.** ALWAYS call `get_brain_context(connectionId, question)` before writing any non-trivial SQL or proposing schema. The brain knows business rules, anti-patterns, and inferred foreign keys that the raw schema does not. Skipping it produces "technically valid, semantically wrong" SQL — the worst kind.
|
|
10
|
-
|
|
11
|
-
3. **Schema comes from `get_schema`, not `information_schema`.** It is cached, fast, and authoritative. Never trust column names inferred from the codebase — they drift.
|
|
12
|
-
|
|
13
|
-
4. **Table-qualify every column** in generated SQL (`table.column`). Honor business rules and anti-patterns silently — if a rule says `always_filter_cancelled`, your query includes the filter without asking permission to follow the user's own rule.
|
|
14
|
-
|
|
15
|
-
5. **Read-only by default.** Developers cannot mutate; admins can with a **two-step confirmation**. If `execute_sql` returns `requiresConfirmation: true`, surface the warnings verbatim, get explicit human approval, then re-call with `confirmMutation: true`. NEVER auto-confirm — that defeats the safety gate. Never try to work around a 403/`EDITOR_MUTATION_FORBIDDEN`; surface it.
|
|
16
|
-
|
|
17
|
-
6. **One execution tool, one analysis tool.** Use `execute_sql` to run SQL; use `analyze_query_plan` for plans. Don't hand-wrap `EXPLAIN` inside `execute_sql`, and don't run a query just to see its plan. `EXPLAIN`/`EXPLAIN ANALYZE` are read-only SQL when you do need them — but `analyze_query_plan` gives the AI-enriched summary.
|
|
18
|
-
|
|
19
|
-
7. **Row limits are real.** `execute_sql` defaults to 100 rows, max 1000. If you need a total, `SELECT COUNT(*)` — don't infer it from a truncated result.
|
|
20
|
-
|
|
21
|
-
8. **Consult before you commit schema.** When the user says "add a table / track X / write a migration," STOP and run the brain consult (`get_brain_context` → `get_schema` → `list_business_rules` → `get_relationships` → `get_anti_patterns`). There is almost always an existing table or column to extend instead of duplicate. Narrate what you found before proposing DDL.
|
|
22
|
-
|
|
23
|
-
## Skills
|
|
24
|
-
|
|
25
|
-
Detailed procedures live in your skills. Load the matching one before acting:
|
|
26
|
-
`bi-query` (answer a data question), `schema-exploration` (map/describe a database), `index-advisor` (what indexes to add/drop), `slow-query-optimize` (why is a query slow / rewrite it), `workload-analysis` (what's driving load, regressions, growth).
|
|
27
|
-
|
|
28
|
-
Every tool call is logged with your identity and the statement you ran. Don't do anything you wouldn't defend in an audit.
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
# Hermes profile distribution — DeepSQL DBA agent.
|
|
2
|
-
# Enterprises install this per user: hermes profile install <git-url|dir> --name <profile>
|
|
3
|
-
# `hermes profile update <profile>` refreshes SOUL.md + skills/ WITHOUT touching
|
|
4
|
-
# the user's memory, sessions, .env, or config.yaml (their identity + history).
|
|
5
|
-
name: deepsql-agent
|
|
6
|
-
version: 0.1.0
|
|
7
|
-
description: "DeepSQL DBA agent — grounded, read-only database assistant over the DeepSQL MCP tools (BI queries, schema exploration, index advice, slow-query optimization, workload analysis)."
|
|
8
|
-
hermes_requires: ">=0.12.0"
|
|
9
|
-
author: "DeepSQL"
|
|
10
|
-
license: "proprietary"
|
|
11
|
-
|
|
12
|
-
# Per-user runtime values. Secrets + identity live in each profile's .env
|
|
13
|
-
# (user-owned, preserved on update); provision-profile.sh writes them.
|
|
14
|
-
env_requires:
|
|
15
|
-
- name: AZURE_OPENAI_KEY
|
|
16
|
-
description: "Azure OpenAI API key for the chat model"
|
|
17
|
-
required: true
|
|
18
|
-
- name: AZURE_OPENAI_ENDPOINT
|
|
19
|
-
description: "Azure OpenAI resource endpoint (…cognitiveservices/…openai.azure.com)"
|
|
20
|
-
required: false
|
|
21
|
-
default: "https://dba-agent-3-resource.openai.azure.com/"
|
|
22
|
-
- name: DEEPSQL_API_BASE_URL
|
|
23
|
-
description: "DeepSQL backend API base URL"
|
|
24
|
-
required: false
|
|
25
|
-
default: "http://localhost:8080/api/"
|
|
26
|
-
- name: DEEPSQL_MCP_SERVER
|
|
27
|
-
description: "Absolute path to mcp/deepsql-phase1-server.js"
|
|
28
|
-
required: true
|
|
29
|
-
- name: DEEPSQL_MCP_USER_ID
|
|
30
|
-
description: "DeepSQL user id this profile acts as — backend audit + RBAC attribution"
|
|
31
|
-
required: true
|
|
32
|
-
- name: DEEPSQL_AUTH_TOKEN
|
|
33
|
-
description: "DeepSQL bearer token for this user — server-enforced RBAC/connection-scope"
|
|
34
|
-
required: false
|
|
35
|
-
|
|
36
|
-
# The agent persona and skills are the distribution's product surface.
|
|
37
|
-
# Runtime config (model, mcp_servers, approvals, toolset scoping) is applied
|
|
38
|
-
# per-user by provision-profile.sh into the profile's preserved config.yaml.
|
|
39
|
-
distribution_owned:
|
|
40
|
-
- SOUL.md
|
|
41
|
-
- skills/
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: bi-query
|
|
3
|
-
description: Answer a question about the data — write and run grounded, read-only SQL against a DeepSQL connection and report the result.
|
|
4
|
-
version: 1.0.0
|
|
5
|
-
platforms: [linux, macos, windows]
|
|
6
|
-
metadata:
|
|
7
|
-
hermes:
|
|
8
|
-
tags: [sql, bi, analytics, query, count, report, deepsql, database]
|
|
9
|
-
related_skills: [schema-exploration, slow-query-optimize]
|
|
10
|
-
---
|
|
11
|
-
|
|
12
|
-
# BI Query
|
|
13
|
-
|
|
14
|
-
Use when the user asks a question whose answer is **in the data** ("how many bookings last week?", "revenue by region", "top 10 customers"). The output is a number/table, not schema advice.
|
|
15
|
-
|
|
16
|
-
## Procedure
|
|
17
|
-
|
|
18
|
-
1. **Resolve the connection.** If you don't already have the UUID, call `list_connections` and match the user's named database. Pass the UUID to every later call.
|
|
19
|
-
|
|
20
|
-
2. **Ground.** Call `get_brain_context(connectionId, "<the user's question>")`. Read what it surfaces — relevant tables, columns, inferred FKs, business rules, anti-patterns. Do not skip this even if you think you know the table.
|
|
21
|
-
|
|
22
|
-
3. **Confirm exact columns** with `get_schema(connectionId)` only if the brain context didn't give you exact column names/types you need. Don't query `information_schema`.
|
|
23
|
-
|
|
24
|
-
4. **Write ONE statement.** Table-qualify every column. Apply every relevant business rule (e.g. cancelled/soft-delete filters) without being asked. Use a CTE (`WITH …`) instead of multiple statements — `execute_sql` rejects multi-statement input.
|
|
25
|
-
|
|
26
|
-
5. **Sanity-check the plan** with `analyze_query_plan(connectionId, sql)` (no `useAnalyze`) when the query has non-trivial joins or runs against an unfamiliar schema. It's cheap and catches bad joins before you show the user a wrong number.
|
|
27
|
-
|
|
28
|
-
6. **Run it** with `execute_sql(connectionId, sql, limit=…)`. Remember: default 100 rows, max 1000. For a total, `SELECT COUNT(*)` rather than counting a truncated result set.
|
|
29
|
-
|
|
30
|
-
7. **Report concisely:** the answer, the table(s) you used, and any business rule you applied ("excluded CANCELLED per `always_filter_cancelled`").
|
|
31
|
-
|
|
32
|
-
## Guardrails
|
|
33
|
-
|
|
34
|
-
- Read-only only. If the question implies a write, switch to the mutation flow (surface warnings, get a human OK, `confirmMutation: true`) and only if the user is an admin.
|
|
35
|
-
- If `get_brain_context` returns nothing useful, do **not** guess a table from memory. First try lightweight discovery against the live DB: `SHOW TABLES LIKE '%<keyword>%'`, then `DESCRIBE <candidate_table>`, then a narrowly-scoped verification query such as `SELECT COUNT(*) ...`. Ask the user only if multiple candidates remain plausible after those probes.
|
|
36
|
-
- If schema/object metadata is too large or `get_database_objects` times out, prefer targeted read-only probes over broad catalog dumps.
|
|
37
|
-
- When the user asks for a chart or ranking and brain context is thin, first identify the fact table and dimension join path with small probes, then run the final aggregation. Example pattern used successfully in `idb_database`: `USER_BOOKINGS.hotel_id -> HOTEL.id` for city-level booking rollups, and `PRICE_BREAKDOWN.booking_id -> USER_BOOKINGS.id -> HOTEL.id` for hotel-level room-price averages.
|
|
38
|
-
- If the user says "active hotels" or similar status language, check brain context for status-source rules before using a status column. In `idb_database`, `HOTEL_PRICING.property_status` is the correct source and `HOTEL.onboarding_status` is specifically the wrong one.
|
|
39
|
-
- For chart requests in chat-only environments, still run the real aggregation query first. If you cannot render a binary image with available tools, provide a clearly labeled text/Markdown chart from the real results rather than fabricating an image.
|
|
40
|
-
- Never present a truncated (limit-capped) result as a total.
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: index-advisor
|
|
3
|
-
description: Recommend which indexes to add or drop for a connection, using DeepSQL's workload-weighted advisor, and optionally dry-run/apply them.
|
|
4
|
-
version: 1.0.0
|
|
5
|
-
platforms: [linux, macos, windows]
|
|
6
|
-
metadata:
|
|
7
|
-
hermes:
|
|
8
|
-
tags: [index, indexes, advisor, performance, create-index, drop-index, unused, deepsql]
|
|
9
|
-
related_skills: [slow-query-optimize, workload-analysis]
|
|
10
|
-
---
|
|
11
|
-
|
|
12
|
-
# Index Advisor
|
|
13
|
-
|
|
14
|
-
Use when the user asks what indexes to add or drop, or "how do I speed up this workload with indexes?" Indexes are a **whole-workload** decision — never recommend one off a single query in isolation; that's what this advisor is for.
|
|
15
|
-
|
|
16
|
-
## Procedure
|
|
17
|
-
|
|
18
|
-
1. **Resolve the connection** (`list_connections` → UUID).
|
|
19
|
-
|
|
20
|
-
2. **Get the workload-weighted recommendations.** `get_index_recommendations(connectionId)` returns the pre-computed top-N (default 5) ranked by net benefit (`Σ calls × mean_exec_time` − write cost). Each carries the contributing query fingerprints, the role each column played, and (on Postgres) a HypoPG cost-delta. This covers both `CREATE_INDEX` and `DROP_INDEX` (unused / redundant-prefix) candidates.
|
|
21
|
-
|
|
22
|
-
3. **Add catalog context** when relevant: `get_index_health(connectionId)` for the overall picture, `get_unused_indexes` / `get_duplicate_indexes` for dead weight, `get_missing_indexes` for catalog-level suggestions, `get_table_index_usage(table)` for one hot table.
|
|
23
|
-
|
|
24
|
-
4. **Explain each recommendation** in terms the user can act on: which queries it helps, expected benefit, and write-cost trade-off. Don't just dump the list.
|
|
25
|
-
|
|
26
|
-
5. **Estimate impact before applying.** `apply_index_recommendation(recommendationId, mode="DRY_RUN")` (default) uses HypoPG on Postgres to install a virtual index and EXPLAIN the contributing queries — zero writes. Report the planner cost delta.
|
|
27
|
-
|
|
28
|
-
6. **Apply only on explicit request, admin only.** `mode="APPLY"` (real `CREATE/DROP INDEX CONCURRENTLY`) or `APPLY_AND_MEASURE` (also runs `EXPLAIN ANALYZE` before/after) require `confirm: true`. The DDL is server-generated from the recommendation row — you never supply index SQL. Surface what will run and get a human OK first.
|
|
29
|
-
|
|
30
|
-
## Guardrails
|
|
31
|
-
|
|
32
|
-
- Recommend against an existing covering index/constraint instead of a redundant single-column one — `get_index_health` / `get_table_index_usage` will show it.
|
|
33
|
-
- A `DROP_INDEX` recommendation for an "unused" index still deserves a sanity check: confirm with the user it isn't a rarely-used-but-critical path before applying.
|
|
34
|
-
- `apply_index_recommendation` is the only write-capable MCP tool. Treat every `APPLY` like a production change.
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: schema-exploration
|
|
3
|
-
description: Map or describe a database — what it tracks, its tables, relationships, and conventions — grounded in DeepSQL's cached schema and brain.
|
|
4
|
-
version: 1.0.0
|
|
5
|
-
platforms: [linux, macos, windows]
|
|
6
|
-
metadata:
|
|
7
|
-
hermes:
|
|
8
|
-
tags: [schema, explore, tables, relationships, foreign-keys, describe, deepsql, database]
|
|
9
|
-
related_skills: [bi-query, workload-analysis]
|
|
10
|
-
---
|
|
11
|
-
|
|
12
|
-
# Schema Exploration
|
|
13
|
-
|
|
14
|
-
Use when the user wants to understand the database itself ("what does this DB track?", "what tables exist?", "how are orders and customers related?", "describe the bookings table").
|
|
15
|
-
|
|
16
|
-
## Procedure
|
|
17
|
-
|
|
18
|
-
1. **Resolve the connection** (`list_connections` → UUID) if you don't have it.
|
|
19
|
-
|
|
20
|
-
2. **Get the shape.** `get_schema(connectionId)` for tables, columns, types, declared FKs. `get_database_objects(connectionId)` when you also need views/functions/procedures, not just columns. The schema is cached and authoritative — trust it over the codebase.
|
|
21
|
-
|
|
22
|
-
3. **Get the meaning.** `get_brain_context(connectionId, "<what the user is asking about>")` for the domain layer: what the tables mean, business terms, documentation. This is what turns "a list of tables" into "what the database tracks."
|
|
23
|
-
|
|
24
|
-
4. **Fill relationship gaps.** Many real databases lack declared foreign keys. `get_relationships(connectionId)` returns inferred + validated FKs with a `confidence` score and `validationStatus`. Report the confidence — a 0.95 inferred FK is reliable; a 0.4 one is a guess.
|
|
25
|
-
|
|
26
|
-
5. **Surface the rules and traps.** `list_business_rules(connectionId)` and `get_anti_patterns(connectionId, kind="table")` so the user learns the conventions and known schema smells, not just the structure.
|
|
27
|
-
|
|
28
|
-
## Reporting
|
|
29
|
-
|
|
30
|
-
- Lead with **what the database is for**, then the largest/most central tables, then notable relationships.
|
|
31
|
-
- For "largest tables," use the row counts from `get_schema` (don't run `COUNT(*)` across every table).
|
|
32
|
-
- Flag anti-patterns and low-confidence inferred FKs explicitly — they're the things a user most needs to know and least expects.
|