@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 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
- - **First run** installs the agent runtime (Python via the Hermes
57
- installer + Node for the TUI) and provisions a local `deepsql` profile.
58
- Subsequent launches are instant.
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.20.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);
@@ -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
- const venv = path.join(AGENT_DIR, ".venv", "bin", "hermes");
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
- if (fs.existsSync(path.join(AGENT_DIR, ".venv", "bin", "hermes"))) return true;
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
- function ensureHermes(io) {
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
- err.write("DeepSQL Agent needs its runtime (first run) — installing, this may take a minute…\n");
49
- const r = spawnSync(
50
- "bash",
51
- ["-lc", "curl -fsSL https://hermes-agent.nousresearch.com/install.sh | bash -s -- --skip-setup"],
52
- { stdio: "inherit" }
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 r.status === 0 && hermesInstalled();
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 = { ...process.env, UV_NO_CONFIG: "1" };
114
- spawnSync(bin, ["profile", "install", src, "--name", PROFILE, "--force", "--yes"], { stdio: "ignore", env: baseEnv });
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: { ...baseEnv, HERMES_HOME: PROFILE_HOME } }
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) — overlay is then
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
- // Overlay the prebuilt, DeepSQL-branded TUI bundle (custom DEEPSQL wordmark,
154
- // "What I can do" welcome panel, DBA placeholders) onto the installed Hermes
155
- // runtime's `ui-tui/dist/entry.js`. This delivers the full branded experience
156
- // with NO build step on the user's machine.
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
- // entry.js speaks the TUI↔CLI protocol of one Hermes release, so we overlay
160
- // only on an exact version match. On any mismatch (or a missing bundle) we
161
- // leave the stock TUI in place it still renders our skin (DeepSQL name +
162
- // maroon palette via skins/deepsql.yaml), just without the custom wordmark.
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
- const bundledEntry = path.join(tuiDir, "entry.js");
172
- if (!fs.existsSync(bundledEntry)) return false; // skin-only branding
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 false; // degrade to stock TUI + skin
179
-
180
- const distDir = path.join(AGENT_DIR, "ui-tui", "dist");
181
- const target = path.join(distDir, "entry.js");
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
- ensureBrandedTui,
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: "Launch the DeepSQL Agent — an interactive chat TUI for DBA work, BI questions, and database guardrails. Running `deepsql` with no command in a terminal launches it too.",
94
- usage: "deepsql agent [options] (or just: deepsql)",
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>", "Default connection for the session"],
97
+ ["--connection <name>", "Connection to ground answers on (default: saved active connection)"],
98
98
  ],
99
- notes: "First run installs the agent runtime. Uses your saved `deepsql login` no LLM key needed (the backend proxies the model).",
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: {
@@ -1,15 +1,44 @@
1
1
  "use strict";
2
2
 
3
- // `deepsql agent` (and bare `deepsql` in a terminal): launch the DeepSQL Agent
4
- // a branded Hermes TUI for DBA / BI / database-guard chat. Resolves the saved
5
- // login, lazily installs the runtime, provisions the `deepsql` profile pointed
6
- // at the backend LLM proxy (no LLM key needed), then hands the terminal to the TUI.
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 { spawn } = require("node:child_process");
14
+ const readline = require("node:readline");
15
+ const { request } = require("../api/client");
9
16
  const { resolveSession } = require("./_session");
10
- const { ensureHermes, ensureProfile, ensureBrandedTui, hermesBin, PROFILE } = require("../agent/bootstrap");
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 (!ensureHermes(io)) {
24
- stderr.write("Could not set up the DeepSQL Agent runtime. See https://github.com/NousResearch/hermes-agent for manual install.\n");
25
- return 1;
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
- if (!ensureProfile(session, io)) {
28
- stderr.write("Could not provision the DeepSQL Agent profile.\n");
29
- return 1;
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
- // Hand the terminal to the branded TUI (profile config sets interface=tui).
34
- const child = spawn(hermesBin(), ["-p", PROFILE, "--tui"], { stdio: "inherit", env: process.env });
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
- child.on("exit", (code) => resolve(code ?? 0));
37
- child.on("error", (err) => {
38
- stderr.write(`Failed to launch the DeepSQL Agent: ${err.message}\n`);
39
- resolve(1);
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