@deepsql/mcp 0.22.0 → 0.26.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/CLAUDE.md CHANGED
@@ -33,8 +33,10 @@ server, and the statement you ran.** Don't be sloppy.
33
33
 
34
34
  ## The tools you have
35
35
 
36
- The MCP server exposes **41 tools** as of 0.19.0 — the original 16 +
37
- 0.18.x's 5 slow-query tools + 0.19.0's 20 CLI-parity additions. Most
36
+ The MCP server exposes **44 tools** as of 0.26.0 — the original 16 +
37
+ 0.18.x's 5 slow-query tools + 0.19.0's 20 CLI-parity additions + 0.26.0's
38
+ 3 brain-notes tools (`list_brain_recommendations`, `save_brain_note`,
39
+ `list_brain_notes`). Most
38
40
  take a `connectionId` (UUID returned by `list_connections`); a few
39
41
  take server-resolved row ids (`apply_index_recommendation` →
40
42
  `recommendationId`, `dismiss_index_recommendation` →
@@ -65,6 +67,9 @@ in this version; ask before mid-session admin work.
65
67
  | `list_business_rules` | Active business rules and SQL guardrails. Honor these — they encode domain semantics. |
66
68
  | `get_relationships` | Inferred + validated foreign keys with confidence scores. Many real DBs lack declared FKs; this fills the gap. |
67
69
  | `get_anti_patterns` | Schema-level (`kind=table`) or query-level (`kind=query`) anti-patterns. |
70
+ | `list_brain_recommendations` | The brain's AI-proposed notes to review for a connection (priority, reason, indicators, suggested prompt). The company-context review queue. |
71
+ | `save_brain_note` | **Accept/save a fact into the shared brain** — grounds every future answer for the connection. TABLE- or COLUMN-scoped. Admin (manage-content), audited. Personal preferences belong in a DeepSQL skill, not here. |
72
+ | `list_brain_notes` | Knowledge already saved to the brain (filter by table/column; can be thousands). |
68
73
  | `analyze_slow_queries` | Recent slow queries with fingerprints, durations, examples. Read-only; doesn't trigger new work. |
69
74
  | `get_slow_query_timeline` | Day-by-day timeline for one query from the 30-day analytics store — call count, mean/max time, regression factor per day. Identify the query by its fingerprint (the `queryId` from `analyze_slow_queries`). Answers "is this query getting slower". |
70
75
  | `get_query_regressions` | Slow queries that regressed (got slower) on the latest daily analysis run, ranked by slowdown factor. Read-only. |
package/README.md CHANGED
@@ -88,7 +88,7 @@ Restart the editor for the entry to load.
88
88
 
89
89
  ## What the MCP server exposes
90
90
 
91
- **41 tools** as of 0.19.0. Read-only at the schema/retrieval/diagnostics
91
+ **44 tools** as of 0.26.0. Read-only at the schema/retrieval/diagnostics
92
92
  layer; policy-gated at the SQL layer (only `execute_sql`,
93
93
  `analyze_query_plan` with `useAnalyze=true`, and
94
94
  `apply_index_recommendation` can write — the rest are reads or low-stakes
@@ -112,7 +112,7 @@ per-user admin ops (`users`/`access`/`permissions`).
112
112
  | `test_connection` | Run the privilege report (+ SSH check); reuses saved creds — no plaintext crosses the wire |
113
113
  | `reinit_connection_brain` | Trigger a fresh schema scan + brain re-embed |
114
114
 
115
- ### Schema + retrieval brain (5)
115
+ ### Schema + retrieval brain (8)
116
116
 
117
117
  | Tool | Purpose |
118
118
  |---|---|
@@ -121,6 +121,9 @@ per-user admin ops (`users`/`access`/`permissions`).
121
121
  | `get_brain_context` | Retrieval brain: tables/columns/FKs/training docs/rules for a question |
122
122
  | `list_business_rules` | Active business rules and SQL guardrails for a connection |
123
123
  | `get_relationships` | Inferred + validated foreign keys with confidence scores |
124
+ | `list_brain_recommendations` | The brain's AI-proposed notes to review (company-context queue) |
125
+ | `save_brain_note` | Accept/save a fact into the shared brain (admin; grounds future answers) |
126
+ | `list_brain_notes` | Knowledge already saved to the brain (filterable) |
124
127
 
125
128
  ### Anti-patterns + daily digest (4)
126
129
 
@@ -143,6 +143,51 @@ const TOOL_DEFINITIONS = [
143
143
  additionalProperties: false,
144
144
  },
145
145
  },
146
+ {
147
+ name: "list_brain_recommendations",
148
+ description:
149
+ "List the brain's AI-proposed recommendations for a connection — high-value tables/columns DeepSQL suggests documenting, each with a priority (P0/P1…), the reason, supporting indicators, and a suggested prompt to explore. This is the company-context review queue: an admin reviews these and accepts the good ones with save_brain_note. Returns { suggestions, totalCount } (totalCount reflects the requested limit, not an absolute total).",
150
+ inputSchema: {
151
+ type: "object",
152
+ properties: {
153
+ connectionId: { type: "string", description: "DeepSQL connection ID." },
154
+ limit: { type: "integer", minimum: 1, maximum: 100, description: "Max recommendations to return (default 10)." },
155
+ },
156
+ required: ["connectionId"],
157
+ additionalProperties: false,
158
+ },
159
+ },
160
+ {
161
+ name: "save_brain_note",
162
+ description:
163
+ "Accept/save a fact into the connection's BRAIN — shared, company-level context that grounds EVERY future answer for this connection (not a per-user preference). Use this to accept a recommendation from list_brain_recommendations, or to record any durable fact about a table/column. Scope is TABLE (tableName only) or COLUMN (tableName + columnName). Requires manage-content permission on the connection (admin) — the backend enforces and audits it. NOTE: an individual's personal preference (how *they* like answers formatted, a private shortcut) belongs in a DeepSQL skill on their own profile, NOT here — this writes to the shared brain everyone sees.",
164
+ inputSchema: {
165
+ type: "object",
166
+ properties: {
167
+ connectionId: { type: "string", description: "DeepSQL connection ID." },
168
+ tableName: { type: "string", description: "Table the note is about (required)." },
169
+ columnName: { type: "string", description: "Column the note is about; omit for a table-scoped note." },
170
+ noteText: { type: "string", description: "The fact/guidance to remember." },
171
+ },
172
+ required: ["connectionId", "tableName", "noteText"],
173
+ additionalProperties: false,
174
+ },
175
+ },
176
+ {
177
+ name: "list_brain_notes",
178
+ description:
179
+ "List knowledge already saved to the connection's brain (the notes that ground answers). Optionally filter by tableName and/or columnName — a trained connection can hold thousands of notes, so filtering is recommended. Use this to check whether a fact is already remembered before saving a new one with save_brain_note.",
180
+ inputSchema: {
181
+ type: "object",
182
+ properties: {
183
+ connectionId: { type: "string", description: "DeepSQL connection ID." },
184
+ tableName: { type: "string", description: "Filter to one table." },
185
+ columnName: { type: "string", description: "Filter to one column (use with tableName)." },
186
+ },
187
+ required: ["connectionId"],
188
+ additionalProperties: false,
189
+ },
190
+ },
146
191
  {
147
192
  name: "apply_index_recommendation",
148
193
  description:
@@ -1149,6 +1194,25 @@ function summarizeRelationships(payload) {
1149
1194
  return `${list.length} relationship(s) (${high} high-confidence).`;
1150
1195
  }
1151
1196
 
1197
+ function summarizeBrainRecommendations(payload) {
1198
+ const list = (payload && payload.suggestions) || [];
1199
+ if (!list.length) return "No brain recommendations pending review for this connection.";
1200
+ const top = list
1201
+ .slice(0, 5)
1202
+ .map((s) => `${s.priority || ""} ${s.columnName ? `${s.tableName}.${s.columnName}` : s.tableName}`.trim());
1203
+ return `${payload.totalCount ?? list.length} recommendation(s) to review. Top: ${top.join("; ")}. Accept the good ones with save_brain_note.`;
1204
+ }
1205
+
1206
+ function summarizeBrainNoteSaved(payload) {
1207
+ const target = payload && (payload.columnName ? `${payload.tableName}.${payload.columnName}` : payload.tableName);
1208
+ return `Saved to brain${target ? ` — ${target}` : ""}. It now grounds future answers for this connection (shared, company-level).`;
1209
+ }
1210
+
1211
+ function summarizeBrainNotes(payload) {
1212
+ const list = Array.isArray(payload) ? payload : (payload && payload.notes) || [];
1213
+ return `${list.length} brain note(s) for the requested scope.`;
1214
+ }
1215
+
1152
1216
  function summarizeAntiPatterns(payload, kind) {
1153
1217
  if (kind === "table") {
1154
1218
  const tables = payload && typeof payload === "object" ? Object.keys(payload).length : 0;
@@ -1548,6 +1612,15 @@ function buildToolResult(name, payload, extra = {}) {
1548
1612
  case "get_growth_anomalies":
1549
1613
  summary = summarizeGrowthAnomalies(payload);
1550
1614
  break;
1615
+ case "list_brain_recommendations":
1616
+ summary = summarizeBrainRecommendations(payload);
1617
+ break;
1618
+ case "save_brain_note":
1619
+ summary = summarizeBrainNoteSaved(payload);
1620
+ break;
1621
+ case "list_brain_notes":
1622
+ summary = summarizeBrainNotes(payload);
1623
+ break;
1551
1624
  case "get_index_recommendations":
1552
1625
  summary = summarizeIndexRecommendations(payload);
1553
1626
  break;
@@ -1596,10 +1669,28 @@ function buildToolError(message, extra = {}) {
1596
1669
  };
1597
1670
  }
1598
1671
 
1672
+ // Short-TTL in-memory cache for the connections list. The MCP server is a
1673
+ // long-lived process and `list_connections` is the "call this first" tool, so an
1674
+ // agent hits it repeatedly in a session — caching makes those calls instant. The
1675
+ // list changes rarely; a 30s TTL keeps it fresh enough.
1676
+ const CONNECTIONS_CACHE_TTL_MS = 30000;
1677
+ let _connectionsCache = null; // { key, ts, payload }
1678
+
1679
+ async function fetchConnectionsCached(config) {
1680
+ const key = `${(config && config.baseUrl) || ""}|${(config && config.authToken) || ""}`;
1681
+ if (_connectionsCache && _connectionsCache.key === key
1682
+ && Date.now() - _connectionsCache.ts < CONNECTIONS_CACHE_TTL_MS) {
1683
+ return _connectionsCache.payload;
1684
+ }
1685
+ const payload = await callDeepSqlApi(config, "/connections");
1686
+ _connectionsCache = { key, ts: Date.now(), payload };
1687
+ return payload;
1688
+ }
1689
+
1599
1690
  async function handleToolCall(config, name, args = {}) {
1600
1691
  switch (name) {
1601
1692
  case "list_connections": {
1602
- const payload = await callDeepSqlApi(config, "/connections");
1693
+ const payload = await fetchConnectionsCached(config);
1603
1694
  return buildToolResult(name, payload);
1604
1695
  }
1605
1696
 
@@ -1684,6 +1775,52 @@ async function handleToolCall(config, name, args = {}) {
1684
1775
  return buildToolResult(name, payload, { kind });
1685
1776
  }
1686
1777
 
1778
+ case "list_brain_recommendations": {
1779
+ const connectionId = String(args.connectionId || "").trim();
1780
+ if (!connectionId) return buildToolError("connectionId is required.");
1781
+ const limit = clampInteger(args.limit, 1, 100, 10);
1782
+ const payload = await callDeepSqlApi(
1783
+ config,
1784
+ `/brain/notes/suggestions/${encodeURIComponent(connectionId)}?limit=${limit}`,
1785
+ );
1786
+ return buildToolResult(name, payload);
1787
+ }
1788
+
1789
+ case "save_brain_note": {
1790
+ const connectionId = String(args.connectionId || "").trim();
1791
+ const tableName = String(args.tableName || "").trim();
1792
+ const noteText = String(args.noteText || "").trim();
1793
+ if (!connectionId) return buildToolError("connectionId is required.");
1794
+ if (!tableName) return buildToolError("tableName is required.");
1795
+ if (!noteText) return buildToolError("noteText is required.");
1796
+ const columnName = args.columnName ? String(args.columnName).trim() : null;
1797
+ // Backend enforces manage-content permission (admin) + audits the write.
1798
+ const payload = await callDeepSqlApi(config, "/brain/notes", {
1799
+ method: "POST",
1800
+ json: {
1801
+ connectionId,
1802
+ scopeType: columnName ? "COLUMN" : "TABLE",
1803
+ tableName,
1804
+ columnName,
1805
+ noteText,
1806
+ },
1807
+ });
1808
+ return buildToolResult(name, payload);
1809
+ }
1810
+
1811
+ case "list_brain_notes": {
1812
+ const connectionId = String(args.connectionId || "").trim();
1813
+ if (!connectionId) return buildToolError("connectionId is required.");
1814
+ const qs = [];
1815
+ if (args.tableName) qs.push(`tableName=${encodeURIComponent(String(args.tableName))}`);
1816
+ if (args.columnName) qs.push(`columnName=${encodeURIComponent(String(args.columnName))}`);
1817
+ const payload = await callDeepSqlApi(
1818
+ config,
1819
+ `/brain/notes/${encodeURIComponent(connectionId)}${qs.length ? `?${qs.join("&")}` : ""}`,
1820
+ );
1821
+ return buildToolResult(name, payload);
1822
+ }
1823
+
1687
1824
  case "get_index_recommendations": {
1688
1825
  const connectionId = String(args.connectionId || "").trim();
1689
1826
  if (!connectionId) return buildToolError("connectionId is required.");
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@deepsql/mcp",
3
- "version": "0.22.0",
4
- "description": "DeepSQL CLI, DBA Agent TUI, and stdio MCP server for self-hosted deployments",
3
+ "version": "0.26.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",
@@ -2,7 +2,7 @@
2
2
 
3
3
  You have two DeepSQL surfaces available:
4
4
 
5
- 1. **MCP tools** — JSON-RPC tools loaded into your session (41 of them
5
+ 1. **MCP tools** — JSON-RPC tools loaded into your session (44 of them
6
6
  as of 0.19.0). The MCP surface mirrors the CLI for almost every
7
7
  read/diagnostic operation, plus the two execute tools and the index-
8
8
  apply tool.
@@ -91,6 +91,9 @@ usually doesn't know about either; that's exactly why DeepSQL exists.
91
91
  | `list_business_rules(connectionId, question?)` | Rules the SQL must respect. |
92
92
  | `get_relationships(connectionId)` | Foreign keys (declared + inferred-with-confidence). |
93
93
  | `get_anti_patterns(connectionId, kind="table"\|"query")` | Patterns to avoid in this DB. |
94
+ | `list_brain_recommendations(connectionId, limit?)` | The brain's AI-proposed things to document (the review queue). Admin accepts good ones with `save_brain_note`. |
95
+ | `save_brain_note(connectionId, tableName, noteText, columnName?)` | **Accept/save a fact to the SHARED company brain** — grounds every future answer for this connection. Admin (manage-content). NOT for personal prefs — those go in a DeepSQL skill. |
96
+ | `list_brain_notes(connectionId, tableName?, columnName?)` | Knowledge already in the brain; filter before saving a duplicate. |
94
97
  | `analyze_slow_queries(connectionId, thresholdMs?, limit?)` | Snapshot of slow queries from live stats. |
95
98
  | `get_slow_query_timeline(connectionId, fingerprint)` | Day-by-day timeline for one fingerprint: call count, mean/max time, regression factor per day. Answers "is this query getting slower". |
96
99
  | `get_query_regressions(connectionId, minFactor?)` | Slow queries that got slower on the latest daily analysis run, ranked by slowdown factor. |
@@ -138,7 +141,7 @@ called by claude-code via deepsql CLI").
138
141
  deepsql query "SELECT 1" --connection prod-pg --caller-agent claude-code --json
139
142
  ```
140
143
 
141
- ### Command catalog (20 top-level commands)
144
+ ### Command catalog (21 top-level commands)
142
145
 
143
146
  | Command | What it does | MCP equivalent? |
144
147
  |---|---|---|
@@ -154,6 +157,7 @@ deepsql query "SELECT 1" --connection prod-pg --caller-agent claude-code --json
154
157
  | `deepsql analyze "<sql>" --connection <c>` | AI plan analysis (`--analyze` for EXPLAIN ANALYZE) | `analyze_query_plan` |
155
158
  | `deepsql schema [tables\|objects] --connection <c>` | Dump full schema as JSON | `get_schema` / `get_database_objects` |
156
159
  | `deepsql brain-context "<question>" --connection <c>` | Same retrieval as the MCP tool | `get_brain_context` |
160
+ | `deepsql brain recommendations\|notes\|remember` | Review what DeepSQL has learned (AI-proposed notes), list saved notes, and `remember "<note>" --table <t> [--column <c>]` to teach it (admin) | none — CLI/web surface for the brain-notes loop |
157
161
  | `deepsql business-rules --connection <c>` | List active business rules | `list_business_rules` |
158
162
  | `deepsql relationships --connection <c>` | Inferred + validated FKs | `get_relationships` |
159
163
  | `deepsql anti-patterns --connection <c> [--kind table\|query]` | Anti-patterns | `get_anti_patterns` |
package/src/cli.js CHANGED
@@ -28,6 +28,7 @@ const COMMANDS = {
28
28
  // Brain tools — give a coding agent the same retrieval context the chat
29
29
  // pipeline uses, then let the agent generate SQL/answers itself.
30
30
  "brain-context": () => require("./commands/brain-context"),
31
+ brain: () => require("./commands/brain"),
31
32
  "business-rules": () => require("./commands/business-rules"),
32
33
  relationships: () => require("./commands/relationships"),
33
34
  "anti-patterns": () => require("./commands/anti-patterns"),
@@ -60,6 +61,7 @@ const COMMAND_LIST = [
60
61
  ["digest", true, "Show DeepSQL daily digests"],
61
62
  ["growth", true, "Table growth analytics — trends, history, anomalies, alert config"],
62
63
  ["brain-context", false, "Retrieve embedding-ranked context for a question"],
64
+ ["brain", true, "Review brain recommendations and remember facts (admin)"],
63
65
  ["business-rules", false, "List active business rules and SQL guardrails"],
64
66
  ["relationships", false, "List inferred and validated FK relationships"],
65
67
  ["anti-patterns", false, "List schema- or query-level anti-patterns"],
@@ -161,7 +163,7 @@ const COMMAND_HELP = {
161
163
  description: "Manage database connections — list, pin, full CRUD.",
162
164
  usage: "deepsql connections <subcommand> [options]",
163
165
  subcommands: [
164
- ["list [--json]", "List database connections (active default marked with *)"],
166
+ ["list [--json] [--refresh]", "List database connections (cached ~60s; --refresh forces live)"],
165
167
  ["use <name>", "Pin <name> as the active default for this profile"],
166
168
  ["current", "Print the active default (exit 1 if none)"],
167
169
  ["unset", "Clear the active default for this profile"],
@@ -174,6 +176,7 @@ const COMMAND_HELP = {
174
176
  ["init <name> [--force] [--wait]", "Trigger brain re-initialization"],
175
177
  ],
176
178
  options: [
179
+ ["--refresh", "Bypass the connections cache and fetch live (list)"],
177
180
  ["--from-file <p>", "Read connection JSON from file"],
178
181
  ["--from-stdin", "Read connection JSON from stdin"],
179
182
  ["--upsert", "PUT instead of POST on add if name collision"],
@@ -261,6 +264,24 @@ const COMMAND_HELP = {
261
264
  notes: "`capture` and `config set` are the only mutations; both are admin-level but neither writes user data. Trends + anomalies are the agent-facing subcommands and are also exposed via the MCP tools `get_table_growth` and `get_growth_anomalies`.",
262
265
  },
263
266
 
267
+ brain: {
268
+ description: "Review what DeepSQL has learned about a connection and teach it more (admin).",
269
+ usage: "deepsql brain <subcommand> [options]",
270
+ subcommands: [
271
+ ["recommendations [--connection <name>] [--limit <n>]", "AI-proposed things to document/investigate (alias: recs)"],
272
+ ["notes [--connection <name>] [--table <t>] [--column <c>]", "Knowledge already saved to the brain (filterable)"],
273
+ ['remember "<note>" --table <t> [--column <c>]', "Save a fact to the brain (alias: save)"],
274
+ ],
275
+ options: [
276
+ ["--connection <name>", "Connection to act on (default: active connection)"],
277
+ ["--table <t>", "Scope notes/remember to a table"],
278
+ ["--column <c>", "Scope to a column (with --table)"],
279
+ ["--limit <n>", "Cap results"],
280
+ ["--json", "Raw JSON output"],
281
+ ],
282
+ notes: "`recommendations` reads /brain/notes/suggestions; `remember` writes a brain note (requires manage-content permission on the connection). The same recommendations surface backs the web UI.",
283
+ },
284
+
264
285
  "brain-context": {
265
286
  description: "Retrieve embedding-ranked tables/columns/FKs, training docs, and business rules for a question.",
266
287
  usage: 'deepsql brain-context "<question>" --connection <name> [options]',
@@ -581,6 +602,9 @@ function buildOpts(parsed) {
581
602
  label: f.label || null,
582
603
  // Connection / users / chat
583
604
  connection: f.connection || f.c || null,
605
+ // Brain notes scoping
606
+ table: f.table || null,
607
+ column: f.column || null,
584
608
  chat: f.chat || null,
585
609
  user: f.user || null,
586
610
  project: f.project || null,
package/src/cli.test.js CHANGED
@@ -75,7 +75,7 @@ test("every command in the catalog has a COMMAND_HELP entry", () => {
75
75
  const expected = [
76
76
  "agent",
77
77
  "login","logout","whoami","config","mcp","connections","query","analyze","schema",
78
- "digest","brain-context","business-rules","relationships","anti-patterns","indexes",
78
+ "digest","brain-context","brain","business-rules","relationships","anti-patterns","indexes",
79
79
  "users","access","permissions","slow-queries","setup",
80
80
  ];
81
81
  for (const name of expected) {
@@ -137,6 +137,7 @@ test("--no-color suppresses ANSI escapes in help output", async () => {
137
137
  // a `SUBCOMMANDS` object whose keys are the dispatchable subcommand names.
138
138
  const HELP_DRIFT_TARGETS = [
139
139
  { command: "slow-queries", modulePath: "./commands/slow-queries" },
140
+ { command: "brain", modulePath: "./commands/brain" },
140
141
  ];
141
142
 
142
143
  for (const { command, modulePath } of HELP_DRIFT_TARGETS) {
@@ -0,0 +1,227 @@
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 AMBER = 178; // #d7af00 — "needs attention" marker, readable on light & dark
39
+ const LOGO_ROWS = [PURPLE_LIGHT, PURPLE_LIGHT, PURPLE, PURPLE, PURPLE_DEEP];
40
+
41
+ function c(n, s, on) {
42
+ return on ? `\x1b[38;5;${n}m${s}\x1b[0m` : s;
43
+ }
44
+ function dim(s, on) {
45
+ return on ? `\x1b[2m${s}\x1b[0m` : s;
46
+ }
47
+
48
+ // Several sample prompts per category; one is picked at random each launch.
49
+ const PROMPTS = {
50
+ DBA: [
51
+ "why is the orders query slow?",
52
+ "what indexes should I add?",
53
+ "is this query using the right index?",
54
+ "what's my slowest query today?",
55
+ ],
56
+ BI: [
57
+ "revenue by city this month",
58
+ "how many bookings last week?",
59
+ "top 10 customers by spend",
60
+ "signups per day this month",
61
+ ],
62
+ Guardian: [
63
+ "is this migration safe?",
64
+ "who’s driving DB load?",
65
+ "any tables bloating?",
66
+ "what schema changed recently?",
67
+ ],
68
+ };
69
+
70
+ function pick(arr) {
71
+ return arr[Math.floor(Math.random() * arr.length)];
72
+ }
73
+
74
+ // Prompt label for the readline input line (purple, brand-forward).
75
+ function promptLabel(useColor) {
76
+ return "\n" + c(PURPLE, "deepsql ›", useColor) + " ";
77
+ }
78
+
79
+ function renderIntro(
80
+ stdout,
81
+ {
82
+ useColor = true,
83
+ connectionLabel = null,
84
+ versionLine = null,
85
+ connections = [],
86
+ suggestions = [],
87
+ recommendationCount = 0,
88
+ } = {}
89
+ ) {
90
+ const out = [""];
91
+ LOGO.forEach((row, i) => out.push(" " + c(LOGO_ROWS[i], row, useColor)));
92
+ out.push("");
93
+ out.push(" " + dim("I am your DBA and Data agent. DeepSQL is the brain for your database", useColor));
94
+ out.push("");
95
+ out.push(" " + c(PURPLE, "Try these prompts", useColor));
96
+ for (const cat of ["DBA", "BI", "Guardian"]) {
97
+ // Label reads "<cat> prompt" (not a bare "DBA" that looks like a command);
98
+ // purple label, prompt text stays default-fg so it's always readable.
99
+ const label = `${cat} prompt`;
100
+ out.push(" " + c(PURPLE_LIGHT, label.padEnd(16), useColor) + `"${pick(PROMPTS[cat])}"`);
101
+ }
102
+
103
+ // Connections the token can see (frictionless onboarding: guide to add one if none).
104
+ out.push("");
105
+ if (connections.length === 0) {
106
+ out.push(" " + c(PURPLE, "No databases connected yet", useColor));
107
+ out.push(" " + dim("→ ", useColor) + "deepsql connections add");
108
+ } else {
109
+ out.push(" " + c(PURPLE, `Connections (${connections.length})`, useColor));
110
+ const width = Math.min(24, Math.max(...connections.map((x) => x.name.length)));
111
+ for (const conn of connections.slice(0, 8)) {
112
+ out.push(" " + conn.name.padEnd(width + 2) + dim(conn.dbType, useColor));
113
+ }
114
+ if (connections.length > 8) out.push(" " + dim(`… +${connections.length - 8} more`, useColor));
115
+ }
116
+
117
+ // Admin "needs attention" — config suggestions for connections the user manages.
118
+ if (suggestions.length) {
119
+ out.push("");
120
+ out.push(" " + c(AMBER, "Needs attention", useColor));
121
+ for (const s of suggestions.slice(0, 6)) {
122
+ out.push(
123
+ " " + c(AMBER, "⚠ ", useColor) + s.conn + dim(" · ", useColor) + s.text
124
+ );
125
+ if (s.fix) out.push(" " + dim("→ ", useColor) + dim(s.fix, useColor));
126
+ }
127
+ if (suggestions.length > 6) out.push(" " + dim(`… +${suggestions.length - 6} more`, useColor));
128
+ }
129
+
130
+ // Brain learning loop: prompt admins to review what DeepSQL wants to remember.
131
+ if (recommendationCount > 0) {
132
+ out.push("");
133
+ out.push(
134
+ " " +
135
+ c(PURPLE, `${recommendationCount} brain recommendation${recommendationCount === 1 ? "" : "s"} to review`, useColor) +
136
+ dim(" → deepsql brain recs", useColor)
137
+ );
138
+ }
139
+
140
+ out.push("");
141
+ if (connectionLabel) {
142
+ out.push(" " + dim("Grounded on ", useColor) + connectionLabel);
143
+ }
144
+ out.push(
145
+ " " +
146
+ dim("Ask a question · ", useColor) +
147
+ "exit" +
148
+ dim(" to quit · ", useColor) +
149
+ "deepsql --help" +
150
+ dim(" for CLI commands", useColor)
151
+ );
152
+ if (versionLine) out.push(" " + versionLine);
153
+ out.push("");
154
+ stdout.write(out.join("\n") + "\n");
155
+ }
156
+
157
+ function compareSemver(a, b) {
158
+ const pa = String(a).split(".").map((n) => parseInt(n, 10) || 0);
159
+ const pb = String(b).split(".").map((n) => parseInt(n, 10) || 0);
160
+ for (let i = 0; i < 3; i++) {
161
+ if ((pa[i] || 0) > (pb[i] || 0)) return 1;
162
+ if ((pa[i] || 0) < (pb[i] || 0)) return -1;
163
+ }
164
+ return 0;
165
+ }
166
+
167
+ // Best-effort latest-version lookup from the npm registry, with a tight timeout
168
+ // so it never noticeably delays startup. Resolves null on any failure.
169
+ function fetchLatest(timeoutMs = 1200) {
170
+ return new Promise((resolve) => {
171
+ let done = false;
172
+ const finish = (v) => {
173
+ if (!done) {
174
+ done = true;
175
+ resolve(v);
176
+ }
177
+ };
178
+ try {
179
+ const req = https.get(
180
+ "https://registry.npmjs.org/@deepsql/mcp/latest",
181
+ { timeout: timeoutMs, headers: { accept: "application/json" } },
182
+ (res) => {
183
+ if (res.statusCode !== 200) {
184
+ res.resume();
185
+ return finish(null);
186
+ }
187
+ let d = "";
188
+ res.on("data", (chunk) => (d += chunk));
189
+ res.on("end", () => {
190
+ try {
191
+ finish(JSON.parse(d).version || null);
192
+ } catch {
193
+ finish(null);
194
+ }
195
+ });
196
+ }
197
+ );
198
+ req.on("timeout", () => {
199
+ req.destroy();
200
+ finish(null);
201
+ });
202
+ req.on("error", () => finish(null));
203
+ } catch {
204
+ finish(null);
205
+ }
206
+ });
207
+ }
208
+
209
+ // "v0.22.0 · latest" / "v0.22.0 · update available → v0.23.0 npm i -g …".
210
+ // Falls back to a plain dim version when the registry can't be reached.
211
+ async function getVersionLine(useColor) {
212
+ const cur = PKG_VERSION;
213
+ if (!cur) return null;
214
+ const latest = await fetchLatest();
215
+ if (!latest) return dim(`v${cur}`, useColor);
216
+ if (compareSemver(latest, cur) > 0) {
217
+ return (
218
+ c(PURPLE_LIGHT, `v${cur}`, useColor) +
219
+ dim(" · update available → ", useColor) +
220
+ c(PURPLE, `v${latest}`, useColor) +
221
+ dim(" npm i -g @deepsql/mcp@latest", useColor)
222
+ );
223
+ }
224
+ return dim(`v${cur} · latest`, useColor);
225
+ }
226
+
227
+ module.exports = { renderIntro, getVersionLine, promptLabel };