sandal-db 1.0.2 → 1.0.4

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.
Files changed (2) hide show
  1. package/dist/cli.js +1033 -341
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -3,15 +3,287 @@
3
3
  // src/cli.ts
4
4
  import fs4 from "node:fs";
5
5
  import { Command } from "commander";
6
- import chalk4 from "chalk";
6
+ import chalk5 from "chalk";
7
7
  import ora2 from "ora";
8
- import { input as input2, select, confirm as confirm2 } from "@inquirer/prompts";
8
+ import { input as input2, select as select2, confirm } from "@inquirer/prompts";
9
9
 
10
10
  // src/config/index.ts
11
11
  import fs from "node:fs";
12
12
  import path from "node:path";
13
13
  import os from "node:os";
14
14
  import dotenv from "dotenv";
15
+
16
+ // src/config/models.ts
17
+ import chalk from "chalk";
18
+ import Table from "cli-table3";
19
+ import { select, input } from "@inquirer/prompts";
20
+ var PROVIDER_MODELS = {
21
+ google: [
22
+ {
23
+ id: "gemini-3.8-flash",
24
+ name: "Gemini 3.8 Flash",
25
+ description: "Latest Gemini 3 Flash. High speed, low latency, advanced reasoning & multimodal",
26
+ contextWindow: "1M",
27
+ badge: "Latest Flagship",
28
+ recommended: true
29
+ },
30
+ {
31
+ id: "gemini-3.7-flash",
32
+ name: "Gemini 3.7 Flash",
33
+ description: "Hybrid reasoning model with dynamic thinking tokens",
34
+ contextWindow: "1M",
35
+ badge: "Hybrid Reasoning"
36
+ },
37
+ {
38
+ id: "gemini-3.1-pro-preview",
39
+ name: "Gemini 3.1 Pro",
40
+ description: "Frontier reasoning for complex schemas, agentic planning & SQL logic",
41
+ contextWindow: "2M",
42
+ badge: "Frontier Pro"
43
+ },
44
+ {
45
+ id: "gemini-2.5-flash",
46
+ name: "Gemini 2.5 Flash",
47
+ description: "Production workhorse, high throughput, balanced performance",
48
+ contextWindow: "1M",
49
+ badge: "Production Stable"
50
+ },
51
+ {
52
+ id: "gemini-2.5-pro",
53
+ name: "Gemini 2.5 Pro",
54
+ description: "Deep reasoning and complex SQL query generation across massive schemas",
55
+ contextWindow: "2M"
56
+ },
57
+ {
58
+ id: "gemini-3.5-flash-lite",
59
+ name: "Gemini 3.5 Flash-Lite",
60
+ description: "Ultra-fast lightweight model for budget-sensitive high-QPS tasks",
61
+ contextWindow: "1M",
62
+ badge: "Fast & Budget"
63
+ },
64
+ {
65
+ id: "gemini-2.5-flash-lite",
66
+ name: "Gemini 2.5 Flash-Lite",
67
+ description: "Fast and economical multimodal model",
68
+ contextWindow: "1M"
69
+ },
70
+ {
71
+ id: "gemini-2.0-flash",
72
+ name: "Gemini 2.0 Flash",
73
+ description: "Stable legacy 2.0 Flash model",
74
+ contextWindow: "1M"
75
+ }
76
+ ],
77
+ openai: [
78
+ {
79
+ id: "gpt-5.6-sol",
80
+ name: "GPT-5.6 Sol",
81
+ description: "Flagship frontier model for complex professional work, coding & database tasks",
82
+ contextWindow: "1M+",
83
+ badge: "Latest Flagship",
84
+ recommended: true
85
+ },
86
+ {
87
+ id: "gpt-6-astra",
88
+ name: "GPT-6 Astra",
89
+ description: "OpenAI most capable model, built for hardest end-to-end coding & reasoning",
90
+ contextWindow: "1M+",
91
+ badge: "Frontier Peak"
92
+ },
93
+ {
94
+ id: "gpt-5.6-terra",
95
+ name: "GPT-5.6 Terra",
96
+ description: "Balanced intelligence and cost for general database exploration",
97
+ contextWindow: "1M+",
98
+ badge: "Balanced"
99
+ },
100
+ {
101
+ id: "gpt-5.6-luna",
102
+ name: "GPT-5.6 Luna",
103
+ description: "Optimized for high-volume, cost-sensitive production workloads",
104
+ contextWindow: "1M+",
105
+ badge: "Economical"
106
+ },
107
+ {
108
+ id: "gpt-5.4",
109
+ name: "GPT-5.4",
110
+ description: "High-performance model for coding and analytical workflows",
111
+ contextWindow: "272K+"
112
+ },
113
+ {
114
+ id: "gpt-5.4-mini",
115
+ name: "GPT-5.4 Mini",
116
+ description: "Strongest mini model for coding, tool use, and subagents",
117
+ contextWindow: "128K",
118
+ badge: "Fast & Smart"
119
+ },
120
+ {
121
+ id: "o4-mini",
122
+ name: "o4-mini",
123
+ description: "Next-gen fast, cost-efficient reasoning model with thinking capabilities",
124
+ contextWindow: "200K",
125
+ badge: "Fast Reasoning"
126
+ },
127
+ {
128
+ id: "o3-mini",
129
+ name: "o3-mini",
130
+ description: "Compact reasoning model with configurable thinking effort",
131
+ contextWindow: "200K"
132
+ },
133
+ {
134
+ id: "o3",
135
+ name: "o3",
136
+ description: "High-compute reasoning model for complex analytics and tricky queries",
137
+ contextWindow: "200K",
138
+ badge: "Deep Reasoning"
139
+ },
140
+ {
141
+ id: "gpt-4.1",
142
+ name: "GPT-4.1",
143
+ description: "Smartest non-reasoning direct chat & generation model",
144
+ contextWindow: "128K"
145
+ },
146
+ {
147
+ id: "gpt-4o",
148
+ name: "GPT-4o",
149
+ description: "Versatile multimodal workhorse for general tasks",
150
+ contextWindow: "128K"
151
+ },
152
+ {
153
+ id: "gpt-4o-mini",
154
+ name: "GPT-4o Mini",
155
+ description: "Affordable small model for fast queries and schema lookups",
156
+ contextWindow: "128K"
157
+ }
158
+ ],
159
+ anthropic: [
160
+ {
161
+ id: "claude-sonnet-5",
162
+ name: "Claude Sonnet 5",
163
+ description: "Optimal balance of speed, intelligence & 1M token context window",
164
+ contextWindow: "1M",
165
+ badge: "Latest Flagship",
166
+ recommended: true
167
+ },
168
+ {
169
+ id: "claude-opus-5",
170
+ name: "Claude Opus 5",
171
+ description: "Complex agentic coding, database refactoring & enterprise reasoning",
172
+ contextWindow: "1M",
173
+ badge: "Deep Intelligence"
174
+ },
175
+ {
176
+ id: "claude-fable-5-1",
177
+ name: "Claude Fable 5.1",
178
+ description: "Demanding reasoning and long-horizon autonomous agentic work",
179
+ contextWindow: "1M",
180
+ badge: "Long Horizon"
181
+ },
182
+ {
183
+ id: "claude-haiku-4-5",
184
+ name: "Claude Haiku 4.5",
185
+ description: "Fastest Claude model with near-frontier intelligence for rapid responses",
186
+ contextWindow: "200K",
187
+ badge: "Ultra Fast"
188
+ },
189
+ {
190
+ id: "claude-sonnet-4-6",
191
+ name: "Claude Sonnet 4.6",
192
+ description: "Pinned dateless enterprise release for production stability",
193
+ contextWindow: "500K"
194
+ },
195
+ {
196
+ id: "claude-3-7-sonnet-latest",
197
+ name: "Claude 3.7 Sonnet",
198
+ description: "Hybrid reasoning model with adjustable thinking budgets",
199
+ contextWindow: "200K",
200
+ badge: "Hybrid Reasoning"
201
+ },
202
+ {
203
+ id: "claude-3-5-sonnet-latest",
204
+ name: "Claude 3.5 Sonnet",
205
+ description: "Industry benchmark for coding and function calling",
206
+ contextWindow: "200K"
207
+ },
208
+ {
209
+ id: "claude-3-5-haiku-latest",
210
+ name: "Claude 3.5 Haiku",
211
+ description: "Ultra-fast lightweight tool use model",
212
+ contextWindow: "200K"
213
+ }
214
+ ]
215
+ };
216
+ function getModelsForProvider(provider) {
217
+ return PROVIDER_MODELS[provider] || [];
218
+ }
219
+ function getModelChoices(provider, currentModel) {
220
+ const models = getModelsForProvider(provider);
221
+ const choices = models.map((m) => {
222
+ const isCurrent = m.id === currentModel;
223
+ const badgeText = m.badge ? chalk.dim(` [${m.badge}]`) : "";
224
+ const recText = m.recommended ? chalk.green(" \u2605 Recommended") : "";
225
+ const currentText = isCurrent ? chalk.cyan(" (Current)") : "";
226
+ return {
227
+ name: `${chalk.bold(m.id)}${recText}${badgeText}${currentText}`,
228
+ value: m.id,
229
+ description: `${m.name} (Context: ${m.contextWindow}) \u2014 ${m.description}`
230
+ };
231
+ });
232
+ choices.push({
233
+ name: chalk.yellow("Custom model (type manually)..."),
234
+ value: "__custom__",
235
+ description: "Specify an unlisted or experimental model identifier"
236
+ });
237
+ return choices;
238
+ }
239
+ async function promptModelSelection(provider, currentModel) {
240
+ const choices = getModelChoices(provider, currentModel);
241
+ const selected = await select({
242
+ message: `Select LLM model for ${chalk.bold.cyan(provider.toUpperCase())}:`,
243
+ choices,
244
+ pageSize: 10
245
+ });
246
+ if (selected === "__custom__") {
247
+ const custom = await input({
248
+ message: "Enter custom model name:",
249
+ validate: (val) => val.trim().length > 0 ? true : "Model name cannot be empty."
250
+ });
251
+ return custom.trim();
252
+ }
253
+ return selected;
254
+ }
255
+ function renderModelsTable(provider) {
256
+ const providers = provider ? [provider] : ["google", "openai", "anthropic"];
257
+ const lines = [];
258
+ for (const prov of providers) {
259
+ const models = getModelsForProvider(prov);
260
+ const table = new Table({
261
+ head: [
262
+ chalk.bold.cyan("Model ID"),
263
+ chalk.bold.cyan("Display Name"),
264
+ chalk.bold.cyan("Context"),
265
+ chalk.bold.cyan("Category / Badge"),
266
+ chalk.bold.cyan("Description")
267
+ ],
268
+ colWidths: [26, 20, 10, 20, 42],
269
+ wordWrap: true
270
+ });
271
+ for (const m of models) {
272
+ const idCol = m.recommended ? chalk.green.bold(`\u2605 ${m.id}`) : chalk.white.bold(m.id);
273
+ const badgeCol = m.badge ? chalk.yellow(m.badge) : chalk.gray("-");
274
+ table.push([idCol, m.name, m.contextWindow, badgeCol, m.description]);
275
+ }
276
+ lines.push(
277
+ chalk.bold.magenta(`
278
+ === Available Public Models for ${prov.toUpperCase()} ===
279
+ `)
280
+ );
281
+ lines.push(table.toString());
282
+ }
283
+ return lines.join("\n");
284
+ }
285
+
286
+ // src/config/index.ts
15
287
  dotenv.config();
16
288
  var CONFIG_DIR_NAME = ".sandal";
17
289
  var LEGACY_CONFIG_DIR_NAME = ".db-agent";
@@ -85,6 +357,17 @@ function maskApiKey(rawKey) {
85
357
  }
86
358
  return `${trimmed.slice(0, 4)}...${trimmed.slice(-4)}`;
87
359
  }
