sandal-db 1.0.3 → 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 +913 -285
  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, select, confirm } 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);
@@ -967,10 +1252,10 @@ function createChatModel(options) {
967
1252
  // src/repl.ts
968
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";
@@ -1383,7 +1668,7 @@ function classifyMongoQuery(rawText, query, rowThreshold, options) {
1383
1668
 
1384
1669
  // src/safety/confirm.ts
1385
1670
  import readline from "node:readline";
1386
- import chalk from "chalk";
1671
+ import chalk2 from "chalk";
1387
1672
  import boxen from "boxen";
1388
1673
  async function defaultAsk(promptText) {
1389
1674
  process.stdin.resume();
@@ -1400,7 +1685,7 @@ async function defaultAsk(promptText) {
1400
1685
  });
1401
1686
  }
1402
1687
  async function promptConfirm(message, defaultValue, ask = defaultAsk) {
1403
- const suffix = defaultValue ? chalk.gray(" (Y/n): ") : chalk.gray(" (y/N): ");
1688
+ const suffix = defaultValue ? chalk2.gray(" (Y/n): ") : chalk2.gray(" (y/N): ");
1404
1689
  const response = (await ask(`${message}${suffix}`)).trim().toLowerCase();
1405
1690
  if (!response) {
1406
1691
  return defaultValue;
@@ -1421,12 +1706,12 @@ async function requestUserConfirmation(query, safety, affectedRows, ask = defaul
1421
1706
  const queryDisplay = (query.sql || query.rawDisplay || "").trim();
1422
1707
  if (safety.isFullWipe) {
1423
1708
  const boxContent = [
1424
- 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}"),
1425
1710
  "",
1426
- chalk.yellow(queryDisplay),
1711
+ chalk2.yellow(queryDisplay),
1427
1712
  "",
1428
- ...safety.warnings.length ? [chalk.red(safety.warnings.join("\n")), ""] : [],
1429
- 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:`)
1430
1715
  ].join("\n");
1431
1716
  console.log(
1432
1717
  boxen(boxContent, {
@@ -1437,7 +1722,7 @@ async function requestUserConfirmation(query, safety, affectedRows, ask = defaul
1437
1722
  })
1438
1723
  );
1439
1724
  const typed = await promptInput(
1440
- chalk.red.bold(`Type "${safety.literalWord || "DROP DATABASE"}" to confirm full wipe: `),
1725
+ chalk2.red.bold(`Type "${safety.literalWord || "DROP DATABASE"}" to confirm full wipe: `),
1441
1726
  ask
1442
1727
  );
1443
1728
  if (typed.trim() === safety.literalWord) {
@@ -1450,20 +1735,20 @@ async function requestUserConfirmation(query, safety, affectedRows, ask = defaul
1450
1735
  }
1451
1736
  if (safety.category === "dangerous") {
1452
1737
  const lines2 = [
1453
- chalk.bold.hex("#FFA500")("\u26A0\uFE0F DANGEROUS OPERATION"),
1738
+ chalk2.bold.hex("#FFA500")("\u26A0\uFE0F DANGEROUS OPERATION"),
1454
1739
  "",
1455
- chalk.white(queryDisplay),
1740
+ chalk2.white(queryDisplay),
1456
1741
  ""
1457
1742
  ];
1458
1743
  if (affectedRows !== null) {
1459
1744
  lines2.push(
1460
- chalk.bold.yellow(`Affected rows: ${affectedRows.toLocaleString()}`),
1745
+ chalk2.bold.yellow(`Affected rows: ${affectedRows.toLocaleString()}`),
1461
1746
  ""
1462
1747
  );
1463
1748
  }
1464
1749
  if (safety.warnings.length > 0) {
1465
1750
  for (const w of safety.warnings) {
1466
- lines2.push(chalk.red(`\u2022 ${w}`));
1751
+ lines2.push(chalk2.red(`\u2022 ${w}`));
1467
1752
  }
1468
1753
  lines2.push("");
1469
1754
  }
@@ -1477,7 +1762,7 @@ async function requestUserConfirmation(query, safety, affectedRows, ask = defaul
1477
1762
  );
1478
1763
  if (safety.requiresLiteralWord && safety.literalWord) {
1479
1764
  const typed = await promptInput(
1480
- chalk.hex("#FFA500").bold(
1765
+ chalk2.hex("#FFA500").bold(
1481
1766
  `Destructive operation with no filter. Type "${safety.literalWord}" to confirm: `
1482
1767
  ),
1483
1768
  ask
@@ -1491,7 +1776,7 @@ async function requestUserConfirmation(query, safety, affectedRows, ask = defaul
1491
1776
  };
1492
1777
  }
1493
1778
  const answer2 = await promptConfirm(
1494
- chalk.hex("#FFA500").bold("Proceed with dangerous operation?"),
1779
+ chalk2.hex("#FFA500").bold("Proceed with dangerous operation?"),
1495
1780
  false,
1496
1781
  ask
1497
1782
  );
@@ -1499,23 +1784,23 @@ async function requestUserConfirmation(query, safety, affectedRows, ask = defaul
1499
1784
  }
1500
1785
  if (safety.isStructural) {
1501
1786
  const lines2 = [
1502
- chalk.bold.cyan("\u{1F6E0}\uFE0F STRUCTURAL DDL OPERATION"),
1787
+ chalk2.bold.cyan("\u{1F6E0}\uFE0F STRUCTURAL DDL OPERATION"),
1503
1788
  "",
1504
- chalk.white(queryDisplay),
1789
+ chalk2.white(queryDisplay),
1505
1790
  "",
1506
- chalk.cyan(`Effect: ${safety.explanation || "Modifies database structure."}`)
1791
+ chalk2.cyan(`Effect: ${safety.explanation || "Modifies database structure."}`)
1507
1792
  ];
1508
1793
  if (safety.suggestedAlternative) {
1509
1794
  lines2.push(
1510
1795
  "",
1511
- chalk.yellow("\u{1F4A1} Recommendation:"),
1512
- chalk.gray(safety.suggestedAlternative)
1796
+ chalk2.yellow("\u{1F4A1} Recommendation:"),
1797
+ chalk2.gray(safety.suggestedAlternative)
1513
1798
  );
1514
1799
  }
1515
1800
  if (safety.warnings.length > 0) {
1516
1801
  lines2.push("");
1517
1802
  for (const w of safety.warnings) {
1518
- lines2.push(chalk.yellow(`\u2022 ${w}`));
1803
+ lines2.push(chalk2.yellow(`\u2022 ${w}`));
1519
1804
  }
1520
1805
  }
1521
1806
  console.log(
@@ -1527,7 +1812,7 @@ async function requestUserConfirmation(query, safety, affectedRows, ask = defaul
1527
1812
  })
1528
1813
  );
1529
1814
  const answer2 = await promptConfirm(
1530
- chalk.cyan.bold("Apply this structural change?"),
1815
+ chalk2.cyan.bold("Apply this structural change?"),
1531
1816
  true,
1532
1817
  ask
1533
1818
  );
@@ -1535,12 +1820,12 @@ async function requestUserConfirmation(query, safety, affectedRows, ask = defaul
1535
1820
  }
1536
1821
  if (safety.category === "write") {
1537
1822
  const lines2 = [
1538
- chalk.bold.magenta("\u270F\uFE0F DATABASE WRITE OPERATION"),
1823
+ chalk2.bold.magenta("\u270F\uFE0F DATABASE WRITE OPERATION"),
1539
1824
  "",
1540
- chalk.white(queryDisplay)
1825
+ chalk2.white(queryDisplay)
1541
1826
  ];
1542
1827
  if (affectedRows !== null) {
1543
- lines2.push("", chalk.magenta(`Estimated affected rows: ${affectedRows.toLocaleString()}`));
1828
+ lines2.push("", chalk2.magenta(`Estimated affected rows: ${affectedRows.toLocaleString()}`));
1544
1829
  }
1545
1830
  console.log(
1546
1831
  boxen(lines2.join("\n"), {
@@ -1551,16 +1836,16 @@ async function requestUserConfirmation(query, safety, affectedRows, ask = defaul
1551
1836
  })
1552
1837
  );
1553
1838
  const answer2 = await promptConfirm(
1554
- chalk.magenta.bold("Execute write query?"),
1839
+ chalk2.magenta.bold("Execute write query?"),
1555
1840
  false,
1556
1841
  ask
1557
1842
  );
1558
1843
  return { confirmed: answer2 };
1559
1844
  }
1560
1845
  const lines = [
1561
- chalk.bold.green("\u{1F50D} READ QUERY"),
1846
+ chalk2.bold.green("\u{1F50D} READ QUERY"),
1562
1847
  "",
1563
- chalk.white(queryDisplay)
1848
+ chalk2.white(queryDisplay)
1564
1849
  ];
1565
1850
  console.log(
1566
1851
  boxen(lines.join("\n"), {
@@ -1571,7 +1856,7 @@ async function requestUserConfirmation(query, safety, affectedRows, ask = defaul
1571
1856
  })
1572
1857
  );
1573
1858
  const answer = await promptConfirm(
1574
- chalk.green.bold("Execute read query?"),
1859
+ chalk2.green.bold("Execute read query?"),
1575
1860
  true,
1576
1861
  ask
1577
1862
  );
@@ -1704,10 +1989,35 @@ var OBVIOUS_DATABASE_PATTERNS = [
1704
1989
  /\b(?:table|tables|collection|collections|schema|column|columns|index|indexes|foreign key|primary key)\b/i,
1705
1990
  /\b(?:find|aggregate|count|where|group by|order by|having|limit|join|left join|inner join)\b/i,
1706
1991
  /\b(?:how many (?:users|rows|records|orders|items|products|documents))\b/i,
1707
- /\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
1708
1994
  ];
1709
- async function classifyIntent(input2, model, historyContext) {
1710
- const trimmed = input2.trim();
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
+ }
2019
+ async function classifyIntent(input3, model, historyContext) {
2020
+ const trimmed = input3.trim();
1711
2021
  for (const pattern of OBVIOUS_OUT_OF_SCOPE_PATTERNS) {
1712
2022
  if (pattern.test(trimmed)) {
1713
2023
  return {
@@ -1716,6 +2026,9 @@ async function classifyIntent(input2, model, historyContext) {
1716
2026
  };
1717
2027
  }
1718
2028
  }
2029
+ if (isExplicitNoQuery(trimmed)) {
2030
+ return { isDatabaseTask: true };
2031
+ }
1719
2032
  for (const pattern of OBVIOUS_DATABASE_PATTERNS) {
1720
2033
  if (pattern.test(trimmed)) {
1721
2034
  return {
@@ -1726,7 +2039,9 @@ async function classifyIntent(input2, model, historyContext) {
1726
2039
  if (/^(?:tables|collections|schema|indexes|views|stats|count|info)$/i.test(trimmed)) {
1727
2040
  return { isDatabaseTask: true };
1728
2041
  }
1729
- 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
+ )) {
1730
2045
  return { isDatabaseTask: true };
1731
2046
  }
1732
2047
  if (!model) {
@@ -1791,10 +2106,22 @@ var AgentStateAnnotation = Annotation.Root({
1791
2106
  reducer: (_, update) => update ?? [],
1792
2107
  default: () => []
1793
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
+ }),
1794
2117
  generatedQuery: Annotation({
1795
2118
  reducer: (_, update) => update,
1796
2119
  default: () => void 0
1797
2120
  }),
2121
+ lastGeneratedQuery: Annotation({
2122
+ reducer: (curr, update) => update ?? curr,
2123
+ default: () => void 0
2124
+ }),
1798
2125
  safety: Annotation({
1799
2126
  reducer: (_, update) => update,
1800
2127
  default: () => void 0
@@ -1815,6 +2142,10 @@ var AgentStateAnnotation = Annotation.Root({
1815
2142
  reducer: (_, update) => update,
1816
2143
  default: () => void 0
1817
2144
  }),
2145
+ lastQueryResult: Annotation({
2146
+ reducer: (curr, update) => update ?? curr,
2147
+ default: () => void 0
2148
+ }),
1818
2149
  analysis: Annotation({
1819
2150
  reducer: (_, update) => update,
1820
2151
  default: () => void 0
@@ -1823,6 +2154,10 @@ var AgentStateAnnotation = Annotation.Root({
1823
2154
  reducer: (_, update) => update,
1824
2155
  default: () => void 0
1825
2156
  }),
2157
+ lastStats: Annotation({
2158
+ reducer: (curr, update) => update ?? curr,
2159
+ default: () => void 0
2160
+ }),
1826
2161
  conclusion: Annotation({
1827
2162
  reducer: (_, update) => update,
1828
2163
  default: () => void 0
@@ -1832,7 +2167,7 @@ var AgentStateAnnotation = Annotation.Root({
1832
2167
  default: () => false
1833
2168
  }),
1834
2169
  messages: Annotation({
1835
- reducer: (curr, update) => curr.concat(update ?? []),
2170
+ reducer: (curr, update) => update !== void 0 ? update : curr,
1836
2171
  default: () => []
1837
2172
  })
1838
2173
  });
@@ -1851,6 +2186,94 @@ function createDatabaseAgent(config) {
1851
2186
  requiresSchemaRefresh: false
1852
2187
  };
1853
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
+ }
1854
2277
  async function identifyTargetsNode(state) {
1855
2278
  const historyMessages = state.messages.slice(-4);
1856
2279
  const prompt = `Based on this user request: "${state.userInput}"
@@ -2022,7 +2445,8 @@ Do not wrap with markdown or code fences.`;
2022
2445
  const conclusion = state.cancelReason || "Operation was not executed.";
2023
2446
  return {
2024
2447
  conclusion,
2025
- messages: [new HumanMessage2(state.userInput), new AIMessage(conclusion)]
2448
+ queryExecuted: false,
2449
+ messages: [...state.messages, new HumanMessage2(state.userInput), new AIMessage(conclusion)]
2026
2450
  };
2027
2451
  }
2028
2452
  const res = state.queryResult;
@@ -2030,14 +2454,18 @@ Do not wrap with markdown or code fences.`;
2030
2454
  const conclusion = "No query was executed.";
2031
2455
  return {
2032
2456
  conclusion,
2033
- messages: [new HumanMessage2(state.userInput), new AIMessage(conclusion)]
2457
+ queryExecuted: false,
2458
+ messages: [...state.messages, new HumanMessage2(state.userInput), new AIMessage(conclusion)]
2034
2459
  };
2035
2460
  }
2036
2461
  if (!res.success) {
2037
2462
  const conclusion = `Query failed with error: ${res.error} (took ${res.durationMs}ms)`;
2038
2463
  return {
2039
2464
  conclusion,
2040
- 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)]
2041
2469
  };
2042
2470
  }
2043
2471
  const prompt = `User request: "${state.userInput}"
@@ -2062,7 +2490,11 @@ Ground your response strictly in the query results above. Never hallucinate rows
2062
2490
  ${conclusion}` : conclusion;
2063
2491
  return {
2064
2492
  conclusion,
2065
- 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)]
2066
2498
  };
