@deepsql/mcp 0.20.0 → 0.22.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/README.md +8 -3
- package/package.json +4 -2
- package/scripts/postinstall.js +27 -0
- package/src/agent/bootstrap.js +146 -56
- package/src/cli.js +4 -4
- package/src/commands/agent.js +94 -19
- /package/agent-profile/tui/{entry.js → dist/entry.js} +0 -0
package/README.md
CHANGED
|
@@ -53,9 +53,14 @@ DBA persona, read-only-by-default tool scoping, and DeepSQL skins.
|
|
|
53
53
|
- **No LLM key needed** — the DeepSQL backend proxies the model
|
|
54
54
|
(`/api/llm/v1`), authenticated by your `deepsql login` token. The agent
|
|
55
55
|
never sees a provider key.
|
|
56
|
-
- **
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
- **Runtime install** — by default `npm install -g @deepsql/mcp` pre-installs
|
|
57
|
+
the agent runtime (a Hermes build pinned to a known-good commit) so the first
|
|
58
|
+
`deepsql` is instant. It's best-effort and never fails the npm install, and
|
|
59
|
+
skips automatically in CI and non-global installs. Opt out with
|
|
60
|
+
`DEEPSQL_SKIP_AGENT_SETUP=1` — pure-MCP users (Claude Code / Cursor) then
|
|
61
|
+
don't pull the runtime, and it installs lazily on first `deepsql agent`
|
|
62
|
+
instead. The per-user `deepsql` profile is provisioned on first launch after
|
|
63
|
+
login (it needs your token).
|
|
59
64
|
- **Read-only by default** — only the DeepSQL tools, memory, and skills are
|
|
60
65
|
enabled; host-affecting toolsets (terminal/file/code-exec/browser) are
|
|
61
66
|
disabled. `apply_index_recommendation` stays server-side confirm-gated.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deepsql/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.0",
|
|
4
4
|
"description": "DeepSQL CLI, DBA Agent TUI, and stdio MCP server for self-hosted deployments",
|
|
5
5
|
"bin": {
|
|
6
6
|
"deepsql": "bin/deepsql.js",
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
"bin",
|
|
15
15
|
"skills",
|
|
16
16
|
"src",
|
|
17
|
+
"scripts",
|
|
17
18
|
"agent-profile",
|
|
18
19
|
"deepsql-phase1-server.js",
|
|
19
20
|
"deepsql-phase1-lib.js",
|
|
@@ -21,7 +22,8 @@
|
|
|
21
22
|
"codex_config.customer.example.toml"
|
|
22
23
|
],
|
|
23
24
|
"scripts": {
|
|
24
|
-
"test": "node --test deepsql-phase1-lib.test.js src/*.test.js src/**/*.test.js"
|
|
25
|
+
"test": "node --test deepsql-phase1-lib.test.js src/*.test.js src/**/*.test.js",
|
|
26
|
+
"postinstall": "node scripts/postinstall.js"
|
|
25
27
|
},
|
|
26
28
|
"engines": {
|
|
27
29
|
"node": ">=20"
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
// Postinstall: intentionally a no-op heavy-wise.
|
|
5
|
+
//
|
|
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
|
+
//
|
|
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).
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
if (process.env.CI) process.exit(0);
|
|
16
|
+
const isGlobal = String(process.env.npm_config_global || "") === "true";
|
|
17
|
+
if (!isGlobal) process.exit(0);
|
|
18
|
+
|
|
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.
|
|
26
|
+
}
|
|
27
|
+
process.exit(0);
|
package/src/agent/bootstrap.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
// config rewrite keeps the token/connection fresh.
|
|
8
8
|
|
|
9
9
|
const fs = require("node:fs");
|
|
10
|
+
const os = require("node:os");
|
|
10
11
|
const path = require("node:path");
|
|
11
12
|
const { spawnSync } = require("node:child_process");
|
|
12
13
|
const { userHome } = require("../user-home");
|
|
@@ -17,15 +18,65 @@ const AGENT_DIR = path.join(HERMES_HOME, "hermes-agent");
|
|
|
17
18
|
const PROFILE = "deepsql";
|
|
18
19
|
const PROFILE_HOME = path.join(HERMES_HOME, "profiles", PROFILE);
|
|
19
20
|
|
|
21
|
+
const INSTALL_URL = "https://hermes-agent.nousresearch.com/install.sh";
|
|
22
|
+
// Pin the Hermes runtime to the exact commit our bundled TUI (agent-profile/
|
|
23
|
+
// tui/entry.js, HERMES_VERSION 0.17.0) was built against. This makes installs
|
|
24
|
+
// reproducible AND guarantees the branded-TUI overlay's version guard matches,
|
|
25
|
+
// so end users get the DeepSQL wordmark/welcome — not a "latest main" build
|
|
26
|
+
// that drifts in behavior and skips our overlay. Bump alongside the bundle.
|
|
27
|
+
const HERMES_COMMIT = "92d40c2553961243991376bf889d833e8326caf7";
|
|
28
|
+
|
|
29
|
+
// Extra dirs the Hermes runtime + its installer rely on but a bare GUI/login
|
|
30
|
+
// shell PATH (as seen by a spawned Node process) often lacks: the venv, the
|
|
31
|
+
// installer's `hermes` symlink targets, and Homebrew (uv/node/git).
|
|
32
|
+
function extraPathDirs() {
|
|
33
|
+
return [
|
|
34
|
+
path.join(AGENT_DIR, ".venv", "bin"),
|
|
35
|
+
path.join(HOME, ".local", "bin"),
|
|
36
|
+
"/usr/local/bin",
|
|
37
|
+
"/opt/homebrew/bin",
|
|
38
|
+
"/usr/local/lib/hermes-agent/.venv/bin",
|
|
39
|
+
];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Env for any spawn that runs (or installs) Hermes: augmented PATH so node/uv/
|
|
43
|
+
// git resolve, UV_NO_CONFIG so uv ignores stray project configs, and a pinned
|
|
44
|
+
// HERMES_HOME so the install/profile lands where we expect.
|
|
45
|
+
function hermesEnv(extra = {}) {
|
|
46
|
+
const sep = path.delimiter;
|
|
47
|
+
const have = (process.env.PATH || "").split(sep);
|
|
48
|
+
const merged = [...extraPathDirs().filter((d) => !have.includes(d)), ...have].join(sep);
|
|
49
|
+
return { ...process.env, PATH: merged, UV_NO_CONFIG: "1", HERMES_HOME, ...extra };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// All the places the official installer may drop a runnable `hermes` for a
|
|
53
|
+
// non-root install (venv) or an FHS/root install (/usr/local), plus the PATH.
|
|
54
|
+
function findHermes() {
|
|
55
|
+
const candidates = [];
|
|
56
|
+
if (process.env.HERMES_INSTALL_DIR) {
|
|
57
|
+
candidates.push(path.join(process.env.HERMES_INSTALL_DIR, ".venv", "bin", "hermes"));
|
|
58
|
+
}
|
|
59
|
+
candidates.push(
|
|
60
|
+
path.join(AGENT_DIR, ".venv", "bin", "hermes"),
|
|
61
|
+
"/usr/local/lib/hermes-agent/.venv/bin/hermes",
|
|
62
|
+
path.join(HOME, ".local", "bin", "hermes"),
|
|
63
|
+
"/usr/local/bin/hermes"
|
|
64
|
+
);
|
|
65
|
+
for (const bin of candidates) {
|
|
66
|
+
if (!fs.existsSync(bin)) continue;
|
|
67
|
+
if (spawnSync(bin, ["--version"], { stdio: "ignore", env: hermesEnv() }).status === 0) return bin;
|
|
68
|
+
}
|
|
69
|
+
// Last resort: a `hermes` already on the (augmented) PATH.
|
|
70
|
+
if (spawnSync("hermes", ["--version"], { stdio: "ignore", env: hermesEnv() }).status === 0) return "hermes";
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
20
74
|
function hermesBin() {
|
|
21
|
-
|
|
22
|
-
return fs.existsSync(venv) ? venv : "hermes";
|
|
75
|
+
return findHermes() || path.join(AGENT_DIR, ".venv", "bin", "hermes");
|
|
23
76
|
}
|
|
24
77
|
|
|
25
78
|
function hermesInstalled() {
|
|
26
|
-
|
|
27
|
-
const r = spawnSync("hermes", ["--version"], { stdio: "ignore" });
|
|
28
|
-
return r.status === 0;
|
|
79
|
+
return findHermes() !== null;
|
|
29
80
|
}
|
|
30
81
|
|
|
31
82
|
function mcpServerPath() {
|
|
@@ -42,16 +93,39 @@ function distSource() {
|
|
|
42
93
|
return null;
|
|
43
94
|
}
|
|
44
95
|
|
|
45
|
-
|
|
96
|
+
// Install the Hermes runtime, pinned + non-interactive, with the output teed to
|
|
97
|
+
// a log so a failure surfaces a real cause instead of a vague "can't set up".
|
|
98
|
+
// `quiet` (postinstall) hides the live stream; interactive first-run shows it.
|
|
99
|
+
function ensureHermes(io, { quiet = false } = {}) {
|
|
46
100
|
const err = (io && io.stderr) || process.stderr;
|
|
47
101
|
if (hermesInstalled()) return true;
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
102
|
+
|
|
103
|
+
err.write("DeepSQL Agent: installing the agent runtime (first run; ~1–2 min)…\n");
|
|
104
|
+
const logFile = path.join(os.tmpdir(), `deepsql-agent-install-${process.pid}.log`);
|
|
105
|
+
// --commit pins the checkout; --skip-setup/--non-interactive avoid the
|
|
106
|
+
// wizard + any TTY prompt. tee keeps a persisted log for diagnostics.
|
|
107
|
+
const script =
|
|
108
|
+
`curl -fsSL ${INSTALL_URL} | bash -s -- ` +
|
|
109
|
+
`--skip-setup --non-interactive --commit ${HERMES_COMMIT} 2>&1 | tee ${JSON.stringify(logFile)}`;
|
|
110
|
+
const r = spawnSync("bash", ["-lc", script], {
|
|
111
|
+
stdio: quiet ? ["ignore", "ignore", "ignore"] : "inherit",
|
|
112
|
+
env: hermesEnv(),
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
if (hermesInstalled()) {
|
|
116
|
+
err.write(quiet
|
|
117
|
+
? "✓ DeepSQL Agent runtime installed.\n"
|
|
118
|
+
: "✓ DeepSQL Agent runtime installed. (Ignore any \"run hermes setup\" note above — DeepSQL configures it for you.)\n");
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
err.write(
|
|
122
|
+
"DeepSQL Agent: the runtime install did not complete.\n" +
|
|
123
|
+
` Install log: ${logFile}\n` +
|
|
124
|
+
` Exit code: ${r.status == null ? "n/a" : r.status}\n` +
|
|
125
|
+
" You can retry, or install Hermes manually: " +
|
|
126
|
+
`curl -fsSL ${INSTALL_URL} | bash -s -- --commit ${HERMES_COMMIT}\n`
|
|
53
127
|
);
|
|
54
|
-
return
|
|
128
|
+
return false;
|
|
55
129
|
}
|
|
56
130
|
|
|
57
131
|
function q(s) {
|
|
@@ -92,16 +166,39 @@ display:
|
|
|
92
166
|
`;
|
|
93
167
|
fs.writeFileSync(path.join(PROFILE_HOME, "config.yaml"), config);
|
|
94
168
|
|
|
169
|
+
// OPENAI_BASE_URL/OPENAI_API_KEY are belt-and-suspenders: Hermes' first-run
|
|
170
|
+
// guard (_has_any_provider_configured) treats either as "a provider is
|
|
171
|
+
// configured", so the "run hermes setup" gate can never fire on a fresh
|
|
172
|
+
// machine even if config-load timing differs. They point at the SAME backend
|
|
173
|
+
// proxy as the model block, authed by the user's DeepSQL token.
|
|
95
174
|
const env =
|
|
96
175
|
`DEEPSQL_API_BASE_URL=${apiBase}
|
|
97
176
|
DEEPSQL_AUTH_TOKEN=${token}
|
|
98
177
|
DEEPSQL_MCP_SERVER=${mcp}
|
|
99
178
|
DEEPSQL_DEFAULT_CONNECTION_ID=${conn}
|
|
179
|
+
OPENAI_BASE_URL=${proxy}
|
|
180
|
+
OPENAI_API_KEY=${token}
|
|
100
181
|
HERMES_REVISION=deepsql-cli
|
|
101
182
|
`;
|
|
102
183
|
fs.writeFileSync(path.join(PROFILE_HOME, ".env"), env, { mode: 0o600 });
|
|
103
184
|
}
|
|
104
185
|
|
|
186
|
+
// Env for spawning the TUI: augmented PATH (so the TUI's node + the MCP
|
|
187
|
+
// server's node resolve), the provider vars that defeat Hermes' first-run
|
|
188
|
+
// guard regardless of config state, and HERMES_TUI_DIR pointing at our prebuilt
|
|
189
|
+
// branded TUI (when the Hermes version matches) so Hermes runs it directly with
|
|
190
|
+
// no esbuild — branded on the first launch.
|
|
191
|
+
function launchEnv(session) {
|
|
192
|
+
const base = session.baseUrl.replace(/\/?$/, "/");
|
|
193
|
+
const extra = {
|
|
194
|
+
OPENAI_BASE_URL: base + "api/llm/v1",
|
|
195
|
+
OPENAI_API_KEY: session.token,
|
|
196
|
+
};
|
|
197
|
+
const tui = brandedTuiDir();
|
|
198
|
+
if (tui) extra.HERMES_TUI_DIR = tui;
|
|
199
|
+
return hermesEnv(extra);
|
|
200
|
+
}
|
|
201
|
+
|
|
105
202
|
function installProfileAssets(io) {
|
|
106
203
|
const err = (io && io.stderr) || process.stderr;
|
|
107
204
|
const src = distSource();
|
|
@@ -110,8 +207,19 @@ function installProfileAssets(io) {
|
|
|
110
207
|
return false;
|
|
111
208
|
}
|
|
112
209
|
const bin = hermesBin();
|
|
113
|
-
const baseEnv =
|
|
114
|
-
|
|
210
|
+
const baseEnv = hermesEnv();
|
|
211
|
+
const inst = spawnSync(
|
|
212
|
+
bin,
|
|
213
|
+
["profile", "install", src, "--name", PROFILE, "--force", "--yes"],
|
|
214
|
+
{ encoding: "utf8", env: baseEnv }
|
|
215
|
+
);
|
|
216
|
+
if (inst.status !== 0) {
|
|
217
|
+
err.write(
|
|
218
|
+
"DeepSQL Agent: provisioning the profile failed.\n" +
|
|
219
|
+
` ${(inst.stderr || inst.stdout || "(no output)").trim().split("\n").slice(-6).join("\n ")}\n`
|
|
220
|
+
);
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
115
223
|
// Skin loads from <profile>/skins/.
|
|
116
224
|
const skin = path.join(src, "skins", "deepsql.yaml");
|
|
117
225
|
if (fs.existsSync(skin)) {
|
|
@@ -119,12 +227,13 @@ function installProfileAssets(io) {
|
|
|
119
227
|
fs.mkdirSync(dst, { recursive: true });
|
|
120
228
|
fs.copyFileSync(skin, path.join(dst, "deepsql.yaml"));
|
|
121
229
|
}
|
|
122
|
-
// Read-only sandbox: only deepsql + skills/memory remain.
|
|
230
|
+
// Read-only sandbox: only deepsql + skills/memory remain. Non-fatal — the
|
|
231
|
+
// shipped distribution already scopes tools; this is defense in depth.
|
|
123
232
|
spawnSync(
|
|
124
233
|
bin,
|
|
125
234
|
["tools", "disable", "terminal", "file", "code_execution", "browser", "computer_use",
|
|
126
235
|
"image_gen", "tts", "vision", "web", "delegation", "cronjob"],
|
|
127
|
-
{ stdio: "ignore", env: {
|
|
236
|
+
{ stdio: "ignore", env: hermesEnv({ HERMES_HOME: PROFILE_HOME }) }
|
|
128
237
|
);
|
|
129
238
|
return fs.existsSync(PROFILE_HOME);
|
|
130
239
|
}
|
|
@@ -138,8 +247,7 @@ function ensureProfile(session, io) {
|
|
|
138
247
|
}
|
|
139
248
|
|
|
140
249
|
// Installed Hermes version (parsed from the runtime's __init__.py). Null if the
|
|
141
|
-
// runtime layout isn't what we expect (e.g. PATH-only install)
|
|
142
|
-
// skipped and the user gets the stock TUI with our skin.
|
|
250
|
+
// runtime layout isn't what we expect (e.g. PATH-only install).
|
|
143
251
|
function installedHermesVersion() {
|
|
144
252
|
try {
|
|
145
253
|
const init = path.join(AGENT_DIR, "hermes_cli", "__init__.py");
|
|
@@ -150,56 +258,38 @@ function installedHermesVersion() {
|
|
|
150
258
|
}
|
|
151
259
|
}
|
|
152
260
|
|
|
153
|
-
//
|
|
154
|
-
//
|
|
155
|
-
//
|
|
156
|
-
//
|
|
261
|
+
// The DeepSQL-branded prebuilt TUI bundle shipped in the package, as a directory
|
|
262
|
+
// laid out the way Hermes' `HERMES_TUI_DIR` mechanism expects: `<dir>/dist/entry.js`.
|
|
263
|
+
// When that env var points here, Hermes runs our entry.js DIRECTLY — no npm
|
|
264
|
+
// install, no esbuild — so the custom DEEPSQL wordmark / welcome panel / DBA
|
|
265
|
+
// placeholders show on the very FIRST launch. (The earlier copy-into-the-clone
|
|
266
|
+
// approach lost every launch because Hermes "always esbuild"s the normal path.)
|
|
157
267
|
//
|
|
158
|
-
// Guarded by the Hermes version our bundle was built against: a prebuilt
|
|
159
|
-
//
|
|
160
|
-
//
|
|
161
|
-
//
|
|
162
|
-
//
|
|
163
|
-
|
|
164
|
-
// We refresh the overlay on every launch and bump its mtime past the TUI build
|
|
165
|
-
// inputs so Hermes' freshness check (`_tui_need_rebuild`, an mtime compare of
|
|
166
|
-
// dist/entry.js vs src/) never rebuilds over it after a `hermes update`.
|
|
167
|
-
function ensureBrandedTui(io) {
|
|
168
|
-
const err = (io && io.stderr) || process.stderr;
|
|
268
|
+
// Guarded by the Hermes version our bundle was built against: a prebuilt entry.js
|
|
269
|
+
// speaks one release's TUI↔CLI protocol, so we only point at it on an exact
|
|
270
|
+
// version match (the runtime is pinned to HERMES_COMMIT, so it matches on our
|
|
271
|
+
// installs). On any mismatch / missing bundle we return null and let Hermes use
|
|
272
|
+
// its own (stock) TUI, which still renders our skin (DeepSQL name + maroon).
|
|
273
|
+
function brandedTuiDir() {
|
|
169
274
|
try {
|
|
170
275
|
const tuiDir = path.resolve(__dirname, "..", "..", "agent-profile", "tui");
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
const builtFor = (() => {
|
|
174
|
-
try { return fs.readFileSync(path.join(tuiDir, "HERMES_VERSION"), "utf8").trim(); }
|
|
175
|
-
catch { return null; }
|
|
176
|
-
})();
|
|
276
|
+
if (!fs.existsSync(path.join(tuiDir, "dist", "entry.js"))) return null;
|
|
277
|
+
const builtFor = fs.readFileSync(path.join(tuiDir, "HERMES_VERSION"), "utf8").trim();
|
|
177
278
|
const installed = installedHermesVersion();
|
|
178
|
-
if (!builtFor || !installed || builtFor !== installed) return
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
fs.mkdirSync(distDir, { recursive: true });
|
|
183
|
-
const same =
|
|
184
|
-
fs.existsSync(target) && fs.statSync(target).size === fs.statSync(bundledEntry).size;
|
|
185
|
-
if (!same) fs.copyFileSync(bundledEntry, target);
|
|
186
|
-
// Bump mtime past every TUI build input so the freshness check never
|
|
187
|
-
// rebuilds the stock TUI over our branded bundle.
|
|
188
|
-
const future = new Date(Date.now() + 60_000);
|
|
189
|
-
fs.utimesSync(target, future, future);
|
|
190
|
-
return true;
|
|
191
|
-
} catch (e) {
|
|
192
|
-
err.write(`DeepSQL Agent: branded TUI overlay skipped (${e.message}); using stock TUI.\n`);
|
|
193
|
-
return false;
|
|
279
|
+
if (!builtFor || !installed || builtFor !== installed) return null;
|
|
280
|
+
return tuiDir;
|
|
281
|
+
} catch {
|
|
282
|
+
return null;
|
|
194
283
|
}
|
|
195
284
|
}
|
|
196
285
|
|
|
197
286
|
module.exports = {
|
|
198
287
|
ensureHermes,
|
|
199
288
|
ensureProfile,
|
|
200
|
-
|
|
289
|
+
brandedTuiDir,
|
|
201
290
|
hermesBin,
|
|
202
291
|
hermesInstalled,
|
|
292
|
+
launchEnv,
|
|
203
293
|
PROFILE,
|
|
204
294
|
PROFILE_HOME,
|
|
205
295
|
};
|
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: {
|
package/src/commands/agent.js
CHANGED
|
@@ -1,15 +1,44 @@
|
|
|
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
|
+
|
|
19
|
+
// The brokered turn can run a full multi-tool agent loop server-side; allow well
|
|
20
|
+
// past the backend's own per-turn ceiling so we don't abandon a valid answer.
|
|
21
|
+
const TURN_TIMEOUT_MS = 320000;
|
|
22
|
+
|
|
23
|
+
async function runTurn(session, connectionId, message, conversationId) {
|
|
24
|
+
return request(session.baseUrl, "/agent/chat", {
|
|
25
|
+
method: "POST",
|
|
26
|
+
token: session.token,
|
|
27
|
+
json: { message, connectionId, conversationId },
|
|
28
|
+
timeoutMs: TURN_TIMEOUT_MS,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function renderReply(stdout, resp) {
|
|
33
|
+
if (resp && resp.ok && resp.answer && String(resp.answer).trim()) {
|
|
34
|
+
stdout.write(`\n${String(resp.answer).trim()}\n`);
|
|
35
|
+
} else {
|
|
36
|
+
stdout.write(`\n⚠ ${(resp && resp.error) || "the agent run ended early"}\n`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
11
39
|
|
|
12
40
|
async function run(opts, io = {}) {
|
|
41
|
+
const stdout = io.stdout || process.stdout;
|
|
13
42
|
const stderr = io.stderr || process.stderr;
|
|
14
43
|
|
|
15
44
|
let session;
|
|
@@ -20,23 +49,69 @@ async function run(opts, io = {}) {
|
|
|
20
49
|
return 1;
|
|
21
50
|
}
|
|
22
51
|
|
|
23
|
-
if
|
|
24
|
-
|
|
25
|
-
|
|
52
|
+
// Connection is optional — the agent can chat without one and will ask if a
|
|
53
|
+
// question needs a database. Resolve the active default when present.
|
|
54
|
+
let connectionId = null;
|
|
55
|
+
try {
|
|
56
|
+
connectionId = await resolveConnectionId(session, opts.connection);
|
|
57
|
+
} catch {
|
|
58
|
+
/* no default connection set — fine */
|
|
26
59
|
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
60
|
+
|
|
61
|
+
const message = (opts.positional || []).join(" ").trim();
|
|
62
|
+
|
|
63
|
+
// One-shot: a question on the command line.
|
|
64
|
+
if (message) {
|
|
65
|
+
try {
|
|
66
|
+
const resp = await runTurn(session, connectionId, message, null);
|
|
67
|
+
renderReply(stdout, resp);
|
|
68
|
+
return resp && resp.ok ? 0 : 1;
|
|
69
|
+
} catch (e) {
|
|
70
|
+
stderr.write(`Agent request failed: ${e.message}\n`);
|
|
71
|
+
return 1;
|
|
72
|
+
}
|
|
30
73
|
}
|
|
31
|
-
ensureBrandedTui(io); // best-effort: full branded TUI when versions match, else stock TUI + skin
|
|
32
74
|
|
|
33
|
-
//
|
|
34
|
-
|
|
75
|
+
// Interactive: a line REPL against the server agent. Requires a TTY.
|
|
76
|
+
if (!stdout.isTTY) {
|
|
77
|
+
stderr.write('Usage: deepsql agent "<question>" (or run in a terminal for interactive chat)\n');
|
|
78
|
+
return 2;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
stdout.write("DeepSQL Agent — connected to your server agent. Type a question; `exit` or Ctrl-C to quit.\n");
|
|
82
|
+
// conversationId stays null on the first turn so the server resumes your most
|
|
83
|
+
// recent conversation for this connection (shared with the web Agent tab);
|
|
84
|
+
// subsequent turns reuse the id it returns.
|
|
85
|
+
let conversationId = null;
|
|
86
|
+
const rl = readline.createInterface({ input: process.stdin, output: stdout, prompt: "\nyou › " });
|
|
87
|
+
rl.prompt();
|
|
88
|
+
|
|
35
89
|
return await new Promise((resolve) => {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
90
|
+
rl.on("line", async (line) => {
|
|
91
|
+
const text = line.trim();
|
|
92
|
+
if (!text) {
|
|
93
|
+
rl.prompt();
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (text === "exit" || text === "quit") {
|
|
97
|
+
rl.close();
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
rl.pause();
|
|
101
|
+
stdout.write("…thinking\n");
|
|
102
|
+
try {
|
|
103
|
+
const resp = await runTurn(session, connectionId, text, conversationId);
|
|
104
|
+
if (resp && resp.conversationId) conversationId = resp.conversationId;
|
|
105
|
+
renderReply(stdout, resp);
|
|
106
|
+
} catch (e) {
|
|
107
|
+
stdout.write(`\n⚠ ${e.message}\n`);
|
|
108
|
+
}
|
|
109
|
+
rl.resume();
|
|
110
|
+
rl.prompt();
|
|
111
|
+
});
|
|
112
|
+
rl.on("close", () => {
|
|
113
|
+
stdout.write("\nBye.\n");
|
|
114
|
+
resolve(0);
|
|
40
115
|
});
|
|
41
116
|
});
|
|
42
117
|
}
|
|
File without changes
|