360
+ function getStoredApiKeyForProvider(provider) {
361
+ const config = readConfig();
362
+ switch (provider) {
363
+ case "google":
364
+ return process.env.GEMINI_API_KEY || config.geminiApiKey;
365
+ case "openai":
366
+ return process.env.OPENAI_API_KEY || config.openaiApiKey;
367
+ case "anthropic":
368
+ return process.env.ANTHROPIC_API_KEY || config.anthropicApiKey;
369
+ }
370
+ }
88
371
  function getDefaultModelForProvider(provider) {
89
372
  switch (provider) {
90
373
  case "google":
@@ -154,7 +437,7 @@ function resolveCredentials(options) {
154
437
  keySource = "config";
155
438
  }
156
439
  }
157
- if (!apiKey) {
440
+ if (!apiKey && !options.cliProvider) {
158
441
  if (process.env.GEMINI_API_KEY || config.geminiApiKey) {
159
442
  provider = "google";
160
443
  apiKey = process.env.GEMINI_API_KEY || config.geminiApiKey;
@@ -170,6 +453,8 @@ function resolveCredentials(options) {
170
453
  } else {
171
454
  keySource = "none";
172
455
  }
456
+ } else if (!apiKey) {
457
+ keySource = "none";
173
458
  }
174
459
  }
175
460
  const model = options.cliModel || config.defaultModel || getDefaultModelForProvider(provider);
@@ -965,12 +1250,12 @@ function createChatModel(options) {
965
1250
  }
966
1251
 
967
1252
  // src/repl.ts
968
- import readline from "node:readline";
1253
+ import readline2 from "node:readline";
969
1254
  import fs3 from "node:fs";
970
- import chalk3 from "chalk";
1255
+ import chalk4 from "chalk";
971
1256
  import boxen2 from "boxen";
972
1257
  import ora from "ora";
973
- import Table from "cli-table3";
1258
+ import Table2 from "cli-table3";
974
1259
 
975
1260
  // src/agent/graph.ts
976
1261
  import { Annotation, StateGraph, START, END, MemorySaver } from "@langchain/langgraph";
@@ -1382,19 +1667,51 @@ function classifyMongoQuery(rawText, query, rowThreshold, options) {
1382
1667
  }
1383
1668
 
1384
1669
  // src/safety/confirm.ts
1385
- import chalk from "chalk";
1670
+ import readline from "node:readline";
1671
+ import chalk2 from "chalk";
1386
1672
  import boxen from "boxen";
1387
- import { confirm, input } from "@inquirer/prompts";
1388
- async function requestUserConfirmation(query, safety, affectedRows) {
1673
+ async function defaultAsk(promptText) {
1674
+ process.stdin.resume();
1675
+ const rl = readline.createInterface({
1676
+ input: process.stdin,
1677
+ output: process.stdout
1678
+ });
1679
+ return new Promise((resolve) => {
1680
+ rl.question(promptText, (answer) => {
1681
+ rl.close();
1682
+ process.stdin.resume();
1683
+ resolve(answer);
1684
+ });
1685
+ });
1686
+ }
1687
+ async function promptConfirm(message, defaultValue, ask = defaultAsk) {
1688
+ const suffix = defaultValue ? chalk2.gray(" (Y/n): ") : chalk2.gray(" (y/N): ");
1689
+ const response = (await ask(`${message}${suffix}`)).trim().toLowerCase();
1690
+ if (!response) {
1691
+ return defaultValue;
1692
+ }
1693
+ if (["y", "yes", "true", "1"].includes(response)) {
1694
+ return true;
1695
+ }
1696
+ if (["n", "no", "false", "0"].includes(response)) {
1697
+ return false;
1698
+ }
1699
+ return defaultValue;
1700
+ }
1701
+ async function promptInput(message, ask = defaultAsk) {
1702
+ const response = await ask(message);
1703
+ return response.trim();
1704
+ }
1705
+ async function requestUserConfirmation(query, safety, affectedRows, ask = defaultAsk) {
1389
1706
  const queryDisplay = (query.sql || query.rawDisplay || "").trim();
1390
1707
  if (safety.isFullWipe) {
1391
1708
  const boxContent = [
1392
- chalk.bold.red("\u{1F6A8} CRITICAL: FULL DATABASE / ALL TABLES WIPE ATTEMPTED \u{1F6A8}"),
1709
+ chalk2.bold.red("\u{1F6A8} CRITICAL: FULL DATABASE / ALL TABLES WIPE ATTEMPTED \u{1F6A8}"),
1393
1710
  "",
1394
- chalk.yellow(queryDisplay),
1711
+ chalk2.yellow(queryDisplay),
1395
1712
  "",
1396
- ...safety.warnings.length ? [chalk.red(safety.warnings.join("\n")), ""] : [],
1397
- chalk.bold.white(`Type ${chalk.red.underline(safety.literalWord || "DROP DATABASE")} to proceed:`)
1713
+ ...safety.warnings.length ? [chalk2.red(safety.warnings.join("\n")), ""] : [],
1714
+ chalk2.bold.white(`Type ${chalk2.red.underline(safety.literalWord || "DROP DATABASE")} to proceed:`)
1398
1715
  ].join("\n");
1399
1716
  console.log(
1400
1717
  boxen(boxContent, {
@@ -1404,9 +1721,10 @@ async function requestUserConfirmation(query, safety, affectedRows) {
1404
1721
  margin: { top: 1, bottom: 1 }
1405
1722
  })
1406
1723
  );
1407
- const typed = await input({
1408
- message: chalk.red.bold(`Type "${safety.literalWord || "DROP DATABASE"}" to confirm full wipe:`)
1409
- });
1724
+ const typed = await promptInput(
1725
+ chalk2.red.bold(`Type "${safety.literalWord || "DROP DATABASE"}" to confirm full wipe: `),
1726
+ ask
1727
+ );
1410
1728
  if (typed.trim() === safety.literalWord) {
1411
1729
  return { confirmed: true };
1412
1730
  }
@@ -1417,20 +1735,20 @@ async function requestUserConfirmation(query, safety, affectedRows) {
1417
1735
  }
1418
1736
  if (safety.category === "dangerous") {
1419
1737
  const lines2 = [
1420
- chalk.bold.hex("#FFA500")("\u26A0\uFE0F DANGEROUS OPERATION"),
1738
+ chalk2.bold.hex("#FFA500")("\u26A0\uFE0F DANGEROUS OPERATION"),
1421
1739
  "",
1422
- chalk.white(queryDisplay),
1740
+ chalk2.white(queryDisplay),
1423
1741
  ""
1424
1742
  ];
1425
1743
  if (affectedRows !== null) {
1426
1744
  lines2.push(
1427
- chalk.bold.yellow(`Affected rows: ${affectedRows.toLocaleString()}`),
1745
+ chalk2.bold.yellow(`Affected rows: ${affectedRows.toLocaleString()}`),
1428
1746
  ""
1429
1747
  );
1430
1748
  }
1431
1749
  if (safety.warnings.length > 0) {
1432
1750
  for (const w of safety.warnings) {
1433
- lines2.push(chalk.red(`\u2022 ${w}`));
1751
+ lines2.push(chalk2.red(`\u2022 ${w}`));
1434
1752
  }
1435
1753
  lines2.push("");
1436
1754
  }
@@ -1443,11 +1761,12 @@ async function requestUserConfirmation(query, safety, affectedRows) {
1443
1761
  })
1444
1762
  );
1445
1763
  if (safety.requiresLiteralWord && safety.literalWord) {
1446
- const typed = await input({
1447
- message: chalk.hex("#FFA500").bold(
1448
- `Destructive operation with no filter. Type "${safety.literalWord}" to confirm:`
1449
- )
1450
- });
1764
+ const typed = await promptInput(
1765
+ chalk2.hex("#FFA500").bold(
1766
+ `Destructive operation with no filter. Type "${safety.literalWord}" to confirm: `
1767
+ ),
1768
+ ask
1769
+ );
1451
1770
  if (typed.trim() === safety.literalWord) {
1452
1771
  return { confirmed: true };
1453
1772
  }
@@ -1456,31 +1775,32 @@ async function requestUserConfirmation(query, safety, affectedRows) {
1456
1775
  reason: `Confirmation word mismatch (expected "${safety.literalWord}"). Operation cancelled.`
1457
1776
  };
1458
1777
  }
1459
- const answer2 = await confirm({
1460
- message: chalk.hex("#FFA500").bold("Proceed with dangerous operation?"),
1461
- default: false
1462
- });
1778
+ const answer2 = await promptConfirm(
1779
+ chalk2.hex("#FFA500").bold("Proceed with dangerous operation?"),
1780
+ false,
1781
+ ask
1782
+ );
1463
1783
  return { confirmed: answer2 };
1464
1784
  }
1465
1785
  if (safety.isStructural) {
1466
1786
  const lines2 = [
1467
- chalk.bold.cyan("\u{1F6E0}\uFE0F STRUCTURAL DDL OPERATION"),
1787
+ chalk2.bold.cyan("\u{1F6E0}\uFE0F STRUCTURAL DDL OPERATION"),
1468
1788
  "",
1469
- chalk.white(queryDisplay),
1789
+ chalk2.white(queryDisplay),
1470
1790
  "",
1471
- chalk.cyan(`Effect: ${safety.explanation || "Modifies database structure."}`)
1791
+ chalk2.cyan(`Effect: ${safety.explanation || "Modifies database structure."}`)
1472
1792
  ];
1473
1793
  if (safety.suggestedAlternative) {
1474
1794
  lines2.push(
1475
1795
  "",
1476
- chalk.yellow("\u{1F4A1} Recommendation:"),
1477
- chalk.gray(safety.suggestedAlternative)
1796
+ chalk2.yellow("\u{1F4A1} Recommendation:"),
1797
+ chalk2.gray(safety.suggestedAlternative)
1478
1798
  );
1479
1799
  }
1480
1800
  if (safety.warnings.length > 0) {
1481
1801
  lines2.push("");
1482
1802
  for (const w of safety.warnings) {
1483
- lines2.push(chalk.yellow(`\u2022 ${w}`));
1803
+ lines2.push(chalk2.yellow(`\u2022 ${w}`));
1484
1804
  }
1485
1805
  }
1486
1806
  console.log(
@@ -1491,20 +1811,21 @@ async function requestUserConfirmation(query, safety, affectedRows) {
1491
1811
  margin: { top: 1, bottom: 1 }
1492
1812
  })
1493
1813
  );
1494
- const answer2 = await confirm({
1495
- message: chalk.cyan.bold("Apply this structural change?"),
1496
- default: true
1497
- });
1814
+ const answer2 = await promptConfirm(
1815
+ chalk2.cyan.bold("Apply this structural change?"),
1816
+ true,
1817
+ ask
1818
+ );
1498
1819
  return { confirmed: answer2 };
1499
1820
  }
1500
1821
  if (safety.category === "write") {
1501
1822
  const lines2 = [
1502
- chalk.bold.magenta("\u270F\uFE0F DATABASE WRITE OPERATION"),
1823
+ chalk2.bold.magenta("\u270F\uFE0F DATABASE WRITE OPERATION"),
1503
1824
  "",
1504
- chalk.white(queryDisplay)
1825
+ chalk2.white(queryDisplay)
1505
1826
  ];
1506
1827
  if (affectedRows !== null) {
1507
- lines2.push("", chalk.magenta(`Estimated affected rows: ${affectedRows.toLocaleString()}`));
1828
+ lines2.push("", chalk2.magenta(`Estimated affected rows: ${affectedRows.toLocaleString()}`));
1508
1829
  }
1509
1830
  console.log(
1510
1831
  boxen(lines2.join("\n"), {
@@ -1514,16 +1835,17 @@ async function requestUserConfirmation(query, safety, affectedRows) {
1514
1835
  margin: { top: 1, bottom: 1 }
1515
1836
  })
1516
1837
  );
1517
- const answer2 = await confirm({
1518
- message: chalk.magenta.bold("Execute write query?"),
1519
- default: false
1520
- });
1838
+ const answer2 = await promptConfirm(
1839
+ chalk2.magenta.bold("Execute write query?"),
1840
+ false,
1841
+ ask
1842
+ );
1521
1843
  return { confirmed: answer2 };
1522
1844
  }
1523
1845
  const lines = [
1524
- chalk.bold.green("\u{1F50D} READ QUERY"),
1846
+ chalk2.bold.green("\u{1F50D} READ QUERY"),
1525
1847
  "",
1526
- chalk.white(queryDisplay)
1848
+ chalk2.white(queryDisplay)
1527
1849
  ];
1528
1850
  console.log(
1529
1851
  boxen(lines.join("\n"), {
@@ -1533,10 +1855,11 @@ async function requestUserConfirmation(query, safety, affectedRows) {
1533
1855
  margin: { top: 1, bottom: 1 }
1534
1856
  })
1535
1857
  );
1536
- const answer = await confirm({
1537
- message: chalk.green.bold("Execute read query?"),
1538
- default: true
1539
- });
1858
+ const answer = await promptConfirm(
1859
+ chalk2.green.bold("Execute read query?"),
1860
+ true,
1861
+ ask
1862
+ );
1540
1863
  return { confirmed: answer };
1541
1864
  }
1542
1865
 
@@ -1666,8 +1989,33 @@ var OBVIOUS_DATABASE_PATTERNS = [
1666
1989
  /\b(?:table|tables|collection|collections|schema|column|columns|index|indexes|foreign key|primary key)\b/i,
1667
1990
  /\b(?:find|aggregate|count|where|group by|order by|having|limit|join|left join|inner join)\b/i,
1668
1991
  /\b(?:how many (?:users|rows|records|orders|items|products|documents))\b/i,
1669
- /\b(?:database|postgres|mongodb|mongo|pg_)\b/i
1992
+ /\b(?:database|postgres|mongodb|mongo|pg_)\b/i,
1993
+ /\b(?:query|queries|sql|result|results|rows?|records?)\b/i
1670
1994
  ];
1995
+ function isExplicitNoQuery(input3) {
1996
+ const trimmed = input3.trim();
1997
+ const patterns = [
1998
+ // don't / do not / shouldn't / stop / avoid running / executing / calling / issuing query / queries / sql / command
1999
+ /\b(?:don'?t|do\s+not|never|shouldn'?t|stop|avoid)\s+(?:run|running|execute|executing|call|calling|issue|issuing|send|sending|make|making)\s+(?:any\s+|the\s+|a\s+|new\s+)?(?:query|queries|sql|command|db\s+query|database\s+query)\b/i,
2000
+ // without / no running / executing / making queries
2001
+ /\b(?:without|no)\s+(?:running|executing|making|issuing|calling)\s+(?:any\s+|the\s+|a\s+|new\s+)?(?:query|queries|sql|command)\b/i,
2002
+ // no query / no queries / skip the query / skip execution
2003
+ /\b(?:no\s+query|no\s+queries|skip\s+(?:the\s+)?query|skip\s+execution)\b/i,
2004
+ // don't query / do not query (the database)
2005
+ /\b(?:don'?t|do\s+not|never)\s+query(?:\s+(?:the\s+database|the\s+db|again))?\b/i,
2006
+ // don't / do not run / execute it
2007
+ /\b(?:don'?t|do\s+not|never)\s+(?:run|execute)\s+it\b/i,
2008
+ // don't execute anything / don't run anything
2009
+ /\b(?:don'?t|do\s+not|never)\s+(?:run|execute)\s+anything\b/i,
2010
+ // just use / take / look at / do something with the previous / last / prior results / output / rows
2011
+ /\bjust\s+(?:use|take|from|look\s+at|work\s+with|do\s+something\s+with)\s+(?:the\s+)?(?:previous|last|prior|above|earlier)\s+(?:results?|data|output|rows?)\b/i,
2012
+ // from / using / with the previous results ... don't run / do not run / without querying
2013
+ /\b(?:from|using|with)\s+(?:the\s+)?(?:previous|last|prior|above|earlier)\s+(?:results?|data|output|rows?)[^,\n.]*?(?:don'?t|do\s+not|without)\s+(?:run|running|query|querying)\b/i,
2014
+ // don't run ... previous results
2015
+ /\b(?:don'?t|do\s+not|without)\s+(?:run|running|execute|executing)[^,\n.]*?(?:previous|last|prior|above|earlier)\s+(?:results?|data|output|rows?)\b/i
2016
+ ];
2017
+ return patterns.some((pattern) => pattern.test(trimmed));
2018
+ }
1671
2019
  async function classifyIntent(input3, model, historyContext) {
1672
2020
  const trimmed = input3.trim();
1673
2021
  for (const pattern of OBVIOUS_OUT_OF_SCOPE_PATTERNS) {
@@ -1678,6 +2026,9 @@ async function classifyIntent(input3, model, historyContext) {
1678
2026
  };
1679
2027
  }
1680
2028
  }
2029
+ if (isExplicitNoQuery(trimmed)) {
2030
+ return { isDatabaseTask: true };
2031
+ }
1681
2032
  for (const pattern of OBVIOUS_DATABASE_PATTERNS) {
1682
2033
  if (pattern.test(trimmed)) {
1683
2034
  return {
@@ -1688,7 +2039,9 @@ async function classifyIntent(input3, model, historyContext) {
1688
2039
  if (/^(?:tables|collections|schema|indexes|views|stats|count|info)$/i.test(trimmed)) {
1689
2040
  return { isDatabaseTask: true };
1690
2041
  }
1691
- if (historyContext && /^(?:now\s+|and\s+|also\s+|then\s+)?(?:count|sort|filter|show|order|group|limit|delete|update|find|export|why|how many|what about|which ones?|where)\b/i.test(trimmed)) {
2042
+ if (historyContext && /^(?:now\s+|and\s+|also\s+|then\s+)?(?:count|sort|filter|show|order|group|limit|delete|update|find|export|why|how many|what about|which ones?|where|don'?t|do not|format|summarize|explain)\b/i.test(
2043
+ trimmed
2044
+ )) {
1692
2045
  return { isDatabaseTask: true };
1693
2046
  }
1694
2047
  if (!model) {
@@ -1753,10 +2106,22 @@ var AgentStateAnnotation = Annotation.Root({
1753
2106
  reducer: (_, update) => update ?? [],
1754
2107
  default: () => []
1755
2108
  }),
2109
+ requiresQuery: Annotation({
2110
+ reducer: (_, update) => update ?? true,
2111
+ default: () => true
2112
+ }),
2113
+ queryExecuted: Annotation({
2114
+ reducer: (_, update) => update ?? false,
2115
+ default: () => false
2116
+ }),
1756
2117
  generatedQuery: Annotation({
1757
2118
  reducer: (_, update) => update,
1758
2119
  default: () => void 0
1759
2120
  }),
2121
+ lastGeneratedQuery: Annotation({
2122
+ reducer: (curr, update) => update ?? curr,
2123
+ default: () => void 0
2124
+ }),
1760
2125
  safety: Annotation({
1761
2126
  reducer: (_, update) => update,
1762
2127
  default: () => void 0
@@ -1777,6 +2142,10 @@ var AgentStateAnnotation = Annotation.Root({
1777
2142
  reducer: (_, update) => update,
1778
2143
  default: () => void 0
1779
2144
  }),
2145
+ lastQueryResult: Annotation({
2146
+ reducer: (curr, update) => update ?? curr,
2147
+ default: () => void 0
2148
+ }),
1780
2149
  analysis: Annotation({
1781
2150
  reducer: (_, update) => update,
1782
2151
  default: () => void 0
@@ -1785,6 +2154,10 @@ var AgentStateAnnotation = Annotation.Root({
1785
2154
  reducer: (_, update) => update,
1786
2155
  default: () => void 0
1787
2156
  }),
2157
+ lastStats: Annotation({
2158
+ reducer: (curr, update) => update ?? curr,
2159
+ default: () => void 0
2160
+ }),
1788
2161
  conclusion: Annotation({
1789
2162
  reducer: (_, update) => update,
1790
2163
  default: () => void 0
@@ -1794,7 +2167,7 @@ var AgentStateAnnotation = Annotation.Root({
1794
2167
  default: () => false
1795
2168
  }),
1796
2169
  messages: Annotation({
1797
- reducer: (curr, update) => curr.concat(update ?? []),
2170
+ reducer: (curr, update) => update !== void 0 ? update : curr,
1798
2171
  default: () => []
1799
2172
  })
1800
2173
  });
@@ -1813,6 +2186,94 @@ function createDatabaseAgent(config) {
1813
2186
  requiresSchemaRefresh: false
1814
2187
  };
1815
2188
  }
2189
+ async function determineActionNode(state) {
2190
+ const userInput = state.userInput.trim();
2191
+ const baseReset = {
2192
+ queryExecuted: false,
2193
+ generatedQuery: void 0,
2194
+ queryResult: void 0,
2195
+ safety: void 0,
2196
+ confirmed: false,
2197
+ cancelReason: void 0,
2198
+ conclusion: void 0
2199
+ };
2200
+ if (isExplicitNoQuery(userInput)) {
2201
+ return {
2202
+ ...baseReset,
2203
+ requiresQuery: false
2204
+ };
2205
+ }
2206
+ const isObviousLiveQuery = /^(?:SELECT|INSERT|UPDATE|DELETE|DROP|CREATE|ALTER|TRUNCATE|GRANT|REVOKE)\b/i.test(userInput) || /^(?:find|search|fetch|get|select|show|display|list|count|delete|update|insert|remove)\s+(?:all\s+|the\s+|every\s+|top\s+\d+\s+)?(?:users?|orders?|products?|customers?|items?|rows?|records?|documents?|accounts?|entries)\b/i.test(userInput) || /\bhow\s+many\s+(?:users|orders|products|items|rows|records|documents|customers|accounts)\b/i.test(userInput);
2207
+ if (isObviousLiveQuery) {
2208
+ return {
2209
+ ...baseReset,
2210
+ requiresQuery: true
2211
+ };
2212
+ }
2213
+ const hasPreviousResults = Boolean(
2214
+ state.lastQueryResult && state.lastQueryResult.rows && state.lastQueryResult.rows.length > 0
2215
+ );
2216
+ const isOperatingOnPreviousResults = hasPreviousResults && /\b(?:previous|last|prior|above|earlier)\s+(?:results?|data|output|rows?)\b/i.test(userInput) && /\b(?:format|display|convert|export|show|summarize|explain|calculate|count|average|find|filter|sort|list)\b/i.test(userInput);
2217
+ if (isOperatingOnPreviousResults) {
2218
+ return {
2219
+ ...baseReset,
2220
+ requiresQuery: false
2221
+ };
2222
+ }
2223
+ const isSchemaOrMetaQuestion = /\b(?:schema|database\s+structure)\b/i.test(userInput) || /\b(?:explain|describe|what|tell\s+me\s+about)\b.*?\b(?:tables?|collections?|columns?|indexes?|foreign\s+keys?|schema)\b/i.test(userInput) || /\b(?:what\s+(?:tables|collections|columns)\s+(?:exist|are\s+there|do\s+we\s+have))\b/i.test(userInput) || /\b(?:what\s+was\s+the\s+(?:last|previous)\s+query|why\s+did\s+(?:the\s+last\s+query|it)\s+fail)\b/i.test(userInput);
2224
+ if (isSchemaOrMetaQuestion && (state.schemaSummary || state.messages.length > 0)) {
2225
+ return {
2226
+ ...baseReset,
2227
+ requiresQuery: false
2228
+ };
2229
+ }
2230
+ const historyMessages = state.messages.slice(-4);
2231
+ const prevQueryStr = state.lastGeneratedQuery?.sql || state.lastGeneratedQuery?.rawDisplay || "None";
2232
+ const prevRowsCount = state.lastQueryResult?.rowCount ?? state.lastQueryResult?.rows?.length ?? 0;
2233
+ const routerPrompt = `Determine whether fulfilling this user request requires generating and executing a LIVE database query (SQL or MongoDB) against the database, or if it should be answered directly without executing any query.
2234
+
2235
+ User Request: "${userInput}"
2236
+
2237
+ Recent Context:
2238
+ - Previous Query: ${prevQueryStr}
2239
+ - Previous Rows Returned: ${prevRowsCount}
2240
+ - Schema Summary Available: ${state.schemaSummary ? "Yes" : "No"}
2241
+
2242
+ Rules:
2243
+ 1. If the user explicitly asks NOT to run a query (e.g., "don't run query", "without querying", "do not execute queries"), requiresQuery MUST be false.
2244
+ 2. If the user asks to format, summarize, filter, inspect, or calculate based on the previous results or conversation context, requiresQuery MUST be false.
2245
+ 3. If the user asks about the schema structure, columns, or general knowledge answerable from context, requiresQuery MUST be false.
2246
+ 4. If the user asks to retrieve fresh records from a table/collection, count rows in the database, modify, insert, delete, or create database objects, requiresQuery MUST be true.
2247
+
2248
+ Respond with JSON only:
2249
+ {"requiresQuery": boolean, "reason": "<short explanation>"}`;
2250
+ try {
2251
+ const response = await model.invoke([
2252
+ ...historyMessages,
2253
+ new HumanMessage2(routerPrompt)
2254
+ ]);
2255
+ const content = typeof response.content === "string" ? response.content.trim() : "";
2256
+ const jsonMatch = content.match(/\{[\s\S]*?\}/);
2257
+ if (jsonMatch) {
2258
+ const parsed = JSON.parse(jsonMatch[0]);
2259
+ if (typeof parsed.requiresQuery === "boolean") {
2260
+ return {
2261
+ ...baseReset,
2262
+ requiresQuery: parsed.requiresQuery
2263
+ };
2264
+ }
2265
+ }
2266
+ return {
2267
+ ...baseReset,
2268
+ requiresQuery: true
2269
+ };
2270
+ } catch {
2271
+ return {
2272
+ ...baseReset,
2273
+ requiresQuery: true
2274
+ };
2275
+ }
2276
+ }
1816
2277
  async function identifyTargetsNode(state) {
1817
2278
  const historyMessages = state.messages.slice(-4);
1818
2279
  const prompt = `Based on this user request: "${state.userInput}"
@@ -1922,7 +2383,8 @@ Do not wrap with markdown or code fences.`;
1922
2383
  if (!state.generatedQuery || !state.safety) {
1923
2384
  return { confirmed: false, cancelReason: "Missing query or safety classification." };
1924
2385
  }
1925
- const result = await requestUserConfirmation(
2386
+ const confirmHandler = config.confirmFn || requestUserConfirmation;
2387
+ const result = await confirmHandler(
1926
2388
  state.generatedQuery,
1927
2389
  state.safety,
1928
2390
  state.affectedRowCount ?? null
@@ -1983,7 +2445,8 @@ Do not wrap with markdown or code fences.`;
1983
2445
  const conclusion = state.cancelReason || "Operation was not executed.";
1984
2446
  return {
1985
2447
  conclusion,
1986
- messages: [new HumanMessage2(state.userInput), new AIMessage(conclusion)]
2448
+ queryExecuted: false,
2449
+ messages: [...state.messages, new HumanMessage2(state.userInput), new AIMessage(conclusion)]
1987
2450
  };
1988
2451
  }
1989
2452
  const res = state.queryResult;
@@ -1991,14 +2454,18 @@ Do not wrap with markdown or code fences.`;
1991
2454
  const conclusion = "No query was executed.";
1992
2455
  return {
1993
2456
  conclusion,
1994
- messages: [new HumanMessage2(state.userInput), new AIMessage(conclusion)]
2457
+ queryExecuted: false,
2458
+ messages: [...state.messages, new HumanMessage2(state.userInput), new AIMessage(conclusion)]
1995
2459
  };
1996
2460
  }
1997
2461
  if (!res.success) {
1998
2462
  const conclusion = `Query failed with error: ${res.error} (took ${res.durationMs}ms)`;
1999
2463
  return {
2000
2464
  conclusion,
2001
- messages: [new HumanMessage2(state.userInput), new AIMessage(conclusion)]
2465
+ queryExecuted: true,
2466
+ lastQueryResult: res,
2467
+ lastGeneratedQuery: state.generatedQuery,
2468
+ messages: [...state.messages, new HumanMessage2(state.userInput), new AIMessage(conclusion)]
2002
2469
  };
2003
2470
  }
2004
2471
  const prompt = `User request: "${state.userInput}"
@@ -2023,7 +2490,11 @@ Ground your response strictly in the query results above. Never hallucinate rows
2023
2490
  ${conclusion}` : conclusion;
2024
2491
  return {
2025
2492
  conclusion,
2026
- messages: [new HumanMessage2(state.userInput), new AIMessage(historyAiText)]
2493
+ queryExecuted: true,
2494
+ lastQueryResult: res,
2495
+ lastGeneratedQuery: state.generatedQuery,
2496
+ lastStats: state.stats,
2497
+ messages: [...state.messages, new HumanMessage2(state.userInput), new AIMessage(historyAiText)]
2027
2498
  };
2028
2499
  } catch (err) {
2029
2500
  const conclusion = `Query executed successfully (${res.rowCount ?? 0} rows, ${res.durationMs}ms).`;
@@ -2032,16 +2503,88 @@ ${conclusion}` : conclusion;
2032
2503
  ${conclusion}` : conclusion;
2033
2504
  return {
2034
2505
  conclusion,
2035
- messages: [new HumanMessage2(state.userInput), new AIMessage(historyAiText)]
2506
+ queryExecuted: true,
2507
+ lastQueryResult: res,
2508
+ lastGeneratedQuery: state.generatedQuery,
2509
+ lastStats: state.stats,
2510
+ messages: [...state.messages, new HumanMessage2(state.userInput), new AIMessage(historyAiText)]
2511
+ };
2512
+ }
2513
+ }
2514
+ async function answerDirectNode(state) {
2515
+ const historyMessages = state.messages.slice(-6);
2516
+ let contextDetails = "";
2517
+ if (state.lastQueryResult && state.lastQueryResult.rows && state.lastQueryResult.rows.length > 0) {
2518
+ const queryStr = state.lastGeneratedQuery?.sql || state.lastGeneratedQuery?.rawDisplay || "N/A";
2519
+ contextDetails += `
2520
+ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
2521
+ `;
2522
+ contextDetails += `PREVIOUS QUERY EXECUTION DETAILS:
2523
+ `;
2524
+ contextDetails += `- Executed Query: ${queryStr}
2525
+ `;
2526
+ contextDetails += `- Rows returned: ${state.lastQueryResult.rowCount ?? state.lastQueryResult.rows.length}
2527
+ `;
2528
+ contextDetails += `- Rows data (sample up to 25 rows):
2529
+ ${JSON.stringify(state.lastQueryResult.rows.slice(0, 25), null, 2)}
2530
+ `;
2531
+ if (state.lastStats) {
2532
+ contextDetails += `- Computed statistics: ${JSON.stringify(state.lastStats, null, 2)}
2533
+ `;
2534
+ }
2535
+ contextDetails += `\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
2536
+ `;
2537
+ }
2538
+ const prompt = `You are "SANDAL", an expert database assistant.
2539
+ The user has provided a request that MUST be answered directly WITHOUT generating or executing any database query.
2540
+
2541
+ User Request: "${state.userInput}"
2542
+
2543
+ Database Schema:
2544
+ ${state.schemaSummary || "No schema loaded."}
2545
+ ${contextDetails}
2546
+
2547
+ Guidelines:
2548
+ 1. Provide a direct, helpful, and accurate natural-language response to the user's request.
2549
+ 2. If the user asked to do something with previous results (e.g. summarize, format, count, calculate, extract, sort), perform that operation using the PREVIOUS QUERY EXECUTION DETAILS above.
2550
+ 3. If the user asked to use previous results but NO previous results exist in the session, politely explain that no previous query results are available in this session.
2551
+ 4. DO NOT output, execute, or propose any new database queries.
2552
+ 5. Ground your answer strictly in the available schema, history, or previous query results. Never hallucinate data.`;
2553
+ try {
2554
+ const response = await model.invoke([
2555
+ ...historyMessages,
2556
+ new HumanMessage2(prompt)
2557
+ ]);
2558
+ const conclusion = typeof response.content === "string" ? response.content : JSON.stringify(response.content);
2559
+ return {
2560
+ conclusion,
2561
+ queryExecuted: false,
2562
+ generatedQuery: void 0,
2563
+ queryResult: void 0,
2564
+ messages: [...state.messages, new HumanMessage2(state.userInput), new AIMessage(conclusion)]
2565
+ };
2566
+ } catch (err) {
2567
+ const conclusion = `Unable to process request: ${err.message}`;
2568
+ return {
2569
+ conclusion,
2570
+ queryExecuted: false,
2571
+ generatedQuery: void 0,
2572
+ queryResult: void 0,
2573
+ messages: [...state.messages, new HumanMessage2(state.userInput), new AIMessage(conclusion)]
2036
2574
  };
2037
2575
  }
2038
2576
  }
2039
- const workflow = new StateGraph(AgentStateAnnotation).addNode("inspect_schema", inspectSchemaNode).addNode("identify_targets", identifyTargetsNode).addNode("generate_query", generateQueryNode).addNode("classify_safety", classifySafetyNode).addNode("request_confirmation", requestConfirmationNode).addNode("execute_query", executeQueryNode).addNode("analyze_results", analyzeResultsNode).addNode("produce_conclusion", produceConclusionNode).addEdge(START, "inspect_schema").addEdge("inspect_schema", "identify_targets").addEdge("identify_targets", "generate_query").addEdge("generate_query", "classify_safety").addEdge("classify_safety", "request_confirmation").addConditionalEdges("request_confirmation", (state) => {
2577
+ const workflow = new StateGraph(AgentStateAnnotation).addNode("inspect_schema", inspectSchemaNode).addNode("determine_action", determineActionNode).addNode("identify_targets", identifyTargetsNode).addNode("generate_query", generateQueryNode).addNode("classify_safety", classifySafetyNode).addNode("request_confirmation", requestConfirmationNode).addNode("execute_query", executeQueryNode).addNode("analyze_results", analyzeResultsNode).addNode("produce_conclusion", produceConclusionNode).addNode("answer_direct", answerDirectNode).addEdge(START, "inspect_schema").addEdge("inspect_schema", "determine_action").addConditionalEdges("determine_action", (state) => {
2578
+ if (state.requiresQuery) {
2579
+ return "identify_targets";
2580
+ }
2581
+ return "answer_direct";
2582
+ }).addEdge("identify_targets", "generate_query").addEdge("generate_query", "classify_safety").addEdge("classify_safety", "request_confirmation").addConditionalEdges("request_confirmation", (state) => {
2040
2583
  if (state.confirmed && !state.cancelReason) {
2041
2584
  return "execute_query";
2042
2585
  }
2043
2586
  return "produce_conclusion";
2044
- }).addEdge("execute_query", "analyze_results").addEdge("analyze_results", "produce_conclusion").addEdge("produce_conclusion", END);
2587
+ }).addEdge("execute_query", "analyze_results").addEdge("analyze_results", "produce_conclusion").addEdge("produce_conclusion", END).addEdge("answer_direct", END);
2045
2588
  const checkpointer = new MemorySaver();
2046
2589
  const app = workflow.compile({ checkpointer });
2047
2590
  return app;
@@ -2050,19 +2593,19 @@ ${conclusion}` : conclusion;
2050
2593
  // src/markdown.ts
2051
2594
  import { Marked } from "marked";
2052
2595
  import { markedTerminal } from "marked-terminal";
2053
- import chalk2 from "chalk";
2596
+ import chalk3 from "chalk";
2054
2597
  function createMarkdownRenderer(options = {}) {
2055
2598
  const terminalOptions = {
2056
- heading: chalk2.bold.cyan,
2057
- firstHeading: chalk2.bold.cyan.underline,
2058
- codespan: chalk2.yellow,
2059
- code: chalk2.yellow,
2060
- blockquote: chalk2.gray.italic,
2599
+ heading: chalk3.bold.cyan,
2600
+ firstHeading: chalk3.bold.cyan.underline,
2601
+ codespan: chalk3.yellow,
2602
+ code: chalk3.yellow,
2603
+ blockquote: chalk3.gray.italic,
2061
2604
  hr: (input3) => {
2062
2605
  const w = Math.min(options.width ?? (process.stdout.columns || 80), 80);
2063
- return chalk2.gray(input3 && input3.trim() ? input3 : "\u2500".repeat(w));
2606
+ return chalk3.gray(input3 && input3.trim() ? input3 : "\u2500".repeat(w));
2064
2607
  },
2065
- table: chalk2.reset,
2608
+ table: chalk3.reset,
2066
2609
  tableOptions: {
2067
2610
  style: {
2068
2611
  head: ["cyan", "bold"],
@@ -2248,6 +2791,10 @@ var ChatMemoryManager = class {
2248
2791
  if (m.query) {
2249
2792
  text = `[Executed Query: ${m.query}]
2250
2793
  ${text}`;
2794
+ }
2795
+ if (m.resultSample && m.resultSample.length > 0) {
2796
+ text += `
2797
+ [Previous Query Results Sample: ${JSON.stringify(m.resultSample.slice(0, 10))}]`;
2251
2798
  }
2252
2799
  result.push(new AIMessage2(text));
2253
2800
  }
@@ -2266,7 +2813,8 @@ ${text}`;
2266
2813
  return recent.map((m) => {
2267
2814
  const prefix = m.role === "user" ? "User:" : "Assistant:";
2268
2815
  const q = m.query ? ` (Query: ${m.query})` : "";
2269
- return `${prefix} ${m.content}${q}`;
2816
+ const rc = typeof m.rowCount === "number" ? ` (${m.rowCount} rows)` : "";
2817
+ return `${prefix} ${m.content}${q}${rc}`;
2270
2818
  }).join("\n");
2271
2819
  }
2272
2820
  /**
@@ -2312,6 +2860,8 @@ var ReplSession = class {
2312
2860
  agent;
2313
2861
  memoryManager;
2314
2862
  classifierOptions;
2863
+ rl;
2864
+ currentSpinner;
2315
2865
  constructor(options) {
2316
2866
  this.adapter = options.adapter;
2317
2867
  this.model = options.model;
@@ -2327,11 +2877,7 @@ var ReplSession = class {
2327
2877
  allowFullWipe: this.allowFullWipe,
2328
2878
  rowThresholdForDangerousUpdate: this.rowThreshold
2329
2879
  };
2330
- this.agent = createDatabaseAgent({
2331
- adapter: this.adapter,
2332
- model: this.model,
2333
- classifierOptions: this.classifierOptions
2334
- });
2880
+ this.initAgent();
2335
2881
  this.memoryManager = new ChatMemoryManager();
2336
2882
  const initialSession = this.memoryManager.createSession("New Chat", this.adapter.connectionUrl);
2337
2883
  this.sessionId = initialSession.id;
@@ -2339,21 +2885,55 @@ var ReplSession = class {
2339
2885
  addSavedConnection(this.adapter.connectionUrl);
2340
2886
  }
2341
2887
  }
2888
+ initAgent() {
2889
+ this.agent = createDatabaseAgent({
2890
+ adapter: this.adapter,
2891
+ model: this.model,
2892
+ classifierOptions: this.classifierOptions,
2893
+ confirmFn: async (query, safety, affectedRows) => {
2894
+ if (this.currentSpinner && this.currentSpinner.isSpinning) {
2895
+ this.currentSpinner.stop();
2896
+ }
2897
+ const confirmResult = await requestUserConfirmation(
2898
+ query,
2899
+ safety,
2900
+ affectedRows,
2901
+ this.askQuestion.bind(this)
2902
+ );
2903
+ if (confirmResult.confirmed && this.currentSpinner) {
2904
+ this.currentSpinner.text = chalk4.cyan("Executing query and analyzing...");
2905
+ this.currentSpinner.start();
2906
+ }
2907
+ return confirmResult;
2908
+ }
2909
+ });
2910
+ }
2911
+ askQuestion(query) {
2912
+ process.stdin.resume();
2913
+ if (!this.rl || this.rl.closed) {
2914
+ this.rl = readline2.createInterface({
2915
+ input: process.stdin,
2916
+ output: process.stdout,
2917
+ terminal: true
2918
+ });
2919
+ }
2920
+ return new Promise((resolve) => this.rl.question(query, resolve));
2921
+ }
2342
2922
  getPromptText() {
2343
- return chalk3.cyan(`sandal [${this.adapter.type}]> `);
2923
+ return chalk4.cyan(`sandal [${this.adapter.type}]> `);
2344
2924
  }
2345
2925
  async start() {
2346
2926
  this.setupSignalHandlers();
2347
2927
  this.printWelcomeBanner();
2348
- const rl = readline.createInterface({
2928
+ process.stdin.resume();
2929
+ this.rl = readline2.createInterface({
2349
2930
  input: process.stdin,
2350
2931
  output: process.stdout,
2351
2932
  terminal: true
2352
2933
  });
2353
- const askQuestion = (query) => {
2354
- return new Promise((resolve) => rl.question(query, resolve));
2355
- };
2934
+ const askQuestion = this.askQuestion.bind(this);
2356
2935
  while (this.isRunning) {
2936
+ process.stdin.resume();
2357
2937
  const input3 = await askQuestion(this.getPromptText());
2358
2938
  const trimmed = input3.trim();
2359
2939
  if (!trimmed) continue;
@@ -2378,12 +2958,12 @@ var ReplSession = class {
2378
2958
  continue;
2379
2959
  }
2380
2960
  if (trimmed === ".refresh") {
2381
- const spinner = ora(chalk3.blue("Refreshing schema cache...")).start();
2961
+ const spinner = ora(chalk4.blue("Refreshing schema cache...")).start();
2382
2962
  try {
2383
2963
  await this.adapter.inspectSchema(true);
2384
- spinner.succeed(chalk3.green("Schema refreshed successfully."));
2964
+ spinner.succeed(chalk4.green("Schema refreshed successfully."));
2385
2965
  } catch (err) {
2386
- spinner.fail(chalk3.red(`Failed to refresh schema: ${err.message}`));
2966
+ spinner.fail(chalk4.red(`Failed to refresh schema: ${err.message}`));
2387
2967
  }
2388
2968
  continue;
2389
2969
  }
@@ -2437,18 +3017,18 @@ var ReplSession = class {
2437
3017
  this.handleClearChat();
2438
3018
  continue;
2439
3019
  }
2440
- const intentSpinner = ora(chalk3.blue("Analyzing intent...")).start();
3020
+ const intentSpinner = ora(chalk4.blue("Analyzing intent...")).start();
2441
3021
  const historySummary = this.memoryManager.getRecentSummary(this.sessionId, 4);
2442
3022
  const intent = await classifyIntent(trimmed, this.model, historySummary);
2443
3023
  intentSpinner.stop();
2444
3024
  if (!intent.isDatabaseTask) {
2445
3025
  console.log(
2446
3026
  boxen2(
2447
- `${chalk3.bold.red("\u{1F6E1}\uFE0F GUARDRAIL ENFORCEMENT")}
3027
+ `${chalk4.bold.red("\u{1F6E1}\uFE0F GUARDRAIL ENFORCEMENT")}
2448
3028
 
2449
- ${chalk3.yellow(REFUSAL_MESSAGE)}
3029
+ ${chalk4.yellow(REFUSAL_MESSAGE)}
2450
3030
 
2451
- ${chalk3.gray(
3031
+ ${chalk4.gray(
2452
3032
  `Reason: ${intent.reason || "Non-database request rejected"}`
2453
3033
  )}`,
2454
3034
  {
@@ -2461,7 +3041,8 @@ ${chalk3.gray(
2461
3041
  );
2462
3042
  continue;
2463
3043
  }
2464
- const agentSpinner = ora(chalk3.cyan("Agent planning query...")).start();
3044
+ const agentSpinner = ora(chalk4.cyan("Agent thinking...")).start();
3045
+ this.currentSpinner = agentSpinner;
2465
3046
  try {
2466
3047
  const historyMessages = this.memoryManager.toLangChainMessages(this.sessionId, 6);
2467
3048
  const result = await this.agent.invoke(
@@ -2475,16 +3056,18 @@ ${chalk3.gray(
2475
3056
  }
2476
3057
  }
2477
3058
  );
2478
- agentSpinner.stop();
2479
- if (result.queryResult && result.queryResult.success && result.queryResult.rows) {
3059
+ if (agentSpinner.isSpinning) {
3060
+ agentSpinner.stop();
3061
+ }
3062
+ if (result.queryExecuted && result.queryResult && result.queryResult.success && result.queryResult.rows) {
2480
3063
  this.renderRowsTable(result.queryResult);
2481
3064
  }
2482
3065
  if (result.conclusion) {
2483
- console.log("\n" + chalk3.green("\u{1F916} Answer:"));
3066
+ console.log("\n" + chalk4.green("\u{1F916} Answer:"));
2484
3067
  if (this.renderMarkdown) {
2485
3068
  console.log(renderMarkdown(result.conclusion) + "\n");
2486
3069
  } else {
2487
- console.log(chalk3.white(result.conclusion) + "\n");
3070
+ console.log(chalk4.white(result.conclusion) + "\n");
2488
3071
  }
2489
3072
  }
2490
3073
  this.memoryManager.addMessage(
@@ -2493,47 +3076,62 @@ ${chalk3.gray(
2493
3076
  this.adapter.connectionUrl
2494
3077
  );
2495
3078
  if (result.conclusion) {
2496
- const queryStr = result.generatedQuery?.sql || result.generatedQuery?.rawDisplay;
3079
+ const queryStr = result.queryExecuted ? result.generatedQuery?.sql || result.generatedQuery?.rawDisplay : void 0;
2497
3080
  this.memoryManager.addMessage(
2498
3081
  this.sessionId,
2499
3082
  {
2500
3083
  role: "assistant",
2501
3084
  content: result.conclusion,
2502
3085
  query: queryStr,
2503
- rowCount: result.queryResult?.rowCount
3086
+ rowCount: result.queryExecuted ? result.queryResult?.rowCount : void 0,
3087
+ resultSample: result.queryExecuted ? result.queryResult?.rows?.slice(0, 15) : void 0
2504
3088
  },
2505
3089
  this.adapter.connectionUrl
2506
3090
  );
2507
3091
  }
2508
3092
  } catch (err) {
2509
- agentSpinner.fail(chalk3.red(`Execution failed: ${err.message}`));
3093
+ if (agentSpinner.isSpinning) {
3094
+ agentSpinner.fail(chalk4.red(`Execution failed: ${err.message}`));
3095
+ } else {
3096
+ console.error(chalk4.red(`
3097
+ Execution failed: ${err.message}
3098
+ `));
3099
+ }
3100
+ } finally {
3101
+ if (agentSpinner.isSpinning) {
3102
+ agentSpinner.stop();
3103
+ }
3104
+ this.currentSpinner = void 0;
3105
+ process.stdin.resume();
2510
3106
  }
2511
3107
  }
2512
- rl.close();
3108
+ if (this.rl && !this.rl.closed) {
3109
+ this.rl.close();
3110
+ }
2513
3111
  await this.shutdown();
2514
3112
  }
2515
3113
  async handleSwitchConnection(targetUrl, ask) {
2516
3114
  let urlToConnect = targetUrl;
2517
3115
  if (!urlToConnect) {
2518
3116
  const saved = getSavedConnections();
2519
- console.log(chalk3.bold.cyan("\nDatabase Connections:"));
3117
+ console.log(chalk4.bold.cyan("\nDatabase Connections:"));
2520
3118
  if (saved.length === 0) {
2521
- console.log(chalk3.gray(" (No previous connections saved)"));
3119
+ console.log(chalk4.gray(" (No previous connections saved)"));
2522
3120
  } else {
2523
3121
  saved.forEach((url, i) => {
2524
3122
  const isCurrent = url === this.adapter.connectionUrl;
2525
- const currentTag = isCurrent ? chalk3.green(" (Current)") : "";
3123
+ const currentTag = isCurrent ? chalk4.green(" (Current)") : "";
2526
3124
  console.log(` [${i + 1}] ${maskUrl(url)}${currentTag}`);
2527
3125
  });
2528
3126
  }
2529
- console.log(chalk3.gray(" [N] Enter a new connection URL"));
2530
- console.log(chalk3.gray(" [C] Cancel\n"));
2531
- const choice = (await ask(chalk3.cyan("Select a connection [number, N, C]: "))).trim();
3127
+ console.log(chalk4.gray(" [N] Enter a new connection URL"));
3128
+ console.log(chalk4.gray(" [C] Cancel\n"));
3129
+ const choice = (await ask(chalk4.cyan("Select a connection [number, N, C]: "))).trim();
2532
3130
  if (!choice || choice.toLowerCase() === "c") {
2533
3131
  return;
2534
3132
  }
2535
3133
  if (choice.toLowerCase() === "n") {
2536
- const entered = (await ask(chalk3.cyan("Enter database connection URL: "))).trim();
3134
+ const entered = (await ask(chalk4.cyan("Enter database connection URL: "))).trim();
2537
3135
  if (!entered) return;
2538
3136
  urlToConnect = entered;
2539
3137
  } else {
@@ -2541,19 +3139,19 @@ ${chalk3.gray(
2541
3139
  if (!isNaN(num) && num >= 1 && num <= saved.length) {
2542
3140
  urlToConnect = saved[num - 1];
2543
3141
  } else {
2544
- console.log(chalk3.red("Invalid selection."));
3142
+ console.log(chalk4.red("Invalid selection."));
2545
3143
  return;
2546
3144
  }
2547
3145
  }
2548
3146
  }
2549
3147
  if (urlToConnect === this.adapter.connectionUrl && this.adapter.isConnected()) {
2550
- console.log(chalk3.yellow("\nAlready connected to this database.\n"));
3148
+ console.log(chalk4.yellow("\nAlready connected to this database.\n"));
2551
3149
  return;
2552
3150
  }
2553
3151
  try {
2554
3152
  detectDatabaseType(urlToConnect);
2555
3153
  } catch (e) {
2556
- console.log(chalk3.red(`Invalid connection URL: ${e.message}`));
3154
+ console.log(chalk4.red(`Invalid connection URL: ${e.message}`));
2557
3155
  return;
2558
3156
  }
2559
3157
  const spinner = ora(`Connecting to ${maskUrl(urlToConnect)}...`).start();
@@ -2566,58 +3164,53 @@ ${chalk3.gray(
2566
3164
  } catch {
2567
3165
  }
2568
3166
  this.adapter = newAdapter;
2569
- this.agent = createDatabaseAgent({
2570
- adapter: this.adapter,
2571
- model: this.model,
2572
- classifierOptions: this.classifierOptions
2573
- });
3167
+ this.initAgent();
2574
3168
  addSavedConnection(urlToConnect);
2575
3169
  spinner.succeed(
2576
- chalk3.green(
3170
+ chalk4.green(
2577
3171
  `Switched to ${newAdapter.type.toUpperCase()} database: ${newAdapter.databaseName} (${newAdapter.getMaskedUrl()})`
2578
3172
  )
2579
3173
  );
2580
3174
  console.log(
2581
- chalk3.gray(
3175
+ chalk4.gray(
2582
3176
  `Chat session [${this.sessionId}] continuing with conversation memory on the new database.
2583
3177
  `
2584
3178
  )
2585
3179
  );
2586
3180
  } catch (err) {
2587
- spinner.fail(chalk3.red(`Failed to connect to database: ${err.message}`));
3181
+ spinner.fail(chalk4.red(`Failed to connect to database: ${err.message}`));
2588
3182
  }
2589
3183
  }
2590
3184
  async handleModelCommand(newModel, ask) {
2591
3185
  let targetModel = newModel;
2592
3186
  if (!targetModel) {
2593
3187
  console.log(
2594
- chalk3.bold.cyan(`
2595
- Current Model: `) + chalk3.white(this.modelName) + chalk3.gray(` (${this.provider})`)
3188
+ chalk4.bold.cyan(`
3189
+ Current Model: `) + chalk4.white(this.modelName) + chalk4.gray(` (${this.provider})`)
2596
3190
  );
2597
- console.log(chalk3.bold.yellow("Suggested models for " + this.provider + ":"));
2598
- const commonModels = {
2599
- google: ["gemini-2.5-flash", "gemini-2.5-pro", "gemini-2.0-flash"],
2600
- openai: ["gpt-4o", "gpt-4o-mini", "o3-mini"],
2601
- anthropic: [
2602
- "claude-3-5-sonnet-latest",
2603
- "claude-3-5-haiku-latest",
2604
- "claude-3-7-sonnet-latest"
2605
- ]
2606
- };
2607
- const list = commonModels[this.provider] || [];
3191
+ console.log(chalk4.bold.yellow(`
3192
+ Available models for ${this.provider.toUpperCase()}:`));
3193
+ const list = getModelsForProvider(this.provider);
2608
3194
  list.forEach((m, i) => {
2609
- const isCurrent = m === this.modelName ? chalk3.green(" (Current)") : "";
2610
- console.log(` [${i + 1}] ${m}${isCurrent}`);
3195
+ const isCurrent = m.id === this.modelName ? chalk4.green(" (Current)") : "";
3196
+ const rec = m.recommended ? chalk4.yellow(" [\u2605 Recommended]") : "";
3197
+ const badge = m.badge ? chalk4.dim(` [${m.badge}]`) : "";
3198
+ console.log(
3199
+ ` [${i + 1}] ${chalk4.bold(m.id)}${rec}${badge}${isCurrent}
3200
+ ${chalk4.gray(
3201
+ `${m.name} (${m.contextWindow}) \u2014 ${m.description}`
3202
+ )}`
3203
+ );
2611
3204
  });
2612
- console.log(chalk3.gray(" [O] Other (type custom model name)"));
2613
- console.log(chalk3.gray(" [C] Cancel\n"));
2614
- const input3 = (await ask(chalk3.cyan("Select model [number, name, C]: "))).trim();
3205
+ console.log(chalk4.gray(" [O] Other (type custom model name)"));
3206
+ console.log(chalk4.gray(" [C] Cancel\n"));
3207
+ const input3 = (await ask(chalk4.cyan("Select model [number, name, C]: "))).trim();
2615
3208
  if (!input3 || input3.toLowerCase() === "c") return;
2616
3209
  const num = parseInt(input3, 10);
2617
3210
  if (!isNaN(num) && num >= 1 && num <= list.length) {
2618
- targetModel = list[num - 1];
3211
+ targetModel = list[num - 1].id;
2619
3212
  } else if (input3.toLowerCase() === "o") {
2620
- const custom = (await ask(chalk3.cyan("Enter model name: "))).trim();
3213
+ const custom = (await ask(chalk4.cyan("Enter model name: "))).trim();
2621
3214
  if (!custom) return;
2622
3215
  targetModel = custom;
2623
3216
  } else {
@@ -2629,11 +3222,11 @@ Current Model: `) + chalk3.white(this.modelName) + chalk3.gray(` (${this.provide
2629
3222
  if (resolved.apiKey) {
2630
3223
  this.apiKey = resolved.apiKey;
2631
3224
  } else {
2632
- console.log(chalk3.yellow(`
3225
+ console.log(chalk4.yellow(`
2633
3226
  No API key found for ${this.provider.toUpperCase()}.`));
2634
- this.apiKey = (await ask(chalk3.cyan(`Enter API key for ${this.provider.toUpperCase()}: `))).trim();
3227
+ this.apiKey = (await ask(chalk4.cyan(`Enter API key for ${this.provider.toUpperCase()}: `))).trim();
2635
3228
  if (!this.apiKey) {
2636
- console.log(chalk3.red("API key is required."));
3229
+ console.log(chalk4.red("API key is required."));
2637
3230
  return;
2638
3231
  }
2639
3232
  }
@@ -2645,92 +3238,129 @@ No API key found for ${this.provider.toUpperCase()}.`));
2645
3238
  apiKey: this.apiKey
2646
3239
  });
2647
3240
  this.modelName = targetModel;
2648
- this.agent = createDatabaseAgent({
2649
- adapter: this.adapter,
2650
- model: this.model,
2651
- classifierOptions: this.classifierOptions
2652
- });
2653
- console.log(chalk3.green(`
3241
+ this.initAgent();
3242
+ console.log(chalk4.green(`
2654
3243
  Updated active model to: ${targetModel} [${this.provider}]
2655
3244
  `));
2656
3245
  } catch (err) {
2657
- console.log(chalk3.red(`
3246
+ console.log(chalk4.red(`
2658
3247
  Failed to update model: ${err.message}
2659
3248
  `));
2660
3249
  }
2661
3250
  }
2662
3251
  async handleProviderCommand(newProvider, ask) {
2663
- let p = newProvider.toLowerCase();
3252
+ let p = newProvider.toLowerCase().trim();
2664
3253
  if (!["google", "openai", "anthropic"].includes(p)) {
2665
- console.log(chalk3.bold.cyan(`
2666
- Current Provider: `) + chalk3.blue(this.provider));
3254
+ console.log(chalk4.bold.cyan(`
3255
+ Current Provider: `) + chalk4.blue(this.provider));
2667
3256
  console.log("Available providers:");
2668
3257
  console.log(" [1] Google Gemini (google)");
2669
3258
  console.log(" [2] OpenAI (openai)");
2670
3259
  console.log(" [3] Anthropic (anthropic)");
2671
3260
  console.log(" [C] Cancel\n");
2672
- const ans = (await ask(chalk3.cyan("Select provider [1-3, name, C]: "))).trim().toLowerCase();
3261
+ const ans = (await ask(chalk4.cyan("Select provider [1-3, name, C]: "))).trim().toLowerCase();
2673
3262
  if (!ans || ans === "c") return;
2674
3263
  if (ans === "1" || ans === "google") p = "google";
2675
3264
  else if (ans === "2" || ans === "openai") p = "openai";
2676
3265
  else if (ans === "3" || ans === "anthropic") p = "anthropic";
2677
3266
  else {
2678
- console.log(chalk3.red("Invalid provider."));
3267
+ console.log(chalk4.red("Invalid provider."));
2679
3268
  return;
2680
3269
  }
2681
3270
  }
2682
- const resolved = resolveCredentials({ cliProvider: p });
2683
- let key = resolved.apiKey;
2684
- if (!key) {
2685
- console.log(chalk3.yellow(`
2686
- No API key found for ${p.toUpperCase()}.`));
2687
- key = (await ask(chalk3.cyan(`Enter API key for ${p.toUpperCase()}: `))).trim();
2688
- if (!key) {
2689
- console.log(chalk3.red("API key cannot be empty."));
2690
- return;
3271
+ const typedProvider = p;
3272
+ let key = getStoredApiKeyForProvider(typedProvider);
3273
+ if (key) {
3274
+ console.log(
3275
+ chalk4.gray(`Found stored API key for ${typedProvider.toUpperCase()} (${maskApiKey(key)})`)
3276
+ );
3277
+ } else {
3278
+ console.log(chalk4.yellow(`
3279
+ No stored API key found for ${typedProvider.toUpperCase()}.`));
3280
+ key = (await ask(chalk4.cyan(`Enter API key for ${typedProvider.toUpperCase()}: `))).trim();
3281
+ while (!key) {
3282
+ console.log(chalk4.red("API key cannot be empty."));
3283
+ key = (await ask(chalk4.cyan(`Enter API key for ${typedProvider.toUpperCase()} (or C to cancel): `))).trim();
3284
+ if (key.toLowerCase() === "c") return;
2691
3285
  }
2692
- const save = (await ask(chalk3.cyan("Save this API key to ~/.sandal/config.json? (Y/n): "))).trim().toLowerCase();
3286
+ const save = (await ask(chalk4.cyan("Save this API key to ~/.sandal/config.json? (Y/n): "))).trim().toLowerCase();
2693
3287
  if (save !== "n") {
2694
- if (p === "google") writeConfig({ geminiApiKey: key, defaultProvider: "google" });
2695
- else if (p === "openai") writeConfig({ openaiApiKey: key, defaultProvider: "openai" });
2696
- else if (p === "anthropic") writeConfig({ anthropicApiKey: key, defaultProvider: "anthropic" });
3288
+ if (typedProvider === "google") writeConfig({ geminiApiKey: key });
3289
+ else if (typedProvider === "openai") writeConfig({ openaiApiKey: key });
3290
+ else if (typedProvider === "anthropic") writeConfig({ anthropicApiKey: key });
2697
3291
  }
2698
3292
  }
2699
- const defaultModel = getDefaultModelForProvider(p);
3293
+ const providerModels = getModelsForProvider(typedProvider);
3294
+ console.log(chalk4.bold.yellow(`
3295
+ Available models for ${typedProvider.toUpperCase()}:`));
3296
+ providerModels.forEach((m, i) => {
3297
+ const isCurrent = typedProvider === this.provider && m.id === this.modelName ? chalk4.green(" (Current)") : "";
3298
+ const rec = m.recommended ? chalk4.yellow(" [\u2605 Recommended]") : "";
3299
+ const badge = m.badge ? chalk4.dim(` [${m.badge}]`) : "";
3300
+ console.log(
3301
+ ` [${i + 1}] ${chalk4.bold(m.id)}${rec}${badge}${isCurrent}
3302
+ ${chalk4.gray(
3303
+ `${m.name} (${m.contextWindow}) \u2014 ${m.description}`
3304
+ )}`
3305
+ );
3306
+ });
3307
+ const defaultModel = getDefaultModelForProvider(typedProvider);
3308
+ console.log(chalk4.gray(` [Enter] Default (${defaultModel})`));
3309
+ console.log(chalk4.gray(" [O] Other (type custom model name)"));
3310
+ console.log(chalk4.gray(" [C] Cancel\n"));
3311
+ const choice = (await ask(chalk4.cyan(`Select model [1-${providerModels.length}, name, Enter]: `))).trim();
3312
+ if (choice.toLowerCase() === "c") return;
3313
+ let targetModel = defaultModel;
3314
+ const mNum = parseInt(choice, 10);
3315
+ if (!isNaN(mNum) && mNum >= 1 && mNum <= providerModels.length) {
3316
+ targetModel = providerModels[mNum - 1].id;
3317
+ } else if (choice.toLowerCase() === "o") {
3318
+ const custom = (await ask(chalk4.cyan("Enter custom model name: "))).trim();
3319
+ if (!custom) return;
3320
+ targetModel = custom;
3321
+ } else if (choice) {
3322
+ targetModel = choice;
3323
+ }
3324
+ const saveDefault = (await ask(chalk4.cyan(`Save ${typedProvider.toUpperCase()} and ${targetModel} as your default in ~/.sandal/config.json? (y/N): `))).trim().toLowerCase();
3325
+ if (saveDefault === "y" || saveDefault === "yes") {
3326
+ writeConfig({
3327
+ defaultProvider: typedProvider,
3328
+ defaultModel: targetModel
3329
+ });
3330
+ console.log(chalk4.green("Saved default provider and model to ~/.sandal/config.json."));
3331
+ }
2700
3332
  try {
2701
3333
  this.model = createChatModel({
2702
- provider: p,
2703
- model: defaultModel,
3334
+ provider: typedProvider,
3335
+ model: targetModel,
2704
3336
  apiKey: key
2705
3337
  });
2706
- this.provider = p;
2707
- this.modelName = defaultModel;
3338
+ this.provider = typedProvider;
3339
+ this.modelName = targetModel;
2708
3340
  this.apiKey = key;
2709
- this.agent = createDatabaseAgent({
2710
- adapter: this.adapter,
2711
- model: this.model,
2712
- classifierOptions: this.classifierOptions
2713
- });
3341
+ this.initAgent();
2714
3342
  console.log(
2715
- chalk3.green(`
2716
- Switched provider to ${p.toUpperCase()} with model ${defaultModel}.
2717
- `)
3343
+ chalk4.green(
3344
+ `
3345
+ Switched provider to ${chalk4.bold(typedProvider.toUpperCase())} with model ${chalk4.bold(targetModel)}.
3346
+ `
3347
+ )
2718
3348
  );
2719
3349
  } catch (err) {
2720
- console.log(chalk3.red(`Failed to switch provider: ${err.message}
3350
+ console.log(chalk4.red(`Failed to switch provider: ${err.message}
2721
3351
  `));
2722
3352
  }
2723
3353
  }
2724
3354
  async handleKeyCommand(arg, ask) {
2725
3355
  const sub = arg.toLowerCase().trim();
2726
3356
  if (sub === "remove" || sub === "clear" || sub === "delete") {
2727
- const confirmAns = (await ask(chalk3.yellow(`Remove stored API key for provider "${this.provider}"? (y/N): `))).trim().toLowerCase();
3357
+ const confirmAns = (await ask(chalk4.yellow(`Remove stored API key for provider "${this.provider}"? (y/N): `))).trim().toLowerCase();
2728
3358
  if (confirmAns === "y" || confirmAns === "yes") {
2729
3359
  removeApiKey(this.provider);
2730
3360
  this.apiKey = void 0;
2731
- console.log(chalk3.green(`
3361
+ console.log(chalk4.green(`
2732
3362
  Removed stored API key for "${this.provider}" from ~/.sandal/config.json.`));
2733
- console.log(chalk3.gray(`Subsequent requests with this provider will prompt for a key.
3363
+ console.log(chalk4.gray(`Subsequent requests with this provider will prompt for a key.
2734
3364
  `));
2735
3365
  }
2736
3366
  return;
@@ -2738,10 +3368,10 @@ Removed stored API key for "${this.provider}" from ~/.sandal/config.json.`));
2738
3368
  if (sub.startsWith("set")) {
2739
3369
  let newKey = sub.replace(/^set\s*/, "").trim();
2740
3370
  if (!newKey) {
2741
- newKey = (await ask(chalk3.cyan(`Enter new API key for ${this.provider}: `))).trim();
3371
+ newKey = (await ask(chalk4.cyan(`Enter new API key for ${this.provider}: `))).trim();
2742
3372
  }
2743
3373
  if (!newKey) {
2744
- console.log(chalk3.red("API key cannot be empty."));
3374
+ console.log(chalk4.red("API key cannot be empty."));
2745
3375
  return;
2746
3376
  }
2747
3377
  this.apiKey = newKey;
@@ -2753,55 +3383,47 @@ Removed stored API key for "${this.provider}" from ~/.sandal/config.json.`));
2753
3383
  model: this.modelName,
2754
3384
  apiKey: newKey
2755
3385
  });
2756
- this.agent = createDatabaseAgent({
2757
- adapter: this.adapter,
2758
- model: this.model,
2759
- classifierOptions: this.classifierOptions
2760
- });
2761
- console.log(chalk3.green(`
3386
+ this.initAgent();
3387
+ console.log(chalk4.green(`
2762
3388
  Updated API key for "${this.provider}" and refreshed model.
2763
3389
  `));
2764
3390
  return;
2765
3391
  }
2766
- console.log(chalk3.bold.cyan("\nAPI Key Status:"));
2767
- console.log(` Provider: ${chalk3.blue(this.provider)}`);
2768
- console.log(` Model: ${chalk3.white(this.modelName)}`);
3392
+ console.log(chalk4.bold.cyan("\nAPI Key Status:"));
3393
+ console.log(` Provider: ${chalk4.blue(this.provider)}`);
3394
+ console.log(` Model: ${chalk4.white(this.modelName)}`);
2769
3395
  console.log(
2770
- ` Active Key: ${this.apiKey ? chalk3.yellow(maskApiKey(this.apiKey)) : chalk3.red("None (not set)")}`
3396
+ ` Active Key: ${this.apiKey ? chalk4.yellow(maskApiKey(this.apiKey)) : chalk4.red("None (not set)")}`
2771
3397
  );
2772
- console.log(chalk3.gray("\nUsage:"));
2773
- console.log(chalk3.gray(" .key remove - Remove stored key from ~/.sandal/config.json"));
2774
- console.log(chalk3.gray(" .key set <api-key> - Update API key on the fly\n"));
3398
+ console.log(chalk4.gray("\nUsage:"));
3399
+ console.log(chalk4.gray(" .key remove - Remove stored key from ~/.sandal/config.json"));
3400
+ console.log(chalk4.gray(" .key set <api-key> - Update API key on the fly\n"));
2775
3401
  }
2776
3402
  handleListChats() {
2777
3403
  const list = this.memoryManager.listSessions();
2778
3404
  if (list.length === 0) {
2779
- console.log(chalk3.yellow("\nNo saved chat sessions found.\n"));
3405
+ console.log(chalk4.yellow("\nNo saved chat sessions found.\n"));
2780
3406
  return;
2781
3407
  }
2782
- console.log(chalk3.bold.cyan("\nSaved Chat Sessions:"));
3408
+ console.log(chalk4.bold.cyan("\nSaved Chat Sessions:"));
2783
3409
  list.forEach((s, i) => {
2784
3410
  const isActive = s.id === this.sessionId;
2785
- const activeMarker = isActive ? chalk3.green(" [ACTIVE]") : "";
3411
+ const activeMarker = isActive ? chalk4.green(" [ACTIVE]") : "";
2786
3412
  const dateStr = new Date(s.updatedAt).toLocaleString();
2787
3413
  console.log(
2788
- ` [${i + 1}] ${chalk3.bold(s.title)}${activeMarker}
2789
- ID: ${chalk3.gray(s.id)} | ${chalk3.yellow(s.messageCount + " messages")} | ${chalk3.gray(dateStr)}`
3414
+ ` [${i + 1}] ${chalk4.bold(s.title)}${activeMarker}
3415
+ ID: ${chalk4.gray(s.id)} | ${chalk4.yellow(s.messageCount + " messages")} | ${chalk4.gray(dateStr)}`
2790
3416
  );
2791
3417
  });
2792
- console.log(chalk3.gray("\nCommands: .chat <id|num> to switch, .new to start fresh chat\n"));
3418
+ console.log(chalk4.gray("\nCommands: .chat <id|num> to switch, .new to start fresh chat\n"));
2793
3419
  }
2794
3420
  handleNewChat() {
2795
3421
  const newSession = this.memoryManager.createSession("New Chat", this.adapter.connectionUrl);
2796
3422
  this.sessionId = newSession.id;
2797
- this.agent = createDatabaseAgent({
2798
- adapter: this.adapter,
2799
- model: this.model,
2800
- classifierOptions: this.classifierOptions
2801
- });
2802
- console.log(chalk3.green(`
3423
+ this.initAgent();
3424
+ console.log(chalk4.green(`
2803
3425
  Started fresh chat session: ${this.sessionId}`));
2804
- console.log(chalk3.gray("Chat memory is clean for this new session.\n"));
3426
+ console.log(chalk4.gray("Chat memory is clean for this new session.\n"));
2805
3427
  }
2806
3428
  handleSwitchChat(arg) {
2807
3429
  const list = this.memoryManager.listSessions();
@@ -2816,26 +3438,22 @@ Started fresh chat session: ${this.sessionId}`));
2816
3438
  if (found) targetId = found.id;
2817
3439
  }
2818
3440
  if (!targetId) {
2819
- console.log(chalk3.red(`
3441
+ console.log(chalk4.red(`
2820
3442
  Chat session "${arg}" not found. Use .chats to list sessions.
2821
3443
  `));
2822
3444
  return;
2823
3445
  }
2824
3446
  const session = this.memoryManager.getSession(targetId);
2825
3447
  if (!session) {
2826
- console.log(chalk3.red(`
3448
+ console.log(chalk4.red(`
2827
3449
  Could not load chat session "${targetId}".
2828
3450
  `));
2829
3451
  return;
2830
3452
  }
2831
3453
  this.sessionId = session.id;
2832
- this.agent = createDatabaseAgent({
2833
- adapter: this.adapter,
2834
- model: this.model,
2835
- classifierOptions: this.classifierOptions
2836
- });
3454
+ this.initAgent();
2837
3455
  console.log(
2838
- chalk3.green(
3456
+ chalk4.green(
2839
3457
  `
2840
3458
  Switched to chat: "${session.title}" (${session.messages.length} messages in memory).
2841
3459
  `
@@ -2845,46 +3463,42 @@ Switched to chat: "${session.title}" (${session.messages.length} messages in mem
2845
3463
  handleShowHistory() {
2846
3464
  const session = this.memoryManager.getSession(this.sessionId);
2847
3465
  if (!session || session.messages.length === 0) {
2848
- console.log(chalk3.yellow(`
3466
+ console.log(chalk4.yellow(`
2849
3467
  No message history in current chat (${this.sessionId}).
2850
3468
  `));
2851
3469
  return;
2852
3470
  }
2853
- console.log(chalk3.bold.cyan(`
3471
+ console.log(chalk4.bold.cyan(`
2854
3472
  Chat History: "${session.title}" [${this.sessionId}]:`));
2855
- console.log(chalk3.gray("\u2500".repeat(60)));
3473
+ console.log(chalk4.gray("\u2500".repeat(60)));
2856
3474
  for (const msg of session.messages) {
2857
3475
  const time = new Date(msg.timestamp).toLocaleTimeString();
2858
3476
  if (msg.role === "user") {
2859
- console.log(`${chalk3.bold.blue("\u{1F464} User")} ${chalk3.gray(`(${time})`)}:
3477
+ console.log(`${chalk4.bold.blue("\u{1F464} User")} ${chalk4.gray(`(${time})`)}:
2860
3478
  ${msg.content}`);
2861
3479
  } else if (msg.role === "assistant") {
2862
3480
  if (msg.query) {
2863
- console.log(` ${chalk3.gray(`[Query: ${msg.query}]`)}`);
3481
+ console.log(` ${chalk4.gray(`[Query: ${msg.query}]`)}`);
2864
3482
  }
2865
- console.log(`${chalk3.bold.green("\u{1F916} Assistant")} ${chalk3.gray(`(${time})`)}:
3483
+ console.log(`${chalk4.bold.green("\u{1F916} Assistant")} ${chalk4.gray(`(${time})`)}:
2866
3484
  ${msg.content}`);
2867
3485
  }
2868
- console.log(chalk3.gray("\u2500".repeat(60)));
3486
+ console.log(chalk4.gray("\u2500".repeat(60)));
2869
3487
  }
2870
3488
  console.log("");
2871
3489
  }
2872
3490
  handleClearChat() {
2873
3491
  this.memoryManager.clearSession(this.sessionId);
2874
- this.agent = createDatabaseAgent({
2875
- adapter: this.adapter,
2876
- model: this.model,
2877
- classifierOptions: this.classifierOptions
2878
- });
2879
- console.log(chalk3.green(`
3492
+ this.initAgent();
3493
+ console.log(chalk4.green(`
2880
3494
  Chat memory for session [${this.sessionId}] has been cleared.
2881
3495
  `));
2882
3496
  }
2883
3497
  handleRenderMarkdown(arg) {
2884
3498
  if (!arg) {
2885
- console.log(chalk3.yellow("Usage: .md <markdown text or file path>"));
2886
- console.log(chalk3.gray("Example: .md # Hello World"));
2887
- console.log(chalk3.gray("Example: .md ./README.md"));
3499
+ console.log(chalk4.yellow("Usage: .md <markdown text or file path>"));
3500
+ console.log(chalk4.gray("Example: .md # Hello World"));
3501
+ console.log(chalk4.gray("Example: .md ./README.md"));
2888
3502
  return;
2889
3503
  }
2890
3504
  try {
@@ -2900,21 +3514,21 @@ Chat memory for session [${this.sessionId}] has been cleared.
2900
3514
  renderRowsTable(queryResult) {
2901
3515
  const rows = queryResult.rows;
2902
3516
  if (!rows || rows.length === 0) {
2903
- console.log(chalk3.gray("(0 rows returned)"));
3517
+ console.log(chalk4.gray("(0 rows returned)"));
2904
3518
  return;
2905
3519
  }
2906
3520
  const fields = queryResult.fields && queryResult.fields.length > 0 ? queryResult.fields : Object.keys(rows[0] || {});
2907
3521
  if (fields.length === 0) return;
2908
3522
  const displayRows = rows.slice(0, 25);
2909
- const table = new Table({
2910
- head: fields.map((f) => chalk3.cyan.bold(f)),
3523
+ const table = new Table2({
3524
+ head: fields.map((f) => chalk4.cyan.bold(f)),
2911
3525
  style: { head: [], border: ["gray"] }
2912
3526
  });
2913
3527
  for (const r of displayRows) {
2914
3528
  table.push(
2915
3529
  fields.map((f) => {
2916
3530
  const val = r[f];
2917
- if (val === null || val === void 0) return chalk3.gray("NULL");
3531
+ if (val === null || val === void 0) return chalk4.gray("NULL");
2918
3532
  if (typeof val === "object") {
2919
3533
  try {
2920
3534
  return JSON.stringify(val);
@@ -2928,55 +3542,55 @@ Chat memory for session [${this.sessionId}] has been cleared.
2928
3542
  }
2929
3543
  console.log(table.toString());
2930
3544
  if (rows.length > 25) {
2931
- console.log(chalk3.yellow(`Showing first 25 of ${rows.length.toLocaleString()} rows.`));
3545
+ console.log(chalk4.yellow(`Showing first 25 of ${rows.length.toLocaleString()} rows.`));
2932
3546
  }
2933
3547
  console.log(
2934
- chalk3.gray(
3548
+ chalk4.gray(
2935
3549
  `Total rows: ${queryResult.rowCount ?? rows.length} | Execution duration: ${queryResult.durationMs}ms
2936
3550
  `
2937
3551
  )
2938
3552
  );
2939
3553
  }
2940
3554
  async handleListEntities() {
2941
- const spinner = ora(chalk3.blue("Inspecting schema...")).start();
3555
+ const spinner = ora(chalk4.blue("Inspecting schema...")).start();
2942
3556
  try {
2943
3557
  const schema = await this.adapter.inspectSchema(false);
2944
3558
  spinner.stop();
2945
3559
  if (schema.type === "postgres") {
2946
3560
  const tables = schema.tables || [];
2947
3561
  if (tables.length === 0) {
2948
- console.log(chalk3.yellow("No tables found in public schema."));
3562
+ console.log(chalk4.yellow("No tables found in public schema."));
2949
3563
  return;
2950
3564
  }
2951
- console.log(chalk3.bold.cyan(`
3565
+ console.log(chalk4.bold.cyan(`
2952
3566
  Tables in "${schema.databaseName}":`));
2953
3567
  for (const t of tables) {
2954
3568
  console.log(
2955
- ` \u2022 ${chalk3.bold(t.name)} ${chalk3.gray(`(~${t.approximateRowCount ?? 0} rows, ${t.columns.length} columns)`)}`
3569
+ ` \u2022 ${chalk4.bold(t.name)} ${chalk4.gray(`(~${t.approximateRowCount ?? 0} rows, ${t.columns.length} columns)`)}`
2956
3570
  );
2957
3571
  }
2958
3572
  console.log("");
2959
3573
  } else {
2960
3574
  const collections = schema.collections || [];
2961
3575
  if (collections.length === 0) {
2962
- console.log(chalk3.yellow("No collections found."));
3576
+ console.log(chalk4.yellow("No collections found."));
2963
3577
  return;
2964
3578
  }
2965
- console.log(chalk3.bold.cyan(`
3579
+ console.log(chalk4.bold.cyan(`
2966
3580
  Collections in "${schema.databaseName}":`));
2967
3581
  for (const c of collections) {
2968
3582
  console.log(
2969
- ` \u2022 ${chalk3.bold(c.name)} ${chalk3.gray(`(${c.documentCount ?? 0} docs, ${c.fields.length} sampled fields)`)}`
3583
+ ` \u2022 ${chalk4.bold(c.name)} ${chalk4.gray(`(${c.documentCount ?? 0} docs, ${c.fields.length} sampled fields)`)}`
2970
3584
  );
2971
3585
  }
2972
3586
  console.log("");
2973
3587
  }
2974
3588
  } catch (err) {
2975
- spinner.fail(chalk3.red(`Failed to list entities: ${err.message}`));
3589
+ spinner.fail(chalk4.red(`Failed to list entities: ${err.message}`));
2976
3590
  }
2977
3591
  }
2978
3592
  async handleShowSchema(entityName) {
2979
- const spinner = ora(chalk3.blue("Fetching schema details...")).start();
3593
+ const spinner = ora(chalk4.blue("Fetching schema details...")).start();
2980
3594
  try {
2981
3595
  const schema = await this.adapter.inspectSchema(false);
2982
3596
  spinner.stop();
@@ -2989,14 +3603,14 @@ Collections in "${schema.databaseName}":`));
2989
3603
  (t) => t.name.toLowerCase() === entityName.toLowerCase()
2990
3604
  );
2991
3605
  if (!table) {
2992
- console.log(chalk3.red(`Table "${entityName}" not found.`));
3606
+ console.log(chalk4.red(`Table "${entityName}" not found.`));
2993
3607
  return;
2994
3608
  }
2995
- console.log(chalk3.bold.cyan(`
3609
+ console.log(chalk4.bold.cyan(`
2996
3610
  Schema for table: ${table.schema}.${table.name}`));
2997
- const tableDetails = new Table({
3611
+ const tableDetails = new Table2({
2998
3612
  head: ["Column", "Data Type", "Nullable", "Primary Key", "Default"].map(
2999
- (h) => chalk3.cyan.bold(h)
3613
+ (h) => chalk4.cyan.bold(h)
3000
3614
  )
3001
3615
  });
3002
3616
  for (const c of table.columns) {
@@ -3004,8 +3618,8 @@ Schema for table: ${table.schema}.${table.name}`));
3004
3618
  c.name,
3005
3619
  c.dataType,
3006
3620
  c.isNullable ? "YES" : "NO",
3007
- c.isPrimaryKey ? chalk3.green("YES") : "NO",
3008
- c.defaultValue ?? chalk3.gray("NULL")
3621
+ c.isPrimaryKey ? chalk4.green("YES") : "NO",
3622
+ c.defaultValue ?? chalk4.gray("NULL")
3009
3623
  ]);
3010
3624
  }
3011
3625
  console.log(tableDetails.toString());
@@ -3014,13 +3628,13 @@ Schema for table: ${table.schema}.${table.name}`));
3014
3628
  (c) => c.name.toLowerCase() === entityName.toLowerCase()
3015
3629
  );
3016
3630
  if (!collection) {
3017
- console.log(chalk3.red(`Collection "${entityName}" not found.`));
3631
+ console.log(chalk4.red(`Collection "${entityName}" not found.`));
3018
3632
  return;
3019
3633
  }
3020
- console.log(chalk3.bold.cyan(`
3634
+ console.log(chalk4.bold.cyan(`
3021
3635
  Schema for collection: ${collection.name}`));
3022
- const colDetails = new Table({
3023
- head: ["Field", "Inferred Types"].map((h) => chalk3.cyan.bold(h))
3636
+ const colDetails = new Table2({
3637
+ head: ["Field", "Inferred Types"].map((h) => chalk4.cyan.bold(h))
3024
3638
  });
3025
3639
  for (const f of collection.fields) {
3026
3640
  colDetails.push([f.name, f.types.join(", ")]);
@@ -3028,22 +3642,22 @@ Schema for collection: ${collection.name}`));
3028
3642
  console.log(colDetails.toString());
3029
3643
  }
3030
3644
  } catch (err) {
3031
- spinner.fail(chalk3.red(`Failed to inspect schema: ${err.message}`));
3645
+ spinner.fail(chalk4.red(`Failed to inspect schema: ${err.message}`));
3032
3646
  }
3033
3647
  }
3034
3648
  printWelcomeBanner() {
3035
3649
  const banner = [
3036
- chalk3.bold.cyan("SANDAL: Safe Agentic Natural-language Database Access Layer"),
3037
- chalk3.gray("LangGraph + Multi-Provider AI (Gemini, OpenAI, Anthropic)"),
3650
+ chalk4.bold.cyan("SANDAL: Safe Agentic Natural-language Database Access Layer"),
3651
+ chalk4.gray("LangGraph + Multi-Provider AI (Gemini, OpenAI, Anthropic)"),
3038
3652
  "",
3039
- `${chalk3.bold("Database:")} ${chalk3.green(this.adapter.type.toUpperCase())} (${this.adapter.getMaskedUrl()})`,
3040
- `${chalk3.bold("LLM Provider:")} ${chalk3.blue(this.provider)} [${chalk3.white(this.modelName)}]`,
3041
- `${chalk3.bold("Chat Session:")} ${chalk3.yellow(this.sessionId)}`,
3042
- `${chalk3.bold("Strict Mode:")} ${this.strictMode ? chalk3.green("ON (Full wipes blocked)") : chalk3.yellow("OFF")}`,
3043
- `${chalk3.bold("Markdown:")} ${this.renderMarkdown ? chalk3.green("ENABLED") : chalk3.gray("DISABLED")}`,
3653
+ `${chalk4.bold("Database:")} ${chalk4.green(this.adapter.type.toUpperCase())} (${this.adapter.getMaskedUrl()})`,
3654
+ `${chalk4.bold("LLM Provider:")} ${chalk4.blue(this.provider)} [${chalk4.white(this.modelName)}]`,
3655
+ `${chalk4.bold("Chat Session:")} ${chalk4.yellow(this.sessionId)}`,
3656
+ `${chalk4.bold("Strict Mode:")} ${this.strictMode ? chalk4.green("ON (Full wipes blocked)") : chalk4.yellow("OFF")}`,
3657
+ `${chalk4.bold("Markdown:")} ${this.renderMarkdown ? chalk4.green("ENABLED") : chalk4.gray("DISABLED")}`,
3044
3658
  "",
3045
- chalk3.gray("Type your natural language request or SQL/Mongo query."),
3046
- chalk3.gray("Commands: .connect, .model, .chats, .new, .history, .key, .help, exit")
3659
+ chalk4.gray("Type your natural language request or SQL/Mongo query."),
3660
+ chalk4.gray("Commands: .connect, .model, .chats, .new, .history, .key, .help, exit")
3047
3661
  ].join("\n");
3048
3662
  console.log(
3049
3663
  boxen2(banner, {
@@ -3058,26 +3672,26 @@ Schema for collection: ${collection.name}`));
3058
3672
  console.log(
3059
3673
  boxen2(
3060
3674
  [
3061
- chalk3.bold.cyan("SANDAL REPL Commands:"),
3675
+ chalk4.bold.cyan("SANDAL REPL Commands:"),
3062
3676
  "",
3063
- `${chalk3.bold(".tables / .collections")} List all tables or collections with row counts`,
3064
- `${chalk3.bold(".schema [name]")} Show columns, types, and indexes for a table`,
3065
- `${chalk3.bold(".connect / .switch")} Switch database connection (shows saved connections)`,
3066
- `${chalk3.bold(".model [name]")} Change active LLM model on the fly`,
3067
- `${chalk3.bold(".provider [name]")} Switch LLM provider (google, openai, anthropic)`,
3068
- `${chalk3.bold(".key [remove | set]")} View, remove, or update API key`,
3069
- `${chalk3.bold(".chats")} List saved chat sessions`,
3070
- `${chalk3.bold(".chat <id | num>")} Switch to another chat session`,
3071
- `${chalk3.bold(".new")} Start a fresh chat session with clear memory`,
3072
- `${chalk3.bold(".history")} Show conversation history for current chat`,
3073
- `${chalk3.bold(".clear-chat")} Clear memory for current chat`,
3074
- `${chalk3.bold(".md [file | text]")} Render markdown file or text in terminal`,
3075
- `${chalk3.bold(".refresh")} Force refresh cached schema introspection`,
3076
- `${chalk3.bold(".clear")} Clear terminal screen`,
3077
- `${chalk3.bold(".help")} Display this command reference`,
3078
- `${chalk3.bold("exit / quit")} Gracefully disconnect and exit`,
3677
+ `${chalk4.bold(".tables / .collections")} List all tables or collections with row counts`,
3678
+ `${chalk4.bold(".schema [name]")} Show columns, types, and indexes for a table`,
3679
+ `${chalk4.bold(".connect / .switch")} Switch database connection (shows saved connections)`,
3680
+ `${chalk4.bold(".model [name]")} Change active LLM model on the fly`,
3681
+ `${chalk4.bold(".provider [name]")} Switch LLM provider (google, openai, anthropic)`,
3682
+ `${chalk4.bold(".key [remove | set]")} View, remove, or update API key`,
3683
+ `${chalk4.bold(".chats")} List saved chat sessions`,
3684
+ `${chalk4.bold(".chat <id | num>")} Switch to another chat session`,
3685
+ `${chalk4.bold(".new")} Start a fresh chat session with clear memory`,
3686
+ `${chalk4.bold(".history")} Show conversation history for current chat`,
3687
+ `${chalk4.bold(".clear-chat")} Clear memory for current chat`,
3688
+ `${chalk4.bold(".md [file | text]")} Render markdown file or text in terminal`,
3689
+ `${chalk4.bold(".refresh")} Force refresh cached schema introspection`,
3690
+ `${chalk4.bold(".clear")} Clear terminal screen`,
3691
+ `${chalk4.bold(".help")} Display this command reference`,
3692
+ `${chalk4.bold("exit / quit")} Gracefully disconnect and exit`,
3079
3693
  "",
3080
- chalk3.bold.yellow("Example Natural Language Requests:"),
3694
+ chalk4.bold.yellow("Example Natural Language Requests:"),
3081
3695
  ' "Show top 10 users signed up in the last 30 days"',
3082
3696
  ' "Now count how many of them are active" (multi-turn follow-up)',
3083
3697
  ' "Calculate total revenue grouped by product category"',
@@ -3102,13 +3716,16 @@ Schema for collection: ${collection.name}`));
3102
3716
  process.on("SIGTERM", handleExit);
3103
3717
  }
3104
3718
  async shutdown() {
3719
+ if (this.rl && !this.rl.closed) {
3720
+ this.rl.close();
3721
+ }
3105
3722
  if (this.adapter.isConnected()) {
3106
3723
  try {
3107
3724
  await this.adapter.disconnect();
3108
3725
  } catch {
3109
3726
  }
3110
3727
  }
3111
- console.log(chalk3.cyan("\nDatabase connection closed. Goodbye! \u{1F44B}\n"));
3728
+ console.log(chalk4.cyan("\nDatabase connection closed. Goodbye! \u{1F44B}\n"));
3112
3729
  }
3113
3730
  };
3114
3731
 
@@ -3136,7 +3753,18 @@ program.name("sandal-db").description("SANDAL - Safe Agentic Natural-language Da
3136
3753
  "--markdown",
3137
3754
  "Render LLM answers formatted as terminal markdown (default: true)",
3138
3755
  true
3139
- ).option("--no-markdown", "Disable terminal markdown rendering").action(async (cliDbUrl, options) => {
3756
+ ).option("--no-markdown", "Disable terminal markdown rendering").option(
3757
+ "--select-model",
3758
+ "Interactively choose a model from the list of available models"
3759
+ ).option(
3760
+ "--models [provider]",
3761
+ "List all publicly available models in the terminal (google | openai | anthropic)"
3762
+ ).action(async (cliDbUrl, options) => {
3763
+ if (options.models !== void 0) {
3764
+ const prov = typeof options.models === "string" && options.models.trim() ? options.models.toLowerCase() : options.provider ? options.provider.toLowerCase() : void 0;
3765
+ console.log(renderModelsTable(prov));
3766
+ return;
3767
+ }
3140
3768
  try {
3141
3769
  await runCli({
3142
3770
  cliDbUrl,
@@ -3146,10 +3774,11 @@ program.name("sandal-db").description("SANDAL - Safe Agentic Natural-language Da
3146
3774
  strictMode: options.strict !== false,
3147
3775
  allowFullWipe: Boolean(options.allowFullWipe),
3148
3776
  rowThreshold: parseInt(options.threshold, 10) || 50,
3149
- renderMarkdown: options.markdown !== false
3777
+ renderMarkdown: options.markdown !== false,
3778
+ selectModel: Boolean(options.selectModel)
3150
3779
  });
3151
3780
  } catch (err) {
3152
- console.error(chalk4.red(`
3781
+ console.error(chalk5.red(`
3153
3782
  Error: ${err.message}`));
3154
3783
  process.exit(1);
3155
3784
  }
@@ -3160,6 +3789,12 @@ var configCmd = program.command("config").description(
3160
3789
  "--set-provider <provider>",
3161
3790
  "Set default LLM provider (google | openai | anthropic)"
3162
3791
  ).option("--set-model <model>", "Set default model name").option(
3792
+ "--select-model",
3793
+ "Interactively select default model from the available models list"
3794
+ ).option(
3795
+ "--models [provider]",
3796
+ "List all publicly available models in the terminal (google | openai | anthropic)"
3797
+ ).option(
3163
3798
  "--set-markdown <boolean>",
3164
3799
  "Enable or disable default terminal markdown rendering (true | false)"
3165
3800
  ).option(
@@ -3174,7 +3809,22 @@ var configCmd = program.command("config").description(
3174
3809
  ).option(
3175
3810
  "--show",
3176
3811
  "Display current stored configuration (with masked secrets)"
3177
- ).option("--path", "Print the path to the configuration file").action((opts) => {
3812
+ ).option("--path", "Print the path to the configuration file").action(async (opts) => {
3813
+ if (opts.models !== void 0) {
3814
+ const prov = typeof opts.models === "string" && opts.models.trim() ? opts.models.toLowerCase() : void 0;
3815
+ console.log(renderModelsTable(prov));
3816
+ return;
3817
+ }
3818
+ if (opts.selectModel) {
3819
+ const cfg = readConfig();
3820
+ const prov = opts.setProvider || cfg.defaultProvider || "google";
3821
+ const chosen = await promptModelSelection(prov, cfg.defaultModel);
3822
+ writeConfig({ defaultModel: chosen });
3823
+ console.log(chalk5.green(`
3824
+ Updated default model to: ${chosen} (${prov})
3825
+ `));
3826
+ return;
3827
+ }
3178
3828
  if (opts.path) {
3179
3829
  console.log(getConfigFilePath());
3180
3830
  return;
@@ -3182,9 +3832,9 @@ var configCmd = program.command("config").description(
3182
3832
  if (opts.connections) {
3183
3833
  const list = getSavedConnections();
3184
3834
  if (list.length === 0) {
3185
- console.log(chalk4.yellow("\nNo saved database connections found.\n"));
3835
+ console.log(chalk5.yellow("\nNo saved database connections found.\n"));
3186
3836
  } else {
3187
- console.log(chalk4.bold.cyan("\nSaved Database Connections:"));
3837
+ console.log(chalk5.bold.cyan("\nSaved Database Connections:"));
3188
3838
  list.forEach((url, i) => {
3189
3839
  console.log(` [${i + 1}] ${maskUrl(url)}`);
3190
3840
  });
@@ -3195,47 +3845,47 @@ var configCmd = program.command("config").description(
3195
3845
  if (opts.removeConnection) {
3196
3846
  const removed = removeSavedConnection(opts.removeConnection);
3197
3847
  if (removed) {
3198
- console.log(chalk4.green(`Removed connection from saved history.`));
3848
+ console.log(chalk5.green(`Removed connection from saved history.`));
3199
3849
  } else {
3200
- console.log(chalk4.red(`Connection "${opts.removeConnection}" not found in history.`));
3850
+ console.log(chalk5.red(`Connection "${opts.removeConnection}" not found in history.`));
3201
3851
  }
3202
3852
  return;
3203
3853
  }
3204
3854
  if (opts.removeKey !== void 0) {
3205
3855
  const prov = typeof opts.removeKey === "string" && opts.removeKey.trim() ? opts.removeKey.toLowerCase() : "all";
3206
3856
  removeApiKey(prov);
3207
- console.log(chalk4.green(`Removed API key for "${prov}" from ~/.sandal/config.json.`));
3857
+ console.log(chalk5.green(`Removed API key for "${prov}" from ~/.sandal/config.json.`));
3208
3858
  return;
3209
3859
  }
3210
3860
  if (opts.show) {
3211
3861
  const cfg = readConfig();
3212
3862
  const saved = getSavedConnections();
3213
3863
  console.log(
3214
- chalk4.bold.cyan("\nSaved Configuration (~/.sandal/config.json):")
3864
+ chalk5.bold.cyan("\nSaved Configuration (~/.sandal/config.json):")
3215
3865
  );
3216
3866
  console.log(
3217
- ` Database URL: ${cfg.dbUrl ? chalk4.green(maskUrl(cfg.dbUrl)) : chalk4.gray("Not set")}`
3867
+ ` Database URL: ${cfg.dbUrl ? chalk5.green(maskUrl(cfg.dbUrl)) : chalk5.gray("Not set")}`
3218
3868
  );
3219
3869
  console.log(
3220
- ` Default Provider: ${cfg.defaultProvider ? chalk4.blue(cfg.defaultProvider) : chalk4.gray("Not set (defaults to google)")}`
3870
+ ` Default Provider: ${cfg.defaultProvider ? chalk5.blue(cfg.defaultProvider) : chalk5.gray("Not set (defaults to google)")}`
3221
3871
  );
3222
3872
  console.log(
3223
- ` Default Model: ${cfg.defaultModel ? chalk4.white(cfg.defaultModel) : chalk4.gray("Default for provider")}`
3873
+ ` Default Model: ${cfg.defaultModel ? chalk5.white(cfg.defaultModel) : chalk5.gray("Default for provider")}`
3224
3874
  );
3225
3875
  console.log(
3226
- ` Markdown Rendering: ${cfg.renderMarkdown !== false ? chalk4.green("enabled") : chalk4.yellow("disabled")}`
3876
+ ` Markdown Rendering: ${cfg.renderMarkdown !== false ? chalk5.green("enabled") : chalk5.yellow("disabled")}`
3227
3877
  );
3228
3878
  console.log(
3229
- ` Saved Connections: ${saved.length > 0 ? chalk4.cyan(`${saved.length} connection(s)`) : chalk4.gray("None")}`
3879
+ ` Saved Connections: ${saved.length > 0 ? chalk5.cyan(`${saved.length} connection(s)`) : chalk5.gray("None")}`
3230
3880
  );
3231
3881
  console.log(
3232
- ` Gemini Key: ${cfg.geminiApiKey ? chalk4.yellow(maskApiKey(cfg.geminiApiKey)) : chalk4.gray("Not set")}`
3882
+ ` Gemini Key: ${cfg.geminiApiKey ? chalk5.yellow(maskApiKey(cfg.geminiApiKey)) : chalk5.gray("Not set")}`
3233
3883
  );
3234
3884
  console.log(
3235
- ` OpenAI Key: ${cfg.openaiApiKey ? chalk4.yellow(maskApiKey(cfg.openaiApiKey)) : chalk4.gray("Not set")}`
3885
+ ` OpenAI Key: ${cfg.openaiApiKey ? chalk5.yellow(maskApiKey(cfg.openaiApiKey)) : chalk5.gray("Not set")}`
3236
3886
  );
3237
3887
  console.log(
3238
- ` Anthropic Key: ${cfg.anthropicApiKey ? chalk4.yellow(maskApiKey(cfg.anthropicApiKey)) : chalk4.gray("Not set")}
3888
+ ` Anthropic Key: ${cfg.anthropicApiKey ? chalk5.yellow(maskApiKey(cfg.anthropicApiKey)) : chalk5.gray("Not set")}
3239
3889
  `
3240
3890
  );
3241
3891
  return;
@@ -3246,44 +3896,64 @@ var configCmd = program.command("config").description(
3246
3896
  updates.dbUrl = opts.setDb;
3247
3897
  addSavedConnection(opts.setDb);
3248
3898
  console.log(
3249
- chalk4.green(`Updated default database URL: ${maskUrl(opts.setDb)}`)
3899
+ chalk5.green(`Updated default database URL: ${maskUrl(opts.setDb)}`)
3250
3900
  );
3251
3901
  }
3252
3902
  if (opts.setProvider) {
3253
3903
  const p = opts.setProvider.toLowerCase();
3254
3904
  if (!["google", "openai", "anthropic"].includes(p)) {
3255
3905
  console.error(
3256
- chalk4.red("Invalid provider. Expected google, openai, or anthropic.")
3906
+ chalk5.red("Invalid provider. Expected google, openai, or anthropic.")
3257
3907
  );
3258
3908
  process.exit(1);
3259
3909
  }
3260
3910
  updates.defaultProvider = p;
3261
- console.log(chalk4.green(`Updated default provider: ${p}`));
3911
+ console.log(chalk5.green(`Updated default provider: ${p}`));
3912
+ const existingKey = getStoredApiKeyForProvider(p);
3913
+ if (!existingKey && !opts.setKey && !opts.setGemini && !opts.setOpenai && !opts.setAnthropic) {
3914
+ console.log(chalk5.yellow(`
3915
+ No stored API key found for ${p.toUpperCase()}.`));
3916
+ const newKey = await input2({
3917
+ message: `Enter API key for ${p.toUpperCase()}:`,
3918
+ validate: (val) => val.trim().length > 0 ? true : "API key cannot be empty."
3919
+ });
3920
+ if (p === "google") updates.geminiApiKey = newKey.trim();
3921
+ else if (p === "openai") updates.openaiApiKey = newKey.trim();
3922
+ else if (p === "anthropic") updates.anthropicApiKey = newKey.trim();
3923
+ console.log(chalk5.green(`Saved API key for ${p.toUpperCase()}.`));
3924
+ }
3925
+ if (!opts.setModel) {
3926
+ console.log(chalk5.cyan(`
3927
+ Choose default model for ${p.toUpperCase()}:`));
3928
+ const chosenModel = await promptModelSelection(p);
3929
+ updates.defaultModel = chosenModel;
3930
+ console.log(chalk5.green(`Updated default model for ${p}: ${chosenModel}`));
3931
+ }
3262
3932
  }
3263
3933
  if (opts.setModel) {
3264
3934
  updates.defaultModel = opts.setModel;
3265
- console.log(chalk4.green(`Updated default model: ${opts.setModel}`));
3935
+ console.log(chalk5.green(`Updated default model: ${opts.setModel}`));
3266
3936
  }
3267
3937
  if (opts.setMarkdown !== void 0) {
3268
3938
  const val = opts.setMarkdown.toLowerCase() === "true" || opts.setMarkdown === "1" || opts.setMarkdown === "yes";
3269
3939
  updates.renderMarkdown = val;
3270
3940
  console.log(
3271
- chalk4.green(
3941
+ chalk5.green(
3272
3942
  `Updated default markdown rendering: ${val ? "enabled" : "disabled"}.`
3273
3943
  )
3274
3944
  );
3275
3945
  }
3276
3946
  if (opts.setGemini) {
3277
3947
  updates.geminiApiKey = opts.setGemini;
3278
- console.log(chalk4.green("Updated Gemini API key."));
3948
+ console.log(chalk5.green("Updated Gemini API key."));
3279
3949
  }
3280
3950
  if (opts.setOpenai) {
3281
3951
  updates.openaiApiKey = opts.setOpenai;
3282
- console.log(chalk4.green("Updated OpenAI API key."));
3952
+ console.log(chalk5.green("Updated OpenAI API key."));
3283
3953
  }
3284
3954
  if (opts.setAnthropic) {
3285
3955
  updates.anthropicApiKey = opts.setAnthropic;
3286
- console.log(chalk4.green("Updated Anthropic API key."));
3956
+ console.log(chalk5.green("Updated Anthropic API key."));
3287
3957
  }
3288
3958
  if (opts.setKey) {
3289
3959
  const current = readConfig();
@@ -3291,11 +3961,11 @@ var configCmd = program.command("config").description(
3291
3961
  if (prov === "google") updates.geminiApiKey = opts.setKey;
3292
3962
  else if (prov === "openai") updates.openaiApiKey = opts.setKey;
3293
3963
  else if (prov === "anthropic") updates.anthropicApiKey = opts.setKey;
3294
- console.log(chalk4.green(`Updated API key for provider "${prov}".`));
3964
+ console.log(chalk5.green(`Updated API key for provider "${prov}".`));
3295
3965
  }
3296
3966
  if (Object.keys(updates).length > 0) {
3297
3967
  writeConfig(updates);
3298
- console.log(chalk4.gray(`Saved to ${getConfigFilePath()}`));
3968
+ console.log(chalk5.gray(`Saved to ${getConfigFilePath()}`));
3299
3969
  } else {
3300
3970
  configCmd.help();
3301
3971
  }
@@ -3311,13 +3981,13 @@ program.command("markdown [fileOrText]").alias("md").description("Render markdow
3311
3981
  content = Buffer.concat(chunks).toString("utf-8");
3312
3982
  } else {
3313
3983
  console.log(
3314
- chalk4.yellow(
3984
+ chalk5.yellow(
3315
3985
  "\nUsage: sandal md <file | text> or pipe markdown to sandal md"
3316
3986
  )
3317
3987
  );
3318
- console.log(chalk4.gray("Example: sandal md README.md"));
3319
- console.log(chalk4.gray('Example: sandal md "# Hello World"'));
3320
- console.log(chalk4.gray("Example: cat notes.md | sandal md\n"));
3988
+ console.log(chalk5.gray("Example: sandal md README.md"));
3989
+ console.log(chalk5.gray('Example: sandal md "# Hello World"'));
3990
+ console.log(chalk5.gray("Example: cat notes.md | sandal md\n"));
3321
3991
  return;
3322
3992
  }
3323
3993
  } else {
@@ -3347,7 +4017,7 @@ async function runCli(opts) {
3347
4017
  let dbUrl = resolved.dbUrl;
3348
4018
  if (!dbUrl) {
3349
4019
  console.log(
3350
- chalk4.yellow(
4020
+ chalk5.yellow(
3351
4021
  "\nNo database connection URL found in CLI flags, environment, or config."
3352
4022
  )
3353
4023
  );
@@ -3362,7 +4032,7 @@ async function runCli(opts) {
3362
4032
  }
3363
4033
  }
3364
4034
  });
3365
- const saveDb = await confirm2({
4035
+ const saveDb = await confirm({
3366
4036
  message: "Save this database URL to ~/.sandal/config.json for future runs?",
3367
4037
  default: true
3368
4038
  });
@@ -3377,34 +4047,55 @@ async function runCli(opts) {
3377
4047
  }
3378
4048
  let provider = resolved.provider;
3379
4049
  let apiKey = resolved.apiKey;
4050
+ const isProviderExplicitlyChanged = Boolean(
4051
+ opts.cliProvider && opts.cliProvider.toLowerCase() !== config.defaultProvider
4052
+ );
3380
4053
  if (!apiKey) {
3381
- console.log(chalk4.yellow("\nNo API key found in environment or config."));
3382
- provider = await select({
3383
- message: "Select your LLM provider:",
3384
- choices: [
3385
- { name: "Google Gemini (GEMINI_API_KEY)", value: "google" },
3386
- { name: "OpenAI (OPENAI_API_KEY)", value: "openai" },
3387
- { name: "Anthropic (ANTHROPIC_API_KEY)", value: "anthropic" }
3388
- ]
3389
- });
4054
+ if (opts.cliProvider) {
4055
+ console.log(
4056
+ chalk5.yellow(`
4057
+ No API key found for ${provider.toUpperCase()} in environment or config.`)
4058
+ );
4059
+ } else {
4060
+ console.log(chalk5.yellow("\nNo API key found in environment or config."));
4061
+ provider = await select2({
4062
+ message: "Select your LLM provider:",
4063
+ choices: [
4064
+ { name: "Google Gemini (GEMINI_API_KEY)", value: "google" },
4065
+ { name: "OpenAI (OPENAI_API_KEY)", value: "openai" },
4066
+ { name: "Anthropic (ANTHROPIC_API_KEY)", value: "anthropic" }
4067
+ ]
4068
+ });
4069
+ }
3390
4070
  apiKey = await input2({
3391
4071
  message: `Enter your ${provider.toUpperCase()} API key:`,
3392
4072
  validate: (val) => val.trim().length > 0 ? true : "API key cannot be empty."
3393
4073
  });
3394
- const saveKey = await confirm2({
3395
- message: "Save this API key to ~/.sandal/config.json?",
4074
+ let chosenModel = opts.cliModel;
4075
+ if (!chosenModel) {
4076
+ chosenModel = await promptModelSelection(provider);
4077
+ }
4078
+ const saveKey = await confirm({
4079
+ message: "Save this API key and default model to ~/.sandal/config.json?",
3396
4080
  default: true
3397
4081
  });
3398
4082
  if (saveKey) {
3399
- if (provider === "google")
3400
- writeConfig({ geminiApiKey: apiKey, defaultProvider: "google" });
3401
- else if (provider === "openai")
3402
- writeConfig({ openaiApiKey: apiKey, defaultProvider: "openai" });
3403
- else if (provider === "anthropic")
3404
- writeConfig({ anthropicApiKey: apiKey, defaultProvider: "anthropic" });
4083
+ const updates = {
4084
+ defaultProvider: provider,
4085
+ defaultModel: chosenModel
4086
+ };
4087
+ if (provider === "google") updates.geminiApiKey = apiKey;
4088
+ else if (provider === "openai") updates.openaiApiKey = apiKey;
4089
+ else if (provider === "anthropic") updates.anthropicApiKey = apiKey;
4090
+ writeConfig(updates);
3405
4091
  }
3406
4092
  }
3407
- const modelName = opts.cliModel || resolved.model || getDefaultModelForProvider(provider);
4093
+ let modelName = opts.cliModel;
4094
+ if (opts.selectModel || isProviderExplicitlyChanged && !opts.cliModel) {
4095
+ modelName = await promptModelSelection(provider, resolved.model);
4096
+ } else if (!modelName) {
4097
+ modelName = resolved.model || getDefaultModelForProvider(provider);
4098
+ }
3408
4099
  const dbSpinner = ora2(
3409
4100
  `Connecting to database (${maskUrl(dbUrl)})...`
3410
4101
  ).start();
@@ -3412,13 +4103,13 @@ async function runCli(opts) {
3412
4103
  try {
3413
4104
  await adapter.connect();
3414
4105
  dbSpinner.succeed(
3415
- chalk4.green(
4106
+ chalk5.green(
3416
4107
  `Connected to ${adapter.type.toUpperCase()} database: ${adapter.databaseName}`
3417
4108
  )
3418
4109
  );
3419
4110
  addSavedConnection(dbUrl);
3420
4111
  } catch (err) {
3421
- dbSpinner.fail(chalk4.red(`Failed to connect to database: ${err.message}`));
4112
+ dbSpinner.fail(chalk5.red(`Failed to connect to database: ${err.message}`));
3422
4113
  process.exit(1);
3423
4114
  }
3424
4115
  const model = createChatModel({
@@ -3426,6 +4117,7 @@ async function runCli(opts) {
3426
4117
  model: modelName,
3427
4118
  apiKey
3428
4119
  });
4120
+ process.stdin.resume();
3429
4121
  const repl = new ReplSession({
3430
4122
  adapter,
3431
4123
  model,