2067
2499
  } catch (err) {
2068
2500
  const conclusion = `Query executed successfully (${res.rowCount ?? 0} rows, ${res.durationMs}ms).`;
@@ -2071,16 +2503,88 @@ ${conclusion}` : conclusion;
2071
2503
  ${conclusion}` : conclusion;
2072
2504
  return {
2073
2505
  conclusion,
2074
- 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)]
2075
2511
  };
2076
2512
  }
2077
2513
  }
2078
- 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) => {
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)]
2574
+ };
2575
+ }
2576
+ }
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) => {
2079
2583
  if (state.confirmed && !state.cancelReason) {
2080
2584
  return "execute_query";
2081
2585
  }
2082
2586
  return "produce_conclusion";
2083
- }).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);
2084
2588
  const checkpointer = new MemorySaver();
2085
2589
  const app = workflow.compile({ checkpointer });
2086
2590
  return app;
@@ -2089,19 +2593,19 @@ ${conclusion}` : conclusion;
2089
2593
  // src/markdown.ts
2090
2594
  import { Marked } from "marked";
2091
2595
  import { markedTerminal } from "marked-terminal";
2092
- import chalk2 from "chalk";
2596
+ import chalk3 from "chalk";
2093
2597
  function createMarkdownRenderer(options = {}) {
2094
2598
  const terminalOptions = {
2095
- heading: chalk2.bold.cyan,
2096
- firstHeading: chalk2.bold.cyan.underline,
2097
- codespan: chalk2.yellow,
2098
- code: chalk2.yellow,
2099
- blockquote: chalk2.gray.italic,
2100
- hr: (input2) => {
2599
+ heading: chalk3.bold.cyan,
2600
+ firstHeading: chalk3.bold.cyan.underline,
2601
+ codespan: chalk3.yellow,
2602
+ code: chalk3.yellow,
2603
+ blockquote: chalk3.gray.italic,
2604
+ hr: (input3) => {
2101
2605
  const w = Math.min(options.width ?? (process.stdout.columns || 80), 80);
2102
- return chalk2.gray(input2 && input2.trim() ? input2 : "\u2500".repeat(w));
2606
+ return chalk3.gray(input3 && input3.trim() ? input3 : "\u2500".repeat(w));
2103
2607
  },
2104
- table: chalk2.reset,
2608
+ table: chalk3.reset,
2105
2609
  tableOptions: {
2106
2610
  style: {
2107
2611
  head: ["cyan", "bold"],
@@ -2287,6 +2791,10 @@ var ChatMemoryManager = class {
2287
2791
  if (m.query) {
2288
2792
  text = `[Executed Query: ${m.query}]
2289
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))}]`;
2290
2798
  }
2291
2799
  result.push(new AIMessage2(text));
2292
2800
  }
@@ -2305,7 +2813,8 @@ ${text}`;
2305
2813
  return recent.map((m) => {
2306
2814
  const prefix = m.role === "user" ? "User:" : "Assistant:";
2307
2815
  const q = m.query ? ` (Query: ${m.query})` : "";
2308
- return `${prefix} ${m.content}${q}`;
2816
+ const rc = typeof m.rowCount === "number" ? ` (${m.rowCount} rows)` : "";
2817
+ return `${prefix} ${m.content}${q}${rc}`;
2309
2818
  }).join("\n");
2310
2819
  }
2311
2820
  /**
@@ -2392,7 +2901,7 @@ var ReplSession = class {
2392
2901
  this.askQuestion.bind(this)
2393
2902
  );
2394
2903
  if (confirmResult.confirmed && this.currentSpinner) {
2395
- this.currentSpinner.text = chalk3.cyan("Executing query and analyzing...");
2904
+ this.currentSpinner.text = chalk4.cyan("Executing query and analyzing...");
2396
2905
  this.currentSpinner.start();
2397
2906
  }
2398
2907
  return confirmResult;
@@ -2411,7 +2920,7 @@ var ReplSession = class {
2411
2920
  return new Promise((resolve) => this.rl.question(query, resolve));
2412
2921
  }
2413
2922
  getPromptText() {
2414
- return chalk3.cyan(`sandal [${this.adapter.type}]> `);
2923
+ return chalk4.cyan(`sandal [${this.adapter.type}]> `);
2415
2924
  }
2416
2925
  async start() {
2417
2926
  this.setupSignalHandlers();
@@ -2425,8 +2934,8 @@ var ReplSession = class {
2425
2934
  const askQuestion = this.askQuestion.bind(this);
2426
2935
  while (this.isRunning) {
2427
2936
  process.stdin.resume();
2428
- const input2 = await askQuestion(this.getPromptText());
2429
- const trimmed = input2.trim();
2937
+ const input3 = await askQuestion(this.getPromptText());
2938
+ const trimmed = input3.trim();
2430
2939
  if (!trimmed) continue;
2431
2940
  if (["exit", "quit", ".exit", ".quit"].includes(trimmed.toLowerCase())) {
2432
2941
  break;
@@ -2449,12 +2958,12 @@ var ReplSession = class {
2449
2958
  continue;
2450
2959
  }
2451
2960
  if (trimmed === ".refresh") {
2452
- const spinner = ora(chalk3.blue("Refreshing schema cache...")).start();
2961
+ const spinner = ora(chalk4.blue("Refreshing schema cache...")).start();
2453
2962
  try {
2454
2963
  await this.adapter.inspectSchema(true);
2455
- spinner.succeed(chalk3.green("Schema refreshed successfully."));
2964
+ spinner.succeed(chalk4.green("Schema refreshed successfully."));
2456
2965
  } catch (err) {
2457
- spinner.fail(chalk3.red(`Failed to refresh schema: ${err.message}`));
2966
+ spinner.fail(chalk4.red(`Failed to refresh schema: ${err.message}`));
2458
2967
  }
2459
2968
  continue;
2460
2969
  }
@@ -2508,18 +3017,18 @@ var ReplSession = class {
2508
3017
  this.handleClearChat();
2509
3018
  continue;
2510
3019
  }
2511
- const intentSpinner = ora(chalk3.blue("Analyzing intent...")).start();
3020
+ const intentSpinner = ora(chalk4.blue("Analyzing intent...")).start();
2512
3021
  const historySummary = this.memoryManager.getRecentSummary(this.sessionId, 4);
2513
3022
  const intent = await classifyIntent(trimmed, this.model, historySummary);
2514
3023
  intentSpinner.stop();
2515
3024
  if (!intent.isDatabaseTask) {
2516
3025
  console.log(
2517
3026
  boxen2(
2518
- `${chalk3.bold.red("\u{1F6E1}\uFE0F GUARDRAIL ENFORCEMENT")}
3027
+ `${chalk4.bold.red("\u{1F6E1}\uFE0F GUARDRAIL ENFORCEMENT")}
2519
3028
 
2520
- ${chalk3.yellow(REFUSAL_MESSAGE)}
3029
+ ${chalk4.yellow(REFUSAL_MESSAGE)}
2521
3030
 
2522
- ${chalk3.gray(
3031
+ ${chalk4.gray(
2523
3032
  `Reason: ${intent.reason || "Non-database request rejected"}`
2524
3033
  )}`,
2525
3034
  {
@@ -2532,7 +3041,7 @@ ${chalk3.gray(
2532
3041
  );
2533
3042
  continue;
2534
3043
  }
2535
- const agentSpinner = ora(chalk3.cyan("Agent planning query...")).start();
3044
+ const agentSpinner = ora(chalk4.cyan("Agent thinking...")).start();
2536
3045
  this.currentSpinner = agentSpinner;
2537
3046
  try {
2538
3047
  const historyMessages = this.memoryManager.toLangChainMessages(this.sessionId, 6);
@@ -2550,15 +3059,15 @@ ${chalk3.gray(
2550
3059
  if (agentSpinner.isSpinning) {
2551
3060
  agentSpinner.stop();
2552
3061
  }
2553
- if (result.queryResult && result.queryResult.success && result.queryResult.rows) {
3062
+ if (result.queryExecuted && result.queryResult && result.queryResult.success && result.queryResult.rows) {
2554
3063
  this.renderRowsTable(result.queryResult);
2555
3064
  }
2556
3065
  if (result.conclusion) {
2557
- console.log("\n" + chalk3.green("\u{1F916} Answer:"));
3066
+ console.log("\n" + chalk4.green("\u{1F916} Answer:"));
2558
3067
  if (this.renderMarkdown) {
2559
3068
  console.log(renderMarkdown(result.conclusion) + "\n");
2560
3069
  } else {
2561
- console.log(chalk3.white(result.conclusion) + "\n");
3070
+ console.log(chalk4.white(result.conclusion) + "\n");
2562
3071
  }
2563
3072
  }
2564
3073
  this.memoryManager.addMessage(
@@ -2567,23 +3076,24 @@ ${chalk3.gray(
2567
3076
  this.adapter.connectionUrl
2568
3077
  );
2569
3078
  if (result.conclusion) {
2570
- const queryStr = result.generatedQuery?.sql || result.generatedQuery?.rawDisplay;
3079
+ const queryStr = result.queryExecuted ? result.generatedQuery?.sql || result.generatedQuery?.rawDisplay : void 0;
2571
3080
  this.memoryManager.addMessage(
2572
3081
  this.sessionId,
2573
3082
  {
2574
3083
  role: "assistant",
2575
3084
  content: result.conclusion,
2576
3085
  query: queryStr,
2577
- 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
2578
3088
  },
2579
3089
  this.adapter.connectionUrl
2580
3090
  );
2581
3091
  }
2582
3092
  } catch (err) {
2583
3093
  if (agentSpinner.isSpinning) {
2584
- agentSpinner.fail(chalk3.red(`Execution failed: ${err.message}`));
3094
+ agentSpinner.fail(chalk4.red(`Execution failed: ${err.message}`));
2585
3095
  } else {
2586
- console.error(chalk3.red(`
3096
+ console.error(chalk4.red(`
2587
3097
  Execution failed: ${err.message}
2588
3098
  `));
2589
3099
  }
@@ -2604,24 +3114,24 @@ Execution failed: ${err.message}
2604
3114
  let urlToConnect = targetUrl;
2605
3115
  if (!urlToConnect) {
2606
3116
  const saved = getSavedConnections();
2607
- console.log(chalk3.bold.cyan("\nDatabase Connections:"));
3117
+ console.log(chalk4.bold.cyan("\nDatabase Connections:"));
2608
3118
  if (saved.length === 0) {
2609
- console.log(chalk3.gray(" (No previous connections saved)"));
3119
+ console.log(chalk4.gray(" (No previous connections saved)"));
2610
3120
  } else {
2611
3121
  saved.forEach((url, i) => {
2612
3122
  const isCurrent = url === this.adapter.connectionUrl;
2613
- const currentTag = isCurrent ? chalk3.green(" (Current)") : "";
3123
+ const currentTag = isCurrent ? chalk4.green(" (Current)") : "";
2614
3124
  console.log(` [${i + 1}] ${maskUrl(url)}${currentTag}`);
2615
3125
  });
2616
3126
  }
2617
- console.log(chalk3.gray(" [N] Enter a new connection URL"));
2618
- console.log(chalk3.gray(" [C] Cancel\n"));
2619
- 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();
2620
3130
  if (!choice || choice.toLowerCase() === "c") {
2621
3131
  return;
2622
3132
  }
2623
3133
  if (choice.toLowerCase() === "n") {
2624
- const entered = (await ask(chalk3.cyan("Enter database connection URL: "))).trim();
3134
+ const entered = (await ask(chalk4.cyan("Enter database connection URL: "))).trim();
2625
3135
  if (!entered) return;
2626
3136
  urlToConnect = entered;
2627
3137
  } else {
@@ -2629,19 +3139,19 @@ Execution failed: ${err.message}
2629
3139
  if (!isNaN(num) && num >= 1 && num <= saved.length) {
2630
3140
  urlToConnect = saved[num - 1];
2631
3141
  } else {
2632
- console.log(chalk3.red("Invalid selection."));
3142
+ console.log(chalk4.red("Invalid selection."));
2633
3143
  return;
2634
3144
  }
2635
3145
  }
2636
3146
  }
2637
3147
  if (urlToConnect === this.adapter.connectionUrl && this.adapter.isConnected()) {
2638
- console.log(chalk3.yellow("\nAlready connected to this database.\n"));
3148
+ console.log(chalk4.yellow("\nAlready connected to this database.\n"));
2639
3149
  return;
2640
3150
  }
2641
3151
  try {
2642
3152
  detectDatabaseType(urlToConnect);
2643
3153
  } catch (e) {
2644
- console.log(chalk3.red(`Invalid connection URL: ${e.message}`));
3154
+ console.log(chalk4.red(`Invalid connection URL: ${e.message}`));
2645
3155
  return;
2646
3156
  }
2647
3157
  const spinner = ora(`Connecting to ${maskUrl(urlToConnect)}...`).start();
@@ -2657,55 +3167,54 @@ Execution failed: ${err.message}
2657
3167
  this.initAgent();
2658
3168
  addSavedConnection(urlToConnect);
2659
3169
  spinner.succeed(
2660
- chalk3.green(
3170
+ chalk4.green(
2661
3171
  `Switched to ${newAdapter.type.toUpperCase()} database: ${newAdapter.databaseName} (${newAdapter.getMaskedUrl()})`
2662
3172
  )
2663
3173
  );
2664
3174
  console.log(
2665
- chalk3.gray(
3175
+ chalk4.gray(
2666
3176
  `Chat session [${this.sessionId}] continuing with conversation memory on the new database.
2667
3177
  `
2668
3178
  )
2669
3179
  );
2670
3180
  } catch (err) {
2671
- spinner.fail(chalk3.red(`Failed to connect to database: ${err.message}`));
3181
+ spinner.fail(chalk4.red(`Failed to connect to database: ${err.message}`));
2672
3182
  }
2673
3183
  }
2674
3184
  async handleModelCommand(newModel, ask) {
2675
3185
  let targetModel = newModel;
2676
3186
  if (!targetModel) {
2677
3187
  console.log(
2678
- chalk3.bold.cyan(`
2679
- 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})`)
2680
3190
  );
2681
- console.log(chalk3.bold.yellow("Suggested models for " + this.provider + ":"));
2682
- const commonModels = {
2683
- google: ["gemini-2.5-flash", "gemini-2.5-pro", "gemini-2.0-flash"],
2684
- openai: ["gpt-4o", "gpt-4o-mini", "o3-mini"],
2685
- anthropic: [
2686
- "claude-3-5-sonnet-latest",
2687
- "claude-3-5-haiku-latest",
2688
- "claude-3-7-sonnet-latest"
2689
- ]
2690
- };
2691
- const list = commonModels[this.provider] || [];
3191
+ console.log(chalk4.bold.yellow(`
3192
+ Available models for ${this.provider.toUpperCase()}:`));
3193
+ const list = getModelsForProvider(this.provider);
2692
3194
  list.forEach((m, i) => {
2693
- const isCurrent = m === this.modelName ? chalk3.green(" (Current)") : "";
2694
- 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
+ );
2695
3204
  });
2696
- console.log(chalk3.gray(" [O] Other (type custom model name)"));
2697
- console.log(chalk3.gray(" [C] Cancel\n"));
2698
- const input2 = (await ask(chalk3.cyan("Select model [number, name, C]: "))).trim();
2699
- if (!input2 || input2.toLowerCase() === "c") return;
2700
- const num = parseInt(input2, 10);
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();
3208
+ if (!input3 || input3.toLowerCase() === "c") return;
3209
+ const num = parseInt(input3, 10);
2701
3210
  if (!isNaN(num) && num >= 1 && num <= list.length) {
2702
- targetModel = list[num - 1];
2703
- } else if (input2.toLowerCase() === "o") {
2704
- const custom = (await ask(chalk3.cyan("Enter model name: "))).trim();
3211
+ targetModel = list[num - 1].id;
3212
+ } else if (input3.toLowerCase() === "o") {
3213
+ const custom = (await ask(chalk4.cyan("Enter model name: "))).trim();
2705
3214
  if (!custom) return;
2706
3215
  targetModel = custom;
2707
3216
  } else {
2708
- targetModel = input2;
3217
+ targetModel = input3;
2709
3218
  }
2710
3219
  }
2711
3220
  if (!this.apiKey) {
@@ -2713,11 +3222,11 @@ Current Model: `) + chalk3.white(this.modelName) + chalk3.gray(` (${this.provide
2713
3222
  if (resolved.apiKey) {
2714
3223
  this.apiKey = resolved.apiKey;
2715
3224
  } else {
2716
- console.log(chalk3.yellow(`
3225
+ console.log(chalk4.yellow(`
2717
3226
  No API key found for ${this.provider.toUpperCase()}.`));
2718
- 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();
2719
3228
  if (!this.apiKey) {
2720
- console.log(chalk3.red("API key is required."));
3229
+ console.log(chalk4.red("API key is required."));
2721
3230
  return;
2722
3231
  }
2723
3232
  }
@@ -2730,83 +3239,128 @@ No API key found for ${this.provider.toUpperCase()}.`));
2730
3239
  });
2731
3240
  this.modelName = targetModel;
2732
3241
  this.initAgent();
2733
- console.log(chalk3.green(`
3242
+ console.log(chalk4.green(`
2734
3243
  Updated active model to: ${targetModel} [${this.provider}]
2735
3244
  `));
2736
3245
  } catch (err) {
2737
- console.log(chalk3.red(`
3246
+ console.log(chalk4.red(`
2738
3247
  Failed to update model: ${err.message}
2739
3248
  `));
2740
3249
  }
2741
3250
  }
2742
3251
  async handleProviderCommand(newProvider, ask) {
2743
- let p = newProvider.toLowerCase();
3252
+ let p = newProvider.toLowerCase().trim();
2744
3253
  if (!["google", "openai", "anthropic"].includes(p)) {
2745
- console.log(chalk3.bold.cyan(`
2746
- Current Provider: `) + chalk3.blue(this.provider));
3254
+ console.log(chalk4.bold.cyan(`
3255
+ Current Provider: `) + chalk4.blue(this.provider));
2747
3256
  console.log("Available providers:");
2748
3257
  console.log(" [1] Google Gemini (google)");
2749
3258
  console.log(" [2] OpenAI (openai)");
2750
3259
  console.log(" [3] Anthropic (anthropic)");
2751
3260
  console.log(" [C] Cancel\n");
2752
- 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();
2753
3262
  if (!ans || ans === "c") return;
2754
3263
  if (ans === "1" || ans === "google") p = "google";
2755
3264
  else if (ans === "2" || ans === "openai") p = "openai";
2756
3265
  else if (ans === "3" || ans === "anthropic") p = "anthropic";
2757
3266
  else {
2758
- console.log(chalk3.red("Invalid provider."));
3267
+ console.log(chalk4.red("Invalid provider."));
2759
3268
  return;
2760
3269
  }
2761
3270
  }
2762
- const resolved = resolveCredentials({ cliProvider: p });
2763
- let key = resolved.apiKey;
2764
- if (!key) {
2765
- console.log(chalk3.yellow(`
2766
- No API key found for ${p.toUpperCase()}.`));
2767
- key = (await ask(chalk3.cyan(`Enter API key for ${p.toUpperCase()}: `))).trim();
2768
- if (!key) {
2769
- console.log(chalk3.red("API key cannot be empty."));
2770
- 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;
2771
3285
  }
2772
- 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();
2773
3287
  if (save !== "n") {
2774
- if (p === "google") writeConfig({ geminiApiKey: key, defaultProvider: "google" });
2775
- else if (p === "openai") writeConfig({ openaiApiKey: key, defaultProvider: "openai" });
2776
- 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 });
2777
3291
  }
2778
3292
  }
2779
- 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
+ }
2780
3332
  try {
2781
3333
  this.model = createChatModel({
2782
- provider: p,
2783
- model: defaultModel,
3334
+ provider: typedProvider,
3335
+ model: targetModel,
2784
3336
  apiKey: key
2785
3337
  });
2786
- this.provider = p;
2787
- this.modelName = defaultModel;
3338
+ this.provider = typedProvider;
3339
+ this.modelName = targetModel;
2788
3340
  this.apiKey = key;
2789
3341
  this.initAgent();
2790
3342
  console.log(
2791
- chalk3.green(`
2792
- Switched provider to ${p.toUpperCase()} with model ${defaultModel}.
2793
- `)
3343
+ chalk4.green(
3344
+ `
3345
+ Switched provider to ${chalk4.bold(typedProvider.toUpperCase())} with model ${chalk4.bold(targetModel)}.
3346
+ `
3347
+ )
2794
3348
  );
2795
3349
  } catch (err) {
2796
- console.log(chalk3.red(`Failed to switch provider: ${err.message}
3350
+ console.log(chalk4.red(`Failed to switch provider: ${err.message}
2797
3351
  `));
2798
3352
  }
2799
3353
  }
2800
3354
  async handleKeyCommand(arg, ask) {
2801
3355
  const sub = arg.toLowerCase().trim();
2802
3356
  if (sub === "remove" || sub === "clear" || sub === "delete") {
2803
- 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();
2804
3358
  if (confirmAns === "y" || confirmAns === "yes") {
2805
3359
  removeApiKey(this.provider);
2806
3360
  this.apiKey = void 0;
2807
- console.log(chalk3.green(`
3361
+ console.log(chalk4.green(`
2808
3362
  Removed stored API key for "${this.provider}" from ~/.sandal/config.json.`));
2809
- 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.
2810
3364
  `));
2811
3365
  }
2812
3366
  return;
@@ -2814,10 +3368,10 @@ Removed stored API key for "${this.provider}" from ~/.sandal/config.json.`));
2814
3368
  if (sub.startsWith("set")) {
2815
3369
  let newKey = sub.replace(/^set\s*/, "").trim();
2816
3370
  if (!newKey) {
2817
- 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();
2818
3372
  }
2819
3373
  if (!newKey) {
2820
- console.log(chalk3.red("API key cannot be empty."));
3374
+ console.log(chalk4.red("API key cannot be empty."));
2821
3375
  return;
2822
3376
  }
2823
3377
  this.apiKey = newKey;
@@ -2830,46 +3384,46 @@ Removed stored API key for "${this.provider}" from ~/.sandal/config.json.`));
2830
3384
  apiKey: newKey
2831
3385
  });
2832
3386
  this.initAgent();
2833
- console.log(chalk3.green(`
3387
+ console.log(chalk4.green(`
2834
3388
  Updated API key for "${this.provider}" and refreshed model.
2835
3389
  `));
2836
3390
  return;
2837
3391
  }
2838
- console.log(chalk3.bold.cyan("\nAPI Key Status:"));
2839
- console.log(` Provider: ${chalk3.blue(this.provider)}`);
2840
- 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)}`);
2841
3395
  console.log(
2842
- ` 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)")}`
2843
3397
  );
2844
- console.log(chalk3.gray("\nUsage:"));
2845
- console.log(chalk3.gray(" .key remove - Remove stored key from ~/.sandal/config.json"));
2846
- 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"));
2847
3401
  }
2848
3402
  handleListChats() {
2849
3403
  const list = this.memoryManager.listSessions();
2850
3404
  if (list.length === 0) {
2851
- console.log(chalk3.yellow("\nNo saved chat sessions found.\n"));
3405
+ console.log(chalk4.yellow("\nNo saved chat sessions found.\n"));
2852
3406
  return;
2853
3407
  }
2854
- console.log(chalk3.bold.cyan("\nSaved Chat Sessions:"));
3408
+ console.log(chalk4.bold.cyan("\nSaved Chat Sessions:"));
2855
3409
  list.forEach((s, i) => {
2856
3410
  const isActive = s.id === this.sessionId;
2857
- const activeMarker = isActive ? chalk3.green(" [ACTIVE]") : "";
3411
+ const activeMarker = isActive ? chalk4.green(" [ACTIVE]") : "";
2858
3412
  const dateStr = new Date(s.updatedAt).toLocaleString();
2859
3413
  console.log(
2860
- ` [${i + 1}] ${chalk3.bold(s.title)}${activeMarker}
2861
- 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)}`
2862
3416
  );
2863
3417
  });
2864
- 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"));
2865
3419
  }
2866
3420
  handleNewChat() {
2867
3421
  const newSession = this.memoryManager.createSession("New Chat", this.adapter.connectionUrl);
2868
3422
  this.sessionId = newSession.id;
2869
3423
  this.initAgent();
2870
- console.log(chalk3.green(`
3424
+ console.log(chalk4.green(`
2871
3425
  Started fresh chat session: ${this.sessionId}`));
2872
- 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"));
2873
3427
  }
2874
3428
  handleSwitchChat(arg) {
2875
3429
  const list = this.memoryManager.listSessions();
@@ -2884,14 +3438,14 @@ Started fresh chat session: ${this.sessionId}`));
2884
3438
  if (found) targetId = found.id;
2885
3439
  }
2886
3440
  if (!targetId) {
2887
- console.log(chalk3.red(`
3441
+ console.log(chalk4.red(`
2888
3442
  Chat session "${arg}" not found. Use .chats to list sessions.
2889
3443
  `));
2890
3444
  return;
2891
3445
  }
2892
3446
  const session = this.memoryManager.getSession(targetId);
2893
3447
  if (!session) {
2894
- console.log(chalk3.red(`
3448
+ console.log(chalk4.red(`
2895
3449
  Could not load chat session "${targetId}".
2896
3450
  `));
2897
3451
  return;
@@ -2899,7 +3453,7 @@ Could not load chat session "${targetId}".
2899
3453
  this.sessionId = session.id;
2900
3454
  this.initAgent();
2901
3455
  console.log(
2902
- chalk3.green(
3456
+ chalk4.green(
2903
3457
  `
2904
3458
  Switched to chat: "${session.title}" (${session.messages.length} messages in memory).
2905
3459
  `
@@ -2909,42 +3463,42 @@ Switched to chat: "${session.title}" (${session.messages.length} messages in mem
2909
3463
  handleShowHistory() {
2910
3464
  const session = this.memoryManager.getSession(this.sessionId);
2911
3465
  if (!session || session.messages.length === 0) {
2912
- console.log(chalk3.yellow(`
3466
+ console.log(chalk4.yellow(`
2913
3467
  No message history in current chat (${this.sessionId}).
2914
3468
  `));
2915
3469
  return;
2916
3470
  }
2917
- console.log(chalk3.bold.cyan(`
3471
+ console.log(chalk4.bold.cyan(`
2918
3472
  Chat History: "${session.title}" [${this.sessionId}]:`));
2919
- console.log(chalk3.gray("\u2500".repeat(60)));
3473
+ console.log(chalk4.gray("\u2500".repeat(60)));
2920
3474
  for (const msg of session.messages) {
2921
3475
  const time = new Date(msg.timestamp).toLocaleTimeString();
2922
3476
  if (msg.role === "user") {
2923
- console.log(`${chalk3.bold.blue("\u{1F464} User")} ${chalk3.gray(`(${time})`)}:
3477
+ console.log(`${chalk4.bold.blue("\u{1F464} User")} ${chalk4.gray(`(${time})`)}:
2924
3478
  ${msg.content}`);
2925
3479
  } else if (msg.role === "assistant") {
2926
3480
  if (msg.query) {
2927
- console.log(` ${chalk3.gray(`[Query: ${msg.query}]`)}`);
3481
+ console.log(` ${chalk4.gray(`[Query: ${msg.query}]`)}`);
2928
3482
  }
2929
- console.log(`${chalk3.bold.green("\u{1F916} Assistant")} ${chalk3.gray(`(${time})`)}:
3483
+ console.log(`${chalk4.bold.green("\u{1F916} Assistant")} ${chalk4.gray(`(${time})`)}:
2930
3484
  ${msg.content}`);
2931
3485
  }
2932
- console.log(chalk3.gray("\u2500".repeat(60)));
3486
+ console.log(chalk4.gray("\u2500".repeat(60)));
2933
3487
  }
2934
3488
  console.log("");
2935
3489
  }
2936
3490
  handleClearChat() {
2937
3491
  this.memoryManager.clearSession(this.sessionId);
2938
3492
  this.initAgent();
2939
- console.log(chalk3.green(`
3493
+ console.log(chalk4.green(`
2940
3494
  Chat memory for session [${this.sessionId}] has been cleared.
2941
3495
  `));
2942
3496
  }
2943
3497
  handleRenderMarkdown(arg) {
2944
3498
  if (!arg) {
2945
- console.log(chalk3.yellow("Usage: .md <markdown text or file path>"));
2946
- console.log(chalk3.gray("Example: .md # Hello World"));
2947
- 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"));
2948
3502
  return;
2949
3503
  }
2950
3504
  try {
@@ -2960,21 +3514,21 @@ Chat memory for session [${this.sessionId}] has been cleared.
2960
3514
  renderRowsTable(queryResult) {
2961
3515
  const rows = queryResult.rows;
2962
3516
  if (!rows || rows.length === 0) {
2963
- console.log(chalk3.gray("(0 rows returned)"));
3517
+ console.log(chalk4.gray("(0 rows returned)"));
2964
3518
  return;
2965
3519
  }
2966
3520
  const fields = queryResult.fields && queryResult.fields.length > 0 ? queryResult.fields : Object.keys(rows[0] || {});
2967
3521
  if (fields.length === 0) return;
2968
3522
  const displayRows = rows.slice(0, 25);
2969
- const table = new Table({
2970
- head: fields.map((f) => chalk3.cyan.bold(f)),
3523
+ const table = new Table2({
3524
+ head: fields.map((f) => chalk4.cyan.bold(f)),
2971
3525
  style: { head: [], border: ["gray"] }
2972
3526
  });
2973
3527
  for (const r of displayRows) {
2974
3528
  table.push(
2975
3529
  fields.map((f) => {
2976
3530
  const val = r[f];
2977
- if (val === null || val === void 0) return chalk3.gray("NULL");
3531
+ if (val === null || val === void 0) return chalk4.gray("NULL");
2978
3532
  if (typeof val === "object") {
2979
3533
  try {
2980
3534
  return JSON.stringify(val);
@@ -2988,55 +3542,55 @@ Chat memory for session [${this.sessionId}] has been cleared.
2988
3542
  }
2989
3543
  console.log(table.toString());
2990
3544
  if (rows.length > 25) {
2991
- 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.`));
2992
3546
  }
2993
3547
  console.log(
2994
- chalk3.gray(
3548
+ chalk4.gray(
2995
3549
  `Total rows: ${queryResult.rowCount ?? rows.length} | Execution duration: ${queryResult.durationMs}ms
2996
3550
  `
2997
3551
  )
2998
3552
  );
2999
3553
  }
3000
3554
  async handleListEntities() {
3001
- const spinner = ora(chalk3.blue("Inspecting schema...")).start();
3555
+ const spinner = ora(chalk4.blue("Inspecting schema...")).start();
3002
3556
  try {
3003
3557
  const schema = await this.adapter.inspectSchema(false);
3004
3558
  spinner.stop();
3005
3559
  if (schema.type === "postgres") {
3006
3560
  const tables = schema.tables || [];
3007
3561
  if (tables.length === 0) {
3008
- console.log(chalk3.yellow("No tables found in public schema."));
3562
+ console.log(chalk4.yellow("No tables found in public schema."));
3009
3563
  return;
3010
3564
  }
3011
- console.log(chalk3.bold.cyan(`
3565
+ console.log(chalk4.bold.cyan(`
3012
3566
  Tables in "${schema.databaseName}":`));
3013
3567
  for (const t of tables) {
3014
3568
  console.log(
3015
- ` \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)`)}`
3016
3570
  );
3017
3571
  }
3018
3572
  console.log("");
3019
3573
  } else {
3020
3574
  const collections = schema.collections || [];
3021
3575
  if (collections.length === 0) {
3022
- console.log(chalk3.yellow("No collections found."));
3576
+ console.log(chalk4.yellow("No collections found."));
3023
3577
  return;
3024
3578
  }
3025
- console.log(chalk3.bold.cyan(`
3579
+ console.log(chalk4.bold.cyan(`
3026
3580
  Collections in "${schema.databaseName}":`));
3027
3581
  for (const c of collections) {
3028
3582
  console.log(
3029
- ` \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)`)}`
3030
3584
  );
3031
3585
  }
3032
3586
  console.log("");
3033
3587
  }
3034
3588
  } catch (err) {
3035
- spinner.fail(chalk3.red(`Failed to list entities: ${err.message}`));
3589
+ spinner.fail(chalk4.red(`Failed to list entities: ${err.message}`));
3036
3590
  }
3037
3591
  }
3038
3592
  async handleShowSchema(entityName) {
3039
- const spinner = ora(chalk3.blue("Fetching schema details...")).start();
3593
+ const spinner = ora(chalk4.blue("Fetching schema details...")).start();
3040
3594
  try {
3041
3595
  const schema = await this.adapter.inspectSchema(false);
3042
3596
  spinner.stop();
@@ -3049,14 +3603,14 @@ Collections in "${schema.databaseName}":`));
3049
3603
  (t) => t.name.toLowerCase() === entityName.toLowerCase()
3050
3604
  );
3051
3605
  if (!table) {
3052
- console.log(chalk3.red(`Table "${entityName}" not found.`));
3606
+ console.log(chalk4.red(`Table "${entityName}" not found.`));
3053
3607
  return;
3054
3608
  }
3055
- console.log(chalk3.bold.cyan(`
3609
+ console.log(chalk4.bold.cyan(`
3056
3610
  Schema for table: ${table.schema}.${table.name}`));
3057
- const tableDetails = new Table({
3611
+ const tableDetails = new Table2({
3058
3612
  head: ["Column", "Data Type", "Nullable", "Primary Key", "Default"].map(
3059
- (h) => chalk3.cyan.bold(h)
3613
+ (h) => chalk4.cyan.bold(h)
3060
3614
  )
3061
3615
  });
3062
3616
  for (const c of table.columns) {
@@ -3064,8 +3618,8 @@ Schema for table: ${table.schema}.${table.name}`));
3064
3618
  c.name,
3065
3619
  c.dataType,
3066
3620
  c.isNullable ? "YES" : "NO",
3067
- c.isPrimaryKey ? chalk3.green("YES") : "NO",
3068
- c.defaultValue ?? chalk3.gray("NULL")
3621
+ c.isPrimaryKey ? chalk4.green("YES") : "NO",
3622
+ c.defaultValue ?? chalk4.gray("NULL")
3069
3623
  ]);
3070
3624
  }
3071
3625
  console.log(tableDetails.toString());
@@ -3074,13 +3628,13 @@ Schema for table: ${table.schema}.${table.name}`));
3074
3628
  (c) => c.name.toLowerCase() === entityName.toLowerCase()
3075
3629
  );
3076
3630
  if (!collection) {
3077
- console.log(chalk3.red(`Collection "${entityName}" not found.`));
3631
+ console.log(chalk4.red(`Collection "${entityName}" not found.`));
3078
3632
  return;
3079
3633
  }
3080
- console.log(chalk3.bold.cyan(`
3634
+ console.log(chalk4.bold.cyan(`
3081
3635
  Schema for collection: ${collection.name}`));
3082
- const colDetails = new Table({
3083
- 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))
3084
3638
  });
3085
3639
  for (const f of collection.fields) {
3086
3640
  colDetails.push([f.name, f.types.join(", ")]);
@@ -3088,22 +3642,22 @@ Schema for collection: ${collection.name}`));
3088
3642
  console.log(colDetails.toString());
3089
3643
  }
3090
3644
  } catch (err) {
3091
- spinner.fail(chalk3.red(`Failed to inspect schema: ${err.message}`));
3645
+ spinner.fail(chalk4.red(`Failed to inspect schema: ${err.message}`));
3092
3646
  }
3093
3647
  }
3094
3648
  printWelcomeBanner() {
3095
3649
  const banner = [
3096
- chalk3.bold.cyan("SANDAL: Safe Agentic Natural-language Database Access Layer"),
3097
- 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)"),
3098
3652
  "",
3099
- `${chalk3.bold("Database:")} ${chalk3.green(this.adapter.type.toUpperCase())} (${this.adapter.getMaskedUrl()})`,
3100
- `${chalk3.bold("LLM Provider:")} ${chalk3.blue(this.provider)} [${chalk3.white(this.modelName)}]`,
3101
- `${chalk3.bold("Chat Session:")} ${chalk3.yellow(this.sessionId)}`,
3102
- `${chalk3.bold("Strict Mode:")} ${this.strictMode ? chalk3.green("ON (Full wipes blocked)") : chalk3.yellow("OFF")}`,
3103
- `${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")}`,
3104
3658
  "",
3105
- chalk3.gray("Type your natural language request or SQL/Mongo query."),
3106
- 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")
3107
3661
  ].join("\n");
3108
3662
  console.log(
3109
3663
  boxen2(banner, {
@@ -3118,26 +3672,26 @@ Schema for collection: ${collection.name}`));
3118
3672
  console.log(
3119
3673
  boxen2(
3120
3674
  [
3121
- chalk3.bold.cyan("SANDAL REPL Commands:"),
3675
+ chalk4.bold.cyan("SANDAL REPL Commands:"),
3122
3676
  "",
3123
- `${chalk3.bold(".tables / .collections")} List all tables or collections with row counts`,
3124
- `${chalk3.bold(".schema [name]")} Show columns, types, and indexes for a table`,
3125
- `${chalk3.bold(".connect / .switch")} Switch database connection (shows saved connections)`,
3126
- `${chalk3.bold(".model [name]")} Change active LLM model on the fly`,
3127
- `${chalk3.bold(".provider [name]")} Switch LLM provider (google, openai, anthropic)`,
3128
- `${chalk3.bold(".key [remove | set]")} View, remove, or update API key`,
3129
- `${chalk3.bold(".chats")} List saved chat sessions`,
3130
- `${chalk3.bold(".chat <id | num>")} Switch to another chat session`,
3131
- `${chalk3.bold(".new")} Start a fresh chat session with clear memory`,
3132
- `${chalk3.bold(".history")} Show conversation history for current chat`,
3133
- `${chalk3.bold(".clear-chat")} Clear memory for current chat`,
3134
- `${chalk3.bold(".md [file | text]")} Render markdown file or text in terminal`,
3135
- `${chalk3.bold(".refresh")} Force refresh cached schema introspection`,
3136
- `${chalk3.bold(".clear")} Clear terminal screen`,
3137
- `${chalk3.bold(".help")} Display this command reference`,
3138
- `${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`,
3139
3693
  "",
3140
- chalk3.bold.yellow("Example Natural Language Requests:"),
3694
+ chalk4.bold.yellow("Example Natural Language Requests:"),
3141
3695
  ' "Show top 10 users signed up in the last 30 days"',
3142
3696
  ' "Now count how many of them are active" (multi-turn follow-up)',
3143
3697
  ' "Calculate total revenue grouped by product category"',
@@ -3171,7 +3725,7 @@ Schema for collection: ${collection.name}`));
3171
3725
  } catch {
3172
3726
  }
3173
3727
  }
3174
- console.log(chalk3.cyan("\nDatabase connection closed. Goodbye! \u{1F44B}\n"));
3728
+ console.log(chalk4.cyan("\nDatabase connection closed. Goodbye! \u{1F44B}\n"));
3175
3729
  }
3176
3730
  };
3177
3731
 
@@ -3199,7 +3753,18 @@ program.name("sandal-db").description("SANDAL - Safe Agentic Natural-language Da
3199
3753
  "--markdown",
3200
3754
  "Render LLM answers formatted as terminal markdown (default: true)",
3201
3755
  true
3202
- ).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
+ }
3203
3768
  try {
3204
3769
  await runCli({
3205
3770
  cliDbUrl,
@@ -3209,10 +3774,11 @@ program.name("sandal-db").description("SANDAL - Safe Agentic Natural-language Da
3209
3774
  strictMode: options.strict !== false,
3210
3775
  allowFullWipe: Boolean(options.allowFullWipe),
3211
3776
  rowThreshold: parseInt(options.threshold, 10) || 50,
3212
- renderMarkdown: options.markdown !== false
3777
+ renderMarkdown: options.markdown !== false,
3778
+ selectModel: Boolean(options.selectModel)
3213
3779
  });
3214
3780
  } catch (err) {
3215
- console.error(chalk4.red(`
3781
+ console.error(chalk5.red(`
3216
3782
  Error: ${err.message}`));
3217
3783
  process.exit(1);
3218
3784
  }
@@ -3223,6 +3789,12 @@ var configCmd = program.command("config").description(
3223
3789
  "--set-provider <provider>",
3224
3790
  "Set default LLM provider (google | openai | anthropic)"
3225
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(
3226
3798
  "--set-markdown <boolean>",
3227
3799
  "Enable or disable default terminal markdown rendering (true | false)"
3228
3800
  ).option(
@@ -3237,7 +3809,22 @@ var configCmd = program.command("config").description(
3237
3809
  ).option(
3238
3810
  "--show",
3239
3811
  "Display current stored configuration (with masked secrets)"
3240
- ).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
+ }
3241
3828
  if (opts.path) {
3242
3829
  console.log(getConfigFilePath());
3243
3830
  return;
@@ -3245,9 +3832,9 @@ var configCmd = program.command("config").description(
3245
3832
  if (opts.connections) {
3246
3833
  const list = getSavedConnections();
3247
3834
  if (list.length === 0) {
3248
- console.log(chalk4.yellow("\nNo saved database connections found.\n"));
3835
+ console.log(chalk5.yellow("\nNo saved database connections found.\n"));
3249
3836
  } else {
3250
- console.log(chalk4.bold.cyan("\nSaved Database Connections:"));
3837
+ console.log(chalk5.bold.cyan("\nSaved Database Connections:"));
3251
3838
  list.forEach((url, i) => {
3252
3839
  console.log(` [${i + 1}] ${maskUrl(url)}`);
3253
3840
  });
@@ -3258,47 +3845,47 @@ var configCmd = program.command("config").description(
3258
3845
  if (opts.removeConnection) {
3259
3846
  const removed = removeSavedConnection(opts.removeConnection);
3260
3847
  if (removed) {
3261
- console.log(chalk4.green(`Removed connection from saved history.`));
3848
+ console.log(chalk5.green(`Removed connection from saved history.`));
3262
3849
  } else {
3263
- console.log(chalk4.red(`Connection "${opts.removeConnection}" not found in history.`));
3850
+ console.log(chalk5.red(`Connection "${opts.removeConnection}" not found in history.`));
3264
3851
  }
3265
3852
  return;
3266
3853
  }
3267
3854
  if (opts.removeKey !== void 0) {
3268
3855
  const prov = typeof opts.removeKey === "string" && opts.removeKey.trim() ? opts.removeKey.toLowerCase() : "all";
3269
3856
  removeApiKey(prov);
3270
- 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.`));
3271
3858
  return;
3272
3859
  }
3273
3860
  if (opts.show) {
3274
3861
  const cfg = readConfig();
3275
3862
  const saved = getSavedConnections();
3276
3863
  console.log(
3277
- chalk4.bold.cyan("\nSaved Configuration (~/.sandal/config.json):")
3864
+ chalk5.bold.cyan("\nSaved Configuration (~/.sandal/config.json):")
3278
3865
  );
3279
3866
  console.log(
3280
- ` 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")}`
3281
3868
  );
3282
3869
  console.log(
3283
- ` 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)")}`
3284
3871
  );
3285
3872
  console.log(
3286
- ` 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")}`
3287
3874
  );
3288
3875
  console.log(
3289
- ` Markdown Rendering: ${cfg.renderMarkdown !== false ? chalk4.green("enabled") : chalk4.yellow("disabled")}`
3876
+ ` Markdown Rendering: ${cfg.renderMarkdown !== false ? chalk5.green("enabled") : chalk5.yellow("disabled")}`
3290
3877
  );
3291
3878
  console.log(
3292
- ` 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")}`
3293
3880
  );
3294
3881
  console.log(
3295
- ` 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")}`
3296
3883
  );
3297
3884
  console.log(
3298
- ` 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")}`
3299
3886
  );
3300
3887
  console.log(
3301
- ` 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")}
3302
3889
  `
3303
3890
  );
3304
3891
  return;
@@ -3309,44 +3896,64 @@ var configCmd = program.command("config").description(
3309
3896
  updates.dbUrl = opts.setDb;
3310
3897
  addSavedConnection(opts.setDb);
3311
3898
  console.log(
3312
- chalk4.green(`Updated default database URL: ${maskUrl(opts.setDb)}`)
3899
+ chalk5.green(`Updated default database URL: ${maskUrl(opts.setDb)}`)
3313
3900
  );
3314
3901
  }
3315
3902
  if (opts.setProvider) {
3316
3903
  const p = opts.setProvider.toLowerCase();
3317
3904
  if (!["google", "openai", "anthropic"].includes(p)) {
3318
3905
  console.error(
3319
- chalk4.red("Invalid provider. Expected google, openai, or anthropic.")
3906
+ chalk5.red("Invalid provider. Expected google, openai, or anthropic.")
3320
3907
  );
3321
3908
  process.exit(1);
3322
3909
  }
3323
3910
  updates.defaultProvider = p;
3324
- 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
+ }
3325
3932
  }
3326
3933
  if (opts.setModel) {
3327
3934
  updates.defaultModel = opts.setModel;
3328
- console.log(chalk4.green(`Updated default model: ${opts.setModel}`));
3935
+ console.log(chalk5.green(`Updated default model: ${opts.setModel}`));
3329
3936
  }
3330
3937
  if (opts.setMarkdown !== void 0) {
3331
3938
  const val = opts.setMarkdown.toLowerCase() === "true" || opts.setMarkdown === "1" || opts.setMarkdown === "yes";
3332
3939
  updates.renderMarkdown = val;
3333
3940
  console.log(
3334
- chalk4.green(
3941
+ chalk5.green(
3335
3942
  `Updated default markdown rendering: ${val ? "enabled" : "disabled"}.`
3336
3943
  )
3337
3944
  );
3338
3945
  }
3339
3946
  if (opts.setGemini) {
3340
3947
  updates.geminiApiKey = opts.setGemini;
3341
- console.log(chalk4.green("Updated Gemini API key."));
3948
+ console.log(chalk5.green("Updated Gemini API key."));
3342
3949
  }
3343
3950
  if (opts.setOpenai) {
3344
3951
  updates.openaiApiKey = opts.setOpenai;
3345
- console.log(chalk4.green("Updated OpenAI API key."));
3952
+ console.log(chalk5.green("Updated OpenAI API key."));
3346
3953
  }
3347
3954
  if (opts.setAnthropic) {
3348
3955
  updates.anthropicApiKey = opts.setAnthropic;
3349
- console.log(chalk4.green("Updated Anthropic API key."));
3956
+ console.log(chalk5.green("Updated Anthropic API key."));
3350
3957
  }
3351
3958
  if (opts.setKey) {
3352
3959
  const current = readConfig();
@@ -3354,11 +3961,11 @@ var configCmd = program.command("config").description(
3354
3961
  if (prov === "google") updates.geminiApiKey = opts.setKey;
3355
3962
  else if (prov === "openai") updates.openaiApiKey = opts.setKey;
3356
3963
  else if (prov === "anthropic") updates.anthropicApiKey = opts.setKey;
3357
- console.log(chalk4.green(`Updated API key for provider "${prov}".`));
3964
+ console.log(chalk5.green(`Updated API key for provider "${prov}".`));
3358
3965
  }
3359
3966
  if (Object.keys(updates).length > 0) {
3360
3967
  writeConfig(updates);
3361
- console.log(chalk4.gray(`Saved to ${getConfigFilePath()}`));
3968
+ console.log(chalk5.gray(`Saved to ${getConfigFilePath()}`));
3362
3969
  } else {
3363
3970
  configCmd.help();
3364
3971
  }
@@ -3374,13 +3981,13 @@ program.command("markdown [fileOrText]").alias("md").description("Render markdow
3374
3981
  content = Buffer.concat(chunks).toString("utf-8");
3375
3982
  } else {
3376
3983
  console.log(
3377
- chalk4.yellow(
3984
+ chalk5.yellow(
3378
3985
  "\nUsage: sandal md <file | text> or pipe markdown to sandal md"
3379
3986
  )
3380
3987
  );
3381
- console.log(chalk4.gray("Example: sandal md README.md"));
3382
- console.log(chalk4.gray('Example: sandal md "# Hello World"'));
3383
- 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"));
3384
3991
  return;
3385
3992
  }
3386
3993
  } else {
@@ -3410,11 +4017,11 @@ async function runCli(opts) {
3410
4017
  let dbUrl = resolved.dbUrl;
3411
4018
  if (!dbUrl) {
3412
4019
  console.log(
3413
- chalk4.yellow(
4020
+ chalk5.yellow(
3414
4021
  "\nNo database connection URL found in CLI flags, environment, or config."
3415
4022
  )
3416
4023
  );
3417
- dbUrl = await input({
4024
+ dbUrl = await input2({
3418
4025
  message: "Enter your database URL (postgres://... or mongodb://...):",
3419
4026
  validate: (val) => {
3420
4027
  try {
@@ -3440,34 +4047,55 @@ async function runCli(opts) {
3440
4047
  }
3441
4048
  let provider = resolved.provider;
3442
4049
  let apiKey = resolved.apiKey;
4050
+ const isProviderExplicitlyChanged = Boolean(
4051
+ opts.cliProvider && opts.cliProvider.toLowerCase() !== config.defaultProvider
4052
+ );
3443
4053
  if (!apiKey) {
3444
- console.log(chalk4.yellow("\nNo API key found in environment or config."));
3445
- provider = await select({
3446
- message: "Select your LLM provider:",
3447
- choices: [
3448
- { name: "Google Gemini (GEMINI_API_KEY)", value: "google" },
3449
- { name: "OpenAI (OPENAI_API_KEY)", value: "openai" },
3450
- { name: "Anthropic (ANTHROPIC_API_KEY)", value: "anthropic" }
3451
- ]
3452
- });
3453
- apiKey = await input({
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
+ }
4070
+ apiKey = await input2({
3454
4071
  message: `Enter your ${provider.toUpperCase()} API key:`,
3455
4072
  validate: (val) => val.trim().length > 0 ? true : "API key cannot be empty."
3456
4073
  });
4074
+ let chosenModel = opts.cliModel;
4075
+ if (!chosenModel) {
4076
+ chosenModel = await promptModelSelection(provider);
4077
+ }
3457
4078
  const saveKey = await confirm({
3458
- message: "Save this API key to ~/.sandal/config.json?",
4079
+ message: "Save this API key and default model to ~/.sandal/config.json?",
3459
4080
  default: true
3460
4081
  });
3461
4082
  if (saveKey) {
3462
- if (provider === "google")
3463
- writeConfig({ geminiApiKey: apiKey, defaultProvider: "google" });
3464
- else if (provider === "openai")
3465
- writeConfig({ openaiApiKey: apiKey, defaultProvider: "openai" });
3466
- else if (provider === "anthropic")
3467
- 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);
3468
4091
  }
3469
4092
  }
3470
- 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
+ }
3471
4099
  const dbSpinner = ora2(
3472
4100
  `Connecting to database (${maskUrl(dbUrl)})...`
3473
4101
  ).start();
@@ -3475,13 +4103,13 @@ async function runCli(opts) {
3475
4103
  try {
3476
4104
  await adapter.connect();
3477
4105
  dbSpinner.succeed(
3478
- chalk4.green(
4106
+ chalk5.green(
3479
4107
  `Connected to ${adapter.type.toUpperCase()} database: ${adapter.databaseName}`
3480
4108
  )
3481
4109
  );
3482
4110
  addSavedConnection(dbUrl);
3483
4111
  } catch (err) {
3484
- dbSpinner.fail(chalk4.red(`Failed to connect to database: ${err.message}`));
4112
+ dbSpinner.fail(chalk5.red(`Failed to connect to database: ${err.message}`));
3485
4113
  process.exit(1);
3486
4114
  }
3487
4115
  const model = createChatModel({