sandal-db 1.0.3 → 1.0.5

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 (3) hide show
  1. package/README.md +2 -0
  2. package/dist/cli.js +983 -298
  3. 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);
@@ -935,42 +1220,99 @@ function createDatabaseAdapter(url) {
935
1220
  import { ChatGoogleGenerativeAI } from "@langchain/google-genai";
936
1221
  import { ChatOpenAI } from "@langchain/openai";
937
1222
  import { ChatAnthropic } from "@langchain/anthropic";
938
- function createChatModel(options) {
939
- const { provider, model, apiKey, temperature = 0 } = options;
1223
+ function isReasoningOrFixedTempModel(provider, model) {
1224
+ const normalized = model.toLowerCase().replace(/^models\//, "").trim();
940
1225
  switch (provider) {
941
1226
  case "google":
942
- return new ChatGoogleGenerativeAI({
1227
+ return normalized.includes("thinking") || normalized.includes("reasoning") || /^gemini-([3-9]|\d{2,}|2\.[5-9])/i.test(normalized);
1228
+ case "openai":
1229
+ return /^o[1-9]/i.test(normalized) || normalized.includes("reasoning") || normalized.includes("thinking");
1230
+ case "anthropic":
1231
+ return normalized.includes("thinking") || normalized.includes("reasoning");
1232
+ default:
1233
+ return false;
1234
+ }
1235
+ }
1236
+ function resolveTemperature(provider, model, requestedTemperature) {
1237
+ if (isReasoningOrFixedTempModel(provider, model)) {
1238
+ return void 0;
1239
+ }
1240
+ return requestedTemperature ?? 0;
1241
+ }
1242
+ function attachTemperatureFallback(model) {
1243
+ const originalInvoke = model.invoke.bind(model);
1244
+ model.invoke = (async (input3, options) => {
1245
+ try {
1246
+ return await originalInvoke(input3, options);
1247
+ } catch (err) {
1248
+ const errMsg = err?.message || String(err);
1249
+ const isTempError = errMsg.toLowerCase().includes("temperature") && (errMsg.toLowerCase().includes("unsupported") || errMsg.toLowerCase().includes("does not support") || errMsg.toLowerCase().includes("not support"));
1250
+ if (isTempError) {
1251
+ model.temperature = void 0;
1252
+ if (model.client?.generationConfig) {
1253
+ delete model.client.generationConfig.temperature;
1254
+ }
1255
+ return await originalInvoke(input3, options);
1256
+ }
1257
+ throw err;
1258
+ }
1259
+ });
1260
+ return model;
1261
+ }
1262
+ function createChatModel(options) {
1263
+ const { provider, model, apiKey, temperature: requestedTemp } = options;
1264
+ const temperature = resolveTemperature(provider, model, requestedTemp);
1265
+ let chatModel;
1266
+ switch (provider) {
1267
+ case "google": {
1268
+ const config = {
943
1269
  model,
944
1270
  apiKey,
945
- temperature,
946
1271
  maxRetries: 2
947
- });
948
- case "openai":
949
- return new ChatOpenAI({
1272
+ };
1273
+ if (temperature !== void 0) {
1274
+ config.temperature = temperature;
1275
+ }
1276
+ chatModel = new ChatGoogleGenerativeAI(config);
1277
+ break;
1278
+ }
1279
+ case "openai": {
1280
+ const config = {
950
1281
  modelName: model,
951
1282
  apiKey,
952
- temperature,
953
1283
  maxRetries: 2
954
- });
955
- case "anthropic":
956
- return new ChatAnthropic({
1284
+ };
1285
+ if (temperature !== void 0) {
1286
+ config.temperature = temperature;
1287
+ }
1288
+ chatModel = new ChatOpenAI(config);
1289
+ break;
1290
+ }
1291
+ case "anthropic": {
1292
+ const config = {
957
1293
  modelName: model,
958
1294
  anthropicApiKey: apiKey,
959
- temperature,
960
1295
  maxRetries: 2
961
- });
1296
+ };
1297
+ if (temperature !== void 0) {
1298
+ config.temperature = temperature;
1299
+ }
1300
+ chatModel = new ChatAnthropic(config);
1301
+ break;
1302
+ }
962
1303
  default:
963
1304
  throw new Error(`Unsupported LLM provider: ${provider}`);
964
1305
  }
1306
+ return attachTemperatureFallback(chatModel);
965
1307
  }
966
1308
 
967
1309
  // src/repl.ts
968
1310
  import readline2 from "node:readline";
969
1311
  import fs3 from "node:fs";
970
- import chalk3 from "chalk";
1312
+ import chalk4 from "chalk";
971
1313
  import boxen2 from "boxen";
972
1314
  import ora from "ora";
973
- import Table from "cli-table3";
1315
+ import Table2 from "cli-table3";
974
1316
 
975
1317
  // src/agent/graph.ts
976
1318
  import { Annotation, StateGraph, START, END, MemorySaver } from "@langchain/langgraph";
@@ -1383,7 +1725,7 @@ function classifyMongoQuery(rawText, query, rowThreshold, options) {
1383
1725
 
1384
1726
  // src/safety/confirm.ts
1385
1727
  import readline from "node:readline";
1386
- import chalk from "chalk";
1728
+ import chalk2 from "chalk";
1387
1729
  import boxen from "boxen";
1388
1730
  async function defaultAsk(promptText) {
1389
1731
  process.stdin.resume();
@@ -1400,7 +1742,7 @@ async function defaultAsk(promptText) {
1400
1742
  });
1401
1743
  }
1402
1744
  async function promptConfirm(message, defaultValue, ask = defaultAsk) {
1403
- const suffix = defaultValue ? chalk.gray(" (Y/n): ") : chalk.gray(" (y/N): ");
1745
+ const suffix = defaultValue ? chalk2.gray(" (Y/n): ") : chalk2.gray(" (y/N): ");
1404
1746
  const response = (await ask(`${message}${suffix}`)).trim().toLowerCase();
1405
1747
  if (!response) {
1406
1748
  return defaultValue;
@@ -1421,12 +1763,12 @@ async function requestUserConfirmation(query, safety, affectedRows, ask = defaul
1421
1763
  const queryDisplay = (query.sql || query.rawDisplay || "").trim();
1422
1764
  if (safety.isFullWipe) {
1423
1765
  const boxContent = [
1424
- chalk.bold.red("\u{1F6A8} CRITICAL: FULL DATABASE / ALL TABLES WIPE ATTEMPTED \u{1F6A8}"),
1766
+ chalk2.bold.red("\u{1F6A8} CRITICAL: FULL DATABASE / ALL TABLES WIPE ATTEMPTED \u{1F6A8}"),
1425
1767
  "",
1426
- chalk.yellow(queryDisplay),
1768
+ chalk2.yellow(queryDisplay),
1427
1769
  "",
1428
- ...safety.warnings.length ? [chalk.red(safety.warnings.join("\n")), ""] : [],
1429
- chalk.bold.white(`Type ${chalk.red.underline(safety.literalWord || "DROP DATABASE")} to proceed:`)
1770
+ ...safety.warnings.length ? [chalk2.red(safety.warnings.join("\n")), ""] : [],
1771
+ chalk2.bold.white(`Type ${chalk2.red.underline(safety.literalWord || "DROP DATABASE")} to proceed:`)
1430
1772
  ].join("\n");
1431
1773
  console.log(
1432
1774
  boxen(boxContent, {
@@ -1437,7 +1779,7 @@ async function requestUserConfirmation(query, safety, affectedRows, ask = defaul
1437
1779
  })
1438
1780
  );
1439
1781
  const typed = await promptInput(
1440
- chalk.red.bold(`Type "${safety.literalWord || "DROP DATABASE"}" to confirm full wipe: `),
1782
+ chalk2.red.bold(`Type "${safety.literalWord || "DROP DATABASE"}" to confirm full wipe: `),
1441
1783
  ask
1442
1784
  );
1443
1785
  if (typed.trim() === safety.literalWord) {
@@ -1450,20 +1792,20 @@ async function requestUserConfirmation(query, safety, affectedRows, ask = defaul
1450
1792
  }
1451
1793
  if (safety.category === "dangerous") {
1452
1794
  const lines2 = [
1453
- chalk.bold.hex("#FFA500")("\u26A0\uFE0F DANGEROUS OPERATION"),
1795
+ chalk2.bold.hex("#FFA500")("\u26A0\uFE0F DANGEROUS OPERATION"),
1454
1796
  "",
1455
- chalk.white(queryDisplay),
1797
+ chalk2.white(queryDisplay),
1456
1798
  ""
1457
1799
  ];
1458
1800
  if (affectedRows !== null) {
1459
1801
  lines2.push(
1460
- chalk.bold.yellow(`Affected rows: ${affectedRows.toLocaleString()}`),
1802
+ chalk2.bold.yellow(`Affected rows: ${affectedRows.toLocaleString()}`),
1461
1803
  ""
1462
1804
  );
1463
1805
  }
1464
1806
  if (safety.warnings.length > 0) {
1465
1807
  for (const w of safety.warnings) {
1466
- lines2.push(chalk.red(`\u2022 ${w}`));
1808
+ lines2.push(chalk2.red(`\u2022 ${w}`));
1467
1809
  }
1468
1810
  lines2.push("");
1469
1811
  }
@@ -1477,7 +1819,7 @@ async function requestUserConfirmation(query, safety, affectedRows, ask = defaul
1477
1819
  );
1478
1820
  if (safety.requiresLiteralWord && safety.literalWord) {
1479
1821
  const typed = await promptInput(
1480
- chalk.hex("#FFA500").bold(
1822
+ chalk2.hex("#FFA500").bold(
1481
1823
  `Destructive operation with no filter. Type "${safety.literalWord}" to confirm: `
1482
1824
  ),
1483
1825
  ask
@@ -1491,7 +1833,7 @@ async function requestUserConfirmation(query, safety, affectedRows, ask = defaul
1491
1833
  };
1492
1834
  }
1493
1835
  const answer2 = await promptConfirm(
1494
- chalk.hex("#FFA500").bold("Proceed with dangerous operation?"),
1836
+ chalk2.hex("#FFA500").bold("Proceed with dangerous operation?"),
1495
1837
  false,
1496
1838
  ask
1497
1839
  );
@@ -1499,23 +1841,23 @@ async function requestUserConfirmation(query, safety, affectedRows, ask = defaul
1499
1841
  }
1500
1842
  if (safety.isStructural) {
1501
1843
  const lines2 = [
1502
- chalk.bold.cyan("\u{1F6E0}\uFE0F STRUCTURAL DDL OPERATION"),
1844
+ chalk2.bold.cyan("\u{1F6E0}\uFE0F STRUCTURAL DDL OPERATION"),
1503
1845
  "",
1504
- chalk.white(queryDisplay),
1846
+ chalk2.white(queryDisplay),
1505
1847
  "",
1506
- chalk.cyan(`Effect: ${safety.explanation || "Modifies database structure."}`)
1848
+ chalk2.cyan(`Effect: ${safety.explanation || "Modifies database structure."}`)
1507
1849
  ];
1508
1850
  if (safety.suggestedAlternative) {
1509
1851
  lines2.push(
1510
1852
  "",
1511
- chalk.yellow("\u{1F4A1} Recommendation:"),
1512
- chalk.gray(safety.suggestedAlternative)
1853
+ chalk2.yellow("\u{1F4A1} Recommendation:"),
1854
+ chalk2.gray(safety.suggestedAlternative)
1513
1855
  );
1514
1856
  }
1515
1857
  if (safety.warnings.length > 0) {
1516
1858
  lines2.push("");
1517
1859
  for (const w of safety.warnings) {
1518
- lines2.push(chalk.yellow(`\u2022 ${w}`));
1860
+ lines2.push(chalk2.yellow(`\u2022 ${w}`));
1519
1861
  }
1520
1862
  }
1521
1863
  console.log(
@@ -1527,7 +1869,7 @@ async function requestUserConfirmation(query, safety, affectedRows, ask = defaul
1527
1869
  })
1528
1870
  );
1529
1871
  const answer2 = await promptConfirm(
1530
- chalk.cyan.bold("Apply this structural change?"),
1872
+ chalk2.cyan.bold("Apply this structural change?"),
1531
1873
  true,
1532
1874
  ask
1533
1875
  );
@@ -1535,12 +1877,12 @@ async function requestUserConfirmation(query, safety, affectedRows, ask = defaul
1535
1877
  }
1536
1878
  if (safety.category === "write") {
1537
1879
  const lines2 = [
1538
- chalk.bold.magenta("\u270F\uFE0F DATABASE WRITE OPERATION"),
1880
+ chalk2.bold.magenta("\u270F\uFE0F DATABASE WRITE OPERATION"),
1539
1881
  "",
1540
- chalk.white(queryDisplay)
1882
+ chalk2.white(queryDisplay)
1541
1883
  ];
1542
1884
  if (affectedRows !== null) {
1543
- lines2.push("", chalk.magenta(`Estimated affected rows: ${affectedRows.toLocaleString()}`));
1885
+ lines2.push("", chalk2.magenta(`Estimated affected rows: ${affectedRows.toLocaleString()}`));
1544
1886
  }
1545
1887
  console.log(
1546
1888
  boxen(lines2.join("\n"), {
@@ -1551,16 +1893,16 @@ async function requestUserConfirmation(query, safety, affectedRows, ask = defaul
1551
1893
  })
1552
1894
  );
1553
1895
  const answer2 = await promptConfirm(
1554
- chalk.magenta.bold("Execute write query?"),
1896
+ chalk2.magenta.bold("Execute write query?"),
1555
1897
  false,
1556
1898
  ask
1557
1899
  );
1558
1900
  return { confirmed: answer2 };
1559
1901
  }
1560
1902
  const lines = [
1561
- chalk.bold.green("\u{1F50D} READ QUERY"),
1903
+ chalk2.bold.green("\u{1F50D} READ QUERY"),
1562
1904
  "",
1563
- chalk.white(queryDisplay)
1905
+ chalk2.white(queryDisplay)
1564
1906
  ];
1565
1907
  console.log(
1566
1908
  boxen(lines.join("\n"), {
@@ -1571,7 +1913,7 @@ async function requestUserConfirmation(query, safety, affectedRows, ask = defaul
1571
1913
  })
1572
1914
  );
1573
1915
  const answer = await promptConfirm(
1574
- chalk.green.bold("Execute read query?"),
1916
+ chalk2.green.bold("Execute read query?"),
1575
1917
  true,
1576
1918
  ask
1577
1919
  );
@@ -1704,10 +2046,35 @@ var OBVIOUS_DATABASE_PATTERNS = [
1704
2046
  /\b(?:table|tables|collection|collections|schema|column|columns|index|indexes|foreign key|primary key)\b/i,
1705
2047
  /\b(?:find|aggregate|count|where|group by|order by|having|limit|join|left join|inner join)\b/i,
1706
2048
  /\b(?:how many (?:users|rows|records|orders|items|products|documents))\b/i,
1707
- /\b(?:database|postgres|mongodb|mongo|pg_)\b/i
2049
+ /\b(?:database|postgres|mongodb|mongo|pg_)\b/i,
2050
+ /\b(?:query|queries|sql|result|results|rows?|records?)\b/i
1708
2051
  ];
1709
- async function classifyIntent(input2, model, historyContext) {
1710
- const trimmed = input2.trim();
2052
+ function isExplicitNoQuery(input3) {
2053
+ const trimmed = input3.trim();
2054
+ const patterns = [
2055
+ // don't / do not / shouldn't / stop / avoid running / executing / calling / issuing query / queries / sql / command
2056
+ /\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,
2057
+ // without / no running / executing / making queries
2058
+ /\b(?:without|no)\s+(?:running|executing|making|issuing|calling)\s+(?:any\s+|the\s+|a\s+|new\s+)?(?:query|queries|sql|command)\b/i,
2059
+ // no query / no queries / skip the query / skip execution
2060
+ /\b(?:no\s+query|no\s+queries|skip\s+(?:the\s+)?query|skip\s+execution)\b/i,
2061
+ // don't query / do not query (the database)
2062
+ /\b(?:don'?t|do\s+not|never)\s+query(?:\s+(?:the\s+database|the\s+db|again))?\b/i,
2063
+ // don't / do not run / execute it
2064
+ /\b(?:don'?t|do\s+not|never)\s+(?:run|execute)\s+it\b/i,
2065
+ // don't execute anything / don't run anything
2066
+ /\b(?:don'?t|do\s+not|never)\s+(?:run|execute)\s+anything\b/i,
2067
+ // just use / take / look at / do something with the previous / last / prior results / output / rows
2068
+ /\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,
2069
+ // from / using / with the previous results ... don't run / do not run / without querying
2070
+ /\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,
2071
+ // don't run ... previous results
2072
+ /\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
2073
+ ];
2074
+ return patterns.some((pattern) => pattern.test(trimmed));
2075
+ }
2076
+ async function classifyIntent(input3, model, historyContext) {
2077
+ const trimmed = input3.trim();
1711
2078
  for (const pattern of OBVIOUS_OUT_OF_SCOPE_PATTERNS) {
1712
2079
  if (pattern.test(trimmed)) {
1713
2080
  return {
@@ -1716,6 +2083,9 @@ async function classifyIntent(input2, model, historyContext) {
1716
2083
  };
1717
2084
  }
1718
2085
  }
2086
+ if (isExplicitNoQuery(trimmed)) {
2087
+ return { isDatabaseTask: true };
2088
+ }
1719
2089
  for (const pattern of OBVIOUS_DATABASE_PATTERNS) {
1720
2090
  if (pattern.test(trimmed)) {
1721
2091
  return {
@@ -1726,7 +2096,9 @@ async function classifyIntent(input2, model, historyContext) {
1726
2096
  if (/^(?:tables|collections|schema|indexes|views|stats|count|info)$/i.test(trimmed)) {
1727
2097
  return { isDatabaseTask: true };
1728
2098
  }
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)) {
2099
+ 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(
2100
+ trimmed
2101
+ )) {
1730
2102
  return { isDatabaseTask: true };
1731
2103
  }
1732
2104
  if (!model) {
@@ -1791,10 +2163,22 @@ var AgentStateAnnotation = Annotation.Root({
1791
2163
  reducer: (_, update) => update ?? [],
1792
2164
  default: () => []
1793
2165
  }),
2166
+ requiresQuery: Annotation({
2167
+ reducer: (_, update) => update ?? true,
2168
+ default: () => true
2169
+ }),
2170
+ queryExecuted: Annotation({
2171
+ reducer: (_, update) => update ?? false,
2172
+ default: () => false
2173
+ }),
1794
2174
  generatedQuery: Annotation({
1795
2175
  reducer: (_, update) => update,
1796
2176
  default: () => void 0
1797
2177
  }),
2178
+ lastGeneratedQuery: Annotation({
2179
+ reducer: (curr, update) => update ?? curr,
2180
+ default: () => void 0
2181
+ }),
1798
2182
  safety: Annotation({
1799
2183
  reducer: (_, update) => update,
1800
2184
  default: () => void 0
@@ -1815,6 +2199,10 @@ var AgentStateAnnotation = Annotation.Root({
1815
2199
  reducer: (_, update) => update,
1816
2200
  default: () => void 0
1817
2201
  }),
2202
+ lastQueryResult: Annotation({
2203
+ reducer: (curr, update) => update ?? curr,
2204
+ default: () => void 0
2205
+ }),
1818
2206
  analysis: Annotation({
1819
2207
  reducer: (_, update) => update,
1820
2208
  default: () => void 0
@@ -1823,6 +2211,10 @@ var AgentStateAnnotation = Annotation.Root({
1823
2211
  reducer: (_, update) => update,
1824
2212
  default: () => void 0
1825
2213
  }),
2214
+ lastStats: Annotation({
2215
+ reducer: (curr, update) => update ?? curr,
2216
+ default: () => void 0
2217
+ }),
1826
2218
  conclusion: Annotation({
1827
2219
  reducer: (_, update) => update,
1828
2220
  default: () => void 0
@@ -1832,7 +2224,7 @@ var AgentStateAnnotation = Annotation.Root({
1832
2224
  default: () => false
1833
2225
  }),
1834
2226
  messages: Annotation({
1835
- reducer: (curr, update) => curr.concat(update ?? []),
2227
+ reducer: (curr, update) => update !== void 0 ? update : curr,
1836
2228
  default: () => []
1837
2229
  })
1838
2230
  });
@@ -1851,6 +2243,94 @@ function createDatabaseAgent(config) {
1851
2243
  requiresSchemaRefresh: false
1852
2244
  };
1853
2245
  }
2246
+ async function determineActionNode(state) {
2247
+ const userInput = state.userInput.trim();
2248
+ const baseReset = {
2249
+ queryExecuted: false,
2250
+ generatedQuery: void 0,
2251
+ queryResult: void 0,
2252
+ safety: void 0,
2253
+ confirmed: false,
2254
+ cancelReason: void 0,
2255
+ conclusion: void 0
2256
+ };
2257
+ if (isExplicitNoQuery(userInput)) {
2258
+ return {
2259
+ ...baseReset,
2260
+ requiresQuery: false
2261
+ };
2262
+ }
2263
+ 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);
2264
+ if (isObviousLiveQuery) {
2265
+ return {
2266
+ ...baseReset,
2267
+ requiresQuery: true
2268
+ };
2269
+ }
2270
+ const hasPreviousResults = Boolean(
2271
+ state.lastQueryResult && state.lastQueryResult.rows && state.lastQueryResult.rows.length > 0
2272
+ );
2273
+ 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);
2274
+ if (isOperatingOnPreviousResults) {
2275
+ return {
2276
+ ...baseReset,
2277
+ requiresQuery: false
2278
+ };
2279
+ }
2280
+ 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);
2281
+ if (isSchemaOrMetaQuestion && (state.schemaSummary || state.messages.length > 0)) {
2282
+ return {
2283
+ ...baseReset,
2284
+ requiresQuery: false
2285
+ };
2286
+ }
2287
+ const historyMessages = state.messages.slice(-4);
2288
+ const prevQueryStr = state.lastGeneratedQuery?.sql || state.lastGeneratedQuery?.rawDisplay || "None";
2289
+ const prevRowsCount = state.lastQueryResult?.rowCount ?? state.lastQueryResult?.rows?.length ?? 0;
2290
+ 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.
2291
+
2292
+ User Request: "${userInput}"
2293
+
2294
+ Recent Context:
2295
+ - Previous Query: ${prevQueryStr}
2296
+ - Previous Rows Returned: ${prevRowsCount}
2297
+ - Schema Summary Available: ${state.schemaSummary ? "Yes" : "No"}
2298
+
2299
+ Rules:
2300
+ 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.
2301
+ 2. If the user asks to format, summarize, filter, inspect, or calculate based on the previous results or conversation context, requiresQuery MUST be false.
2302
+ 3. If the user asks about the schema structure, columns, or general knowledge answerable from context, requiresQuery MUST be false.
2303
+ 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.
2304
+
2305
+ Respond with JSON only:
2306
+ {"requiresQuery": boolean, "reason": "<short explanation>"}`;
2307
+ try {
2308
+ const response = await model.invoke([
2309
+ ...historyMessages,
2310
+ new HumanMessage2(routerPrompt)
2311
+ ]);
2312
+ const content = typeof response.content === "string" ? response.content.trim() : "";
2313
+ const jsonMatch = content.match(/\{[\s\S]*?\}/);
2314
+ if (jsonMatch) {
2315
+ const parsed = JSON.parse(jsonMatch[0]);
2316
+ if (typeof parsed.requiresQuery === "boolean") {
2317
+ return {
2318
+ ...baseReset,
2319
+ requiresQuery: parsed.requiresQuery
2320
+ };
2321
+ }
2322
+ }
2323
+ return {
2324
+ ...baseReset,
2325
+ requiresQuery: true
2326
+ };
2327
+ } catch {
2328
+ return {
2329
+ ...baseReset,
2330
+ requiresQuery: true
2331
+ };
2332
+ }
2333
+ }
1854
2334
  async function identifyTargetsNode(state) {
1855
2335
  const historyMessages = state.messages.slice(-4);
1856
2336
  const prompt = `Based on this user request: "${state.userInput}"
@@ -2022,7 +2502,8 @@ Do not wrap with markdown or code fences.`;
2022
2502
  const conclusion = state.cancelReason || "Operation was not executed.";
2023
2503
  return {
2024
2504
  conclusion,
2025
- messages: [new HumanMessage2(state.userInput), new AIMessage(conclusion)]
2505
+ queryExecuted: false,
2506
+ messages: [...state.messages, new HumanMessage2(state.userInput), new AIMessage(conclusion)]
2026
2507
  };
2027
2508
  }
2028
2509
  const res = state.queryResult;
@@ -2030,14 +2511,18 @@ Do not wrap with markdown or code fences.`;
2030
2511
  const conclusion = "No query was executed.";
2031
2512
  return {
2032
2513
  conclusion,
2033
- messages: [new HumanMessage2(state.userInput), new AIMessage(conclusion)]
2514
+ queryExecuted: false,
2515
+ messages: [...state.messages, new HumanMessage2(state.userInput), new AIMessage(conclusion)]
2034
2516
  };
2035
2517
  }
2036
2518
  if (!res.success) {
2037
2519
  const conclusion = `Query failed with error: ${res.error} (took ${res.durationMs}ms)`;
2038
2520
  return {
2039
2521
  conclusion,
2040
- messages: [new HumanMessage2(state.userInput), new AIMessage(conclusion)]
2522
+ queryExecuted: true,
2523
+ lastQueryResult: res,
2524
+ lastGeneratedQuery: state.generatedQuery,
2525
+ messages: [...state.messages, new HumanMessage2(state.userInput), new AIMessage(conclusion)]
2041
2526
  };
2042
2527
  }
2043
2528
  const prompt = `User request: "${state.userInput}"
@@ -2062,7 +2547,11 @@ Ground your response strictly in the query results above. Never hallucinate rows
2062
2547
  ${conclusion}` : conclusion;
2063
2548
  return {
2064
2549
  conclusion,
2065
- messages: [new HumanMessage2(state.userInput), new AIMessage(historyAiText)]
2550
+ queryExecuted: true,
2551
+ lastQueryResult: res,
2552
+ lastGeneratedQuery: state.generatedQuery,
2553
+ lastStats: state.stats,
2554
+ messages: [...state.messages, new HumanMessage2(state.userInput), new AIMessage(historyAiText)]
2066
2555
  };
2067
2556
  } catch (err) {
2068
2557
  const conclusion = `Query executed successfully (${res.rowCount ?? 0} rows, ${res.durationMs}ms).`;
@@ -2071,16 +2560,88 @@ ${conclusion}` : conclusion;
2071
2560
  ${conclusion}` : conclusion;
2072
2561
  return {
2073
2562
  conclusion,
2074
- messages: [new HumanMessage2(state.userInput), new AIMessage(historyAiText)]
2563
+ queryExecuted: true,
2564
+ lastQueryResult: res,
2565
+ lastGeneratedQuery: state.generatedQuery,
2566
+ lastStats: state.stats,
2567
+ messages: [...state.messages, new HumanMessage2(state.userInput), new AIMessage(historyAiText)]
2075
2568
  };
2076
2569
  }
2077
2570
  }
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) => {
2571
+ async function answerDirectNode(state) {
2572
+ const historyMessages = state.messages.slice(-6);
2573
+ let contextDetails = "";
2574
+ if (state.lastQueryResult && state.lastQueryResult.rows && state.lastQueryResult.rows.length > 0) {
2575
+ const queryStr = state.lastGeneratedQuery?.sql || state.lastGeneratedQuery?.rawDisplay || "N/A";
2576
+ contextDetails += `
2577
+ \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
2578
+ `;
2579
+ contextDetails += `PREVIOUS QUERY EXECUTION DETAILS:
2580
+ `;
2581
+ contextDetails += `- Executed Query: ${queryStr}
2582
+ `;
2583
+ contextDetails += `- Rows returned: ${state.lastQueryResult.rowCount ?? state.lastQueryResult.rows.length}
2584
+ `;
2585
+ contextDetails += `- Rows data (sample up to 25 rows):
2586
+ ${JSON.stringify(state.lastQueryResult.rows.slice(0, 25), null, 2)}
2587
+ `;
2588
+ if (state.lastStats) {
2589
+ contextDetails += `- Computed statistics: ${JSON.stringify(state.lastStats, null, 2)}
2590
+ `;
2591
+ }
2592
+ 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
2593
+ `;
2594
+ }
2595
+ const prompt = `You are "SANDAL", an expert database assistant.
2596
+ The user has provided a request that MUST be answered directly WITHOUT generating or executing any database query.
2597
+
2598
+ User Request: "${state.userInput}"
2599
+
2600
+ Database Schema:
2601
+ ${state.schemaSummary || "No schema loaded."}
2602
+ ${contextDetails}
2603
+
2604
+ Guidelines:
2605
+ 1. Provide a direct, helpful, and accurate natural-language response to the user's request.
2606
+ 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.
2607
+ 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.
2608
+ 4. DO NOT output, execute, or propose any new database queries.
2609
+ 5. Ground your answer strictly in the available schema, history, or previous query results. Never hallucinate data.`;
2610
+ try {
2611
+ const response = await model.invoke([
2612
+ ...historyMessages,
2613
+ new HumanMessage2(prompt)
2614
+ ]);
2615
+ const conclusion = typeof response.content === "string" ? response.content : JSON.stringify(response.content);
2616
+ return {
2617
+ conclusion,
2618
+ queryExecuted: false,
2619
+ generatedQuery: void 0,
2620
+ queryResult: void 0,
2621
+ messages: [...state.messages, new HumanMessage2(state.userInput), new AIMessage(conclusion)]
2622
+ };
2623
+ } catch (err) {
2624
+ const conclusion = `Unable to process request: ${err.message}`;
2625
+ return {
2626
+ conclusion,
2627
+ queryExecuted: false,
2628
+ generatedQuery: void 0,
2629
+ queryResult: void 0,
2630
+ messages: [...state.messages, new HumanMessage2(state.userInput), new AIMessage(conclusion)]
2631
+ };
2632
+ }
2633
+ }
2634
+ 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) => {
2635
+ if (state.requiresQuery) {
2636
+ return "identify_targets";
2637
+ }
2638
+ return "answer_direct";
2639
+ }).addEdge("identify_targets", "generate_query").addEdge("generate_query", "classify_safety").addEdge("classify_safety", "request_confirmation").addConditionalEdges("request_confirmation", (state) => {
2079
2640
  if (state.confirmed && !state.cancelReason) {
2080
2641
  return "execute_query";
2081
2642
  }
2082
2643
  return "produce_conclusion";
2083
- }).addEdge("execute_query", "analyze_results").addEdge("analyze_results", "produce_conclusion").addEdge("produce_conclusion", END);
2644
+ }).addEdge("execute_query", "analyze_results").addEdge("analyze_results", "produce_conclusion").addEdge("produce_conclusion", END).addEdge("answer_direct", END);
2084
2645
  const checkpointer = new MemorySaver();
2085
2646
  const app = workflow.compile({ checkpointer });
2086
2647
  return app;
@@ -2089,19 +2650,19 @@ ${conclusion}` : conclusion;
2089
2650
  // src/markdown.ts
2090
2651
  import { Marked } from "marked";
2091
2652
  import { markedTerminal } from "marked-terminal";
2092
- import chalk2 from "chalk";
2653
+ import chalk3 from "chalk";
2093
2654
  function createMarkdownRenderer(options = {}) {
2094
2655
  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) => {
2656
+ heading: chalk3.bold.cyan,
2657
+ firstHeading: chalk3.bold.cyan.underline,
2658
+ codespan: chalk3.yellow,
2659
+ code: chalk3.yellow,
2660
+ blockquote: chalk3.gray.italic,
2661
+ hr: (input3) => {
2101
2662
  const w = Math.min(options.width ?? (process.stdout.columns || 80), 80);
2102
- return chalk2.gray(input2 && input2.trim() ? input2 : "\u2500".repeat(w));
2663
+ return chalk3.gray(input3 && input3.trim() ? input3 : "\u2500".repeat(w));
2103
2664
  },
2104
- table: chalk2.reset,
2665
+ table: chalk3.reset,
2105
2666
  tableOptions: {
2106
2667
  style: {
2107
2668
  head: ["cyan", "bold"],
@@ -2287,6 +2848,10 @@ var ChatMemoryManager = class {
2287
2848
  if (m.query) {
2288
2849
  text = `[Executed Query: ${m.query}]
2289
2850
  ${text}`;
2851
+ }
2852
+ if (m.resultSample && m.resultSample.length > 0) {
2853
+ text += `
2854
+ [Previous Query Results Sample: ${JSON.stringify(m.resultSample.slice(0, 10))}]`;
2290
2855
  }
2291
2856
  result.push(new AIMessage2(text));
2292
2857
  }
@@ -2305,7 +2870,8 @@ ${text}`;
2305
2870
  return recent.map((m) => {
2306
2871
  const prefix = m.role === "user" ? "User:" : "Assistant:";
2307
2872
  const q = m.query ? ` (Query: ${m.query})` : "";
2308
- return `${prefix} ${m.content}${q}`;
2873
+ const rc = typeof m.rowCount === "number" ? ` (${m.rowCount} rows)` : "";
2874
+ return `${prefix} ${m.content}${q}${rc}`;
2309
2875
  }).join("\n");
2310
2876
  }
2311
2877
  /**
@@ -2392,7 +2958,7 @@ var ReplSession = class {
2392
2958
  this.askQuestion.bind(this)
2393
2959
  );
2394
2960
  if (confirmResult.confirmed && this.currentSpinner) {
2395
- this.currentSpinner.text = chalk3.cyan("Executing query and analyzing...");
2961
+ this.currentSpinner.text = chalk4.cyan("Executing query and analyzing...");
2396
2962
  this.currentSpinner.start();
2397
2963
  }
2398
2964
  return confirmResult;
@@ -2411,7 +2977,7 @@ var ReplSession = class {
2411
2977
  return new Promise((resolve) => this.rl.question(query, resolve));
2412
2978
  }
2413
2979
  getPromptText() {
2414
- return chalk3.cyan(`sandal [${this.adapter.type}]> `);
2980
+ return chalk4.cyan(`sandal [${this.adapter.type}]> `);
2415
2981
  }
2416
2982
  async start() {
2417
2983
  this.setupSignalHandlers();
@@ -2425,8 +2991,8 @@ var ReplSession = class {
2425
2991
  const askQuestion = this.askQuestion.bind(this);
2426
2992
  while (this.isRunning) {
2427
2993
  process.stdin.resume();
2428
- const input2 = await askQuestion(this.getPromptText());
2429
- const trimmed = input2.trim();
2994
+ const input3 = await askQuestion(this.getPromptText());
2995
+ const trimmed = input3.trim();
2430
2996
  if (!trimmed) continue;
2431
2997
  if (["exit", "quit", ".exit", ".quit"].includes(trimmed.toLowerCase())) {
2432
2998
  break;
@@ -2449,12 +3015,12 @@ var ReplSession = class {
2449
3015
  continue;
2450
3016
  }
2451
3017
  if (trimmed === ".refresh") {
2452
- const spinner = ora(chalk3.blue("Refreshing schema cache...")).start();
3018
+ const spinner = ora(chalk4.blue("Refreshing schema cache...")).start();
2453
3019
  try {
2454
3020
  await this.adapter.inspectSchema(true);
2455
- spinner.succeed(chalk3.green("Schema refreshed successfully."));
3021
+ spinner.succeed(chalk4.green("Schema refreshed successfully."));
2456
3022
  } catch (err) {
2457
- spinner.fail(chalk3.red(`Failed to refresh schema: ${err.message}`));
3023
+ spinner.fail(chalk4.red(`Failed to refresh schema: ${err.message}`));
2458
3024
  }
2459
3025
  continue;
2460
3026
  }
@@ -2508,18 +3074,18 @@ var ReplSession = class {
2508
3074
  this.handleClearChat();
2509
3075
  continue;
2510
3076
  }
2511
- const intentSpinner = ora(chalk3.blue("Analyzing intent...")).start();
3077
+ const intentSpinner = ora(chalk4.blue("Analyzing intent...")).start();
2512
3078
  const historySummary = this.memoryManager.getRecentSummary(this.sessionId, 4);
2513
3079
  const intent = await classifyIntent(trimmed, this.model, historySummary);
2514
3080
  intentSpinner.stop();
2515
3081
  if (!intent.isDatabaseTask) {
2516
3082
  console.log(
2517
3083
  boxen2(
2518
- `${chalk3.bold.red("\u{1F6E1}\uFE0F GUARDRAIL ENFORCEMENT")}
3084
+ `${chalk4.bold.red("\u{1F6E1}\uFE0F GUARDRAIL ENFORCEMENT")}
2519
3085
 
2520
- ${chalk3.yellow(REFUSAL_MESSAGE)}
3086
+ ${chalk4.yellow(REFUSAL_MESSAGE)}
2521
3087
 
2522
- ${chalk3.gray(
3088
+ ${chalk4.gray(
2523
3089
  `Reason: ${intent.reason || "Non-database request rejected"}`
2524
3090
  )}`,
2525
3091
  {
@@ -2532,7 +3098,7 @@ ${chalk3.gray(
2532
3098
  );
2533
3099
  continue;
2534
3100
  }
2535
- const agentSpinner = ora(chalk3.cyan("Agent planning query...")).start();
3101
+ const agentSpinner = ora(chalk4.cyan("Agent thinking...")).start();
2536
3102
  this.currentSpinner = agentSpinner;
2537
3103
  try {
2538
3104
  const historyMessages = this.memoryManager.toLangChainMessages(this.sessionId, 6);
@@ -2550,15 +3116,15 @@ ${chalk3.gray(
2550
3116
  if (agentSpinner.isSpinning) {
2551
3117
  agentSpinner.stop();
2552
3118
  }
2553
- if (result.queryResult && result.queryResult.success && result.queryResult.rows) {
3119
+ if (result.queryExecuted && result.queryResult && result.queryResult.success && result.queryResult.rows) {
2554
3120
  this.renderRowsTable(result.queryResult);
2555
3121
  }
2556
3122
  if (result.conclusion) {
2557
- console.log("\n" + chalk3.green("\u{1F916} Answer:"));
3123
+ console.log("\n" + chalk4.green("\u{1F916} Answer:"));
2558
3124
  if (this.renderMarkdown) {
2559
3125
  console.log(renderMarkdown(result.conclusion) + "\n");
2560
3126
  } else {
2561
- console.log(chalk3.white(result.conclusion) + "\n");
3127
+ console.log(chalk4.white(result.conclusion) + "\n");
2562
3128
  }
2563
3129
  }
2564
3130
  this.memoryManager.addMessage(
@@ -2567,23 +3133,24 @@ ${chalk3.gray(
2567
3133
  this.adapter.connectionUrl
2568
3134
  );
2569
3135
  if (result.conclusion) {
2570
- const queryStr = result.generatedQuery?.sql || result.generatedQuery?.rawDisplay;
3136
+ const queryStr = result.queryExecuted ? result.generatedQuery?.sql || result.generatedQuery?.rawDisplay : void 0;
2571
3137
  this.memoryManager.addMessage(
2572
3138
  this.sessionId,
2573
3139
  {
2574
3140
  role: "assistant",
2575
3141
  content: result.conclusion,
2576
3142
  query: queryStr,
2577
- rowCount: result.queryResult?.rowCount
3143
+ rowCount: result.queryExecuted ? result.queryResult?.rowCount : void 0,
3144
+ resultSample: result.queryExecuted ? result.queryResult?.rows?.slice(0, 15) : void 0
2578
3145
  },
2579
3146
  this.adapter.connectionUrl
2580
3147
  );
2581
3148
  }
2582
3149
  } catch (err) {
2583
3150
  if (agentSpinner.isSpinning) {
2584
- agentSpinner.fail(chalk3.red(`Execution failed: ${err.message}`));
3151
+ agentSpinner.fail(chalk4.red(`Execution failed: ${err.message}`));
2585
3152
  } else {
2586
- console.error(chalk3.red(`
3153
+ console.error(chalk4.red(`
2587
3154
  Execution failed: ${err.message}
2588
3155
  `));
2589
3156
  }
@@ -2604,24 +3171,24 @@ Execution failed: ${err.message}
2604
3171
  let urlToConnect = targetUrl;
2605
3172
  if (!urlToConnect) {
2606
3173
  const saved = getSavedConnections();
2607
- console.log(chalk3.bold.cyan("\nDatabase Connections:"));
3174
+ console.log(chalk4.bold.cyan("\nDatabase Connections:"));
2608
3175
  if (saved.length === 0) {
2609
- console.log(chalk3.gray(" (No previous connections saved)"));
3176
+ console.log(chalk4.gray(" (No previous connections saved)"));
2610
3177
  } else {
2611
3178
  saved.forEach((url, i) => {
2612
3179
  const isCurrent = url === this.adapter.connectionUrl;
2613
- const currentTag = isCurrent ? chalk3.green(" (Current)") : "";
3180
+ const currentTag = isCurrent ? chalk4.green(" (Current)") : "";
2614
3181
  console.log(` [${i + 1}] ${maskUrl(url)}${currentTag}`);
2615
3182
  });
2616
3183
  }
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();
3184
+ console.log(chalk4.gray(" [N] Enter a new connection URL"));
3185
+ console.log(chalk4.gray(" [C] Cancel\n"));
3186
+ const choice = (await ask(chalk4.cyan("Select a connection [number, N, C]: "))).trim();
2620
3187
  if (!choice || choice.toLowerCase() === "c") {
2621
3188
  return;
2622
3189
  }
2623
3190
  if (choice.toLowerCase() === "n") {
2624
- const entered = (await ask(chalk3.cyan("Enter database connection URL: "))).trim();
3191
+ const entered = (await ask(chalk4.cyan("Enter database connection URL: "))).trim();
2625
3192
  if (!entered) return;
2626
3193
  urlToConnect = entered;
2627
3194
  } else {
@@ -2629,19 +3196,19 @@ Execution failed: ${err.message}
2629
3196
  if (!isNaN(num) && num >= 1 && num <= saved.length) {
2630
3197
  urlToConnect = saved[num - 1];
2631
3198
  } else {
2632
- console.log(chalk3.red("Invalid selection."));
3199
+ console.log(chalk4.red("Invalid selection."));
2633
3200
  return;
2634
3201
  }
2635
3202
  }
2636
3203
  }
2637
3204
  if (urlToConnect === this.adapter.connectionUrl && this.adapter.isConnected()) {
2638
- console.log(chalk3.yellow("\nAlready connected to this database.\n"));
3205
+ console.log(chalk4.yellow("\nAlready connected to this database.\n"));
2639
3206
  return;
2640
3207
  }
2641
3208
  try {
2642
3209
  detectDatabaseType(urlToConnect);
2643
3210
  } catch (e) {
2644
- console.log(chalk3.red(`Invalid connection URL: ${e.message}`));
3211
+ console.log(chalk4.red(`Invalid connection URL: ${e.message}`));
2645
3212
  return;
2646
3213
  }
2647
3214
  const spinner = ora(`Connecting to ${maskUrl(urlToConnect)}...`).start();
@@ -2657,55 +3224,54 @@ Execution failed: ${err.message}
2657
3224
  this.initAgent();
2658
3225
  addSavedConnection(urlToConnect);
2659
3226
  spinner.succeed(
2660
- chalk3.green(
3227
+ chalk4.green(
2661
3228
  `Switched to ${newAdapter.type.toUpperCase()} database: ${newAdapter.databaseName} (${newAdapter.getMaskedUrl()})`
2662
3229
  )
2663
3230
  );
2664
3231
  console.log(
2665
- chalk3.gray(
3232
+ chalk4.gray(
2666
3233
  `Chat session [${this.sessionId}] continuing with conversation memory on the new database.
2667
3234
  `
2668
3235
  )
2669
3236
  );
2670
3237
  } catch (err) {
2671
- spinner.fail(chalk3.red(`Failed to connect to database: ${err.message}`));
3238
+ spinner.fail(chalk4.red(`Failed to connect to database: ${err.message}`));
2672
3239
  }
2673
3240
  }
2674
3241
  async handleModelCommand(newModel, ask) {
2675
3242
  let targetModel = newModel;
2676
3243
  if (!targetModel) {
2677
3244
  console.log(
2678
- chalk3.bold.cyan(`
2679
- Current Model: `) + chalk3.white(this.modelName) + chalk3.gray(` (${this.provider})`)
3245
+ chalk4.bold.cyan(`
3246
+ Current Model: `) + chalk4.white(this.modelName) + chalk4.gray(` (${this.provider})`)
2680
3247
  );
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] || [];
3248
+ console.log(chalk4.bold.yellow(`
3249
+ Available models for ${this.provider.toUpperCase()}:`));
3250
+ const list = getModelsForProvider(this.provider);
2692
3251
  list.forEach((m, i) => {
2693
- const isCurrent = m === this.modelName ? chalk3.green(" (Current)") : "";
2694
- console.log(` [${i + 1}] ${m}${isCurrent}`);
3252
+ const isCurrent = m.id === this.modelName ? chalk4.green(" (Current)") : "";
3253
+ const rec = m.recommended ? chalk4.yellow(" [\u2605 Recommended]") : "";
3254
+ const badge = m.badge ? chalk4.dim(` [${m.badge}]`) : "";
3255
+ console.log(
3256
+ ` [${i + 1}] ${chalk4.bold(m.id)}${rec}${badge}${isCurrent}
3257
+ ${chalk4.gray(
3258
+ `${m.name} (${m.contextWindow}) \u2014 ${m.description}`
3259
+ )}`
3260
+ );
2695
3261
  });
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);
3262
+ console.log(chalk4.gray(" [O] Other (type custom model name)"));
3263
+ console.log(chalk4.gray(" [C] Cancel\n"));
3264
+ const input3 = (await ask(chalk4.cyan("Select model [number, name, C]: "))).trim();
3265
+ if (!input3 || input3.toLowerCase() === "c") return;
3266
+ const num = parseInt(input3, 10);
2701
3267
  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();
3268
+ targetModel = list[num - 1].id;
3269
+ } else if (input3.toLowerCase() === "o") {
3270
+ const custom = (await ask(chalk4.cyan("Enter model name: "))).trim();
2705
3271
  if (!custom) return;
2706
3272
  targetModel = custom;
2707
3273
  } else {
2708
- targetModel = input2;
3274
+ targetModel = input3;
2709
3275
  }
2710
3276
  }
2711
3277
  if (!this.apiKey) {
@@ -2713,11 +3279,11 @@ Current Model: `) + chalk3.white(this.modelName) + chalk3.gray(` (${this.provide
2713
3279
  if (resolved.apiKey) {
2714
3280
  this.apiKey = resolved.apiKey;
2715
3281
  } else {
2716
- console.log(chalk3.yellow(`
3282
+ console.log(chalk4.yellow(`
2717
3283
  No API key found for ${this.provider.toUpperCase()}.`));
2718
- this.apiKey = (await ask(chalk3.cyan(`Enter API key for ${this.provider.toUpperCase()}: `))).trim();
3284
+ this.apiKey = (await ask(chalk4.cyan(`Enter API key for ${this.provider.toUpperCase()}: `))).trim();
2719
3285
  if (!this.apiKey) {
2720
- console.log(chalk3.red("API key is required."));
3286
+ console.log(chalk4.red("API key is required."));
2721
3287
  return;
2722
3288
  }
2723
3289
  }
@@ -2730,83 +3296,128 @@ No API key found for ${this.provider.toUpperCase()}.`));
2730
3296
  });
2731
3297
  this.modelName = targetModel;
2732
3298
  this.initAgent();
2733
- console.log(chalk3.green(`
3299
+ console.log(chalk4.green(`
2734
3300
  Updated active model to: ${targetModel} [${this.provider}]
2735
3301
  `));
2736
3302
  } catch (err) {
2737
- console.log(chalk3.red(`
3303
+ console.log(chalk4.red(`
2738
3304
  Failed to update model: ${err.message}
2739
3305
  `));
2740
3306
  }
2741
3307
  }
2742
3308
  async handleProviderCommand(newProvider, ask) {
2743
- let p = newProvider.toLowerCase();
3309
+ let p = newProvider.toLowerCase().trim();
2744
3310
  if (!["google", "openai", "anthropic"].includes(p)) {
2745
- console.log(chalk3.bold.cyan(`
2746
- Current Provider: `) + chalk3.blue(this.provider));
3311
+ console.log(chalk4.bold.cyan(`
3312
+ Current Provider: `) + chalk4.blue(this.provider));
2747
3313
  console.log("Available providers:");
2748
3314
  console.log(" [1] Google Gemini (google)");
2749
3315
  console.log(" [2] OpenAI (openai)");
2750
3316
  console.log(" [3] Anthropic (anthropic)");
2751
3317
  console.log(" [C] Cancel\n");
2752
- const ans = (await ask(chalk3.cyan("Select provider [1-3, name, C]: "))).trim().toLowerCase();
3318
+ const ans = (await ask(chalk4.cyan("Select provider [1-3, name, C]: "))).trim().toLowerCase();
2753
3319
  if (!ans || ans === "c") return;
2754
3320
  if (ans === "1" || ans === "google") p = "google";
2755
3321
  else if (ans === "2" || ans === "openai") p = "openai";
2756
3322
  else if (ans === "3" || ans === "anthropic") p = "anthropic";
2757
3323
  else {
2758
- console.log(chalk3.red("Invalid provider."));
3324
+ console.log(chalk4.red("Invalid provider."));
2759
3325
  return;
2760
3326
  }
2761
3327
  }
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;
3328
+ const typedProvider = p;
3329
+ let key = getStoredApiKeyForProvider(typedProvider);
3330
+ if (key) {
3331
+ console.log(
3332
+ chalk4.gray(`Found stored API key for ${typedProvider.toUpperCase()} (${maskApiKey(key)})`)
3333
+ );
3334
+ } else {
3335
+ console.log(chalk4.yellow(`
3336
+ No stored API key found for ${typedProvider.toUpperCase()}.`));
3337
+ key = (await ask(chalk4.cyan(`Enter API key for ${typedProvider.toUpperCase()}: `))).trim();
3338
+ while (!key) {
3339
+ console.log(chalk4.red("API key cannot be empty."));
3340
+ key = (await ask(chalk4.cyan(`Enter API key for ${typedProvider.toUpperCase()} (or C to cancel): `))).trim();
3341
+ if (key.toLowerCase() === "c") return;
2771
3342
  }
2772
- const save = (await ask(chalk3.cyan("Save this API key to ~/.sandal/config.json? (Y/n): "))).trim().toLowerCase();
3343
+ const save = (await ask(chalk4.cyan("Save this API key to ~/.sandal/config.json? (Y/n): "))).trim().toLowerCase();
2773
3344
  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" });
3345
+ if (typedProvider === "google") writeConfig({ geminiApiKey: key });
3346
+ else if (typedProvider === "openai") writeConfig({ openaiApiKey: key });
3347
+ else if (typedProvider === "anthropic") writeConfig({ anthropicApiKey: key });
2777
3348
  }
2778
3349
  }
2779
- const defaultModel = getDefaultModelForProvider(p);
3350
+ const providerModels = getModelsForProvider(typedProvider);
3351
+ console.log(chalk4.bold.yellow(`
3352
+ Available models for ${typedProvider.toUpperCase()}:`));
3353
+ providerModels.forEach((m, i) => {
3354
+ const isCurrent = typedProvider === this.provider && m.id === this.modelName ? chalk4.green(" (Current)") : "";
3355
+ const rec = m.recommended ? chalk4.yellow(" [\u2605 Recommended]") : "";
3356
+ const badge = m.badge ? chalk4.dim(` [${m.badge}]`) : "";
3357
+ console.log(
3358
+ ` [${i + 1}] ${chalk4.bold(m.id)}${rec}${badge}${isCurrent}
3359
+ ${chalk4.gray(
3360
+ `${m.name} (${m.contextWindow}) \u2014 ${m.description}`
3361
+ )}`
3362
+ );
3363
+ });
3364
+ const defaultModel = getDefaultModelForProvider(typedProvider);
3365
+ console.log(chalk4.gray(` [Enter] Default (${defaultModel})`));
3366
+ console.log(chalk4.gray(" [O] Other (type custom model name)"));
3367
+ console.log(chalk4.gray(" [C] Cancel\n"));
3368
+ const choice = (await ask(chalk4.cyan(`Select model [1-${providerModels.length}, name, Enter]: `))).trim();
3369
+ if (choice.toLowerCase() === "c") return;
3370
+ let targetModel = defaultModel;
3371
+ const mNum = parseInt(choice, 10);
3372
+ if (!isNaN(mNum) && mNum >= 1 && mNum <= providerModels.length) {
3373
+ targetModel = providerModels[mNum - 1].id;
3374
+ } else if (choice.toLowerCase() === "o") {
3375
+ const custom = (await ask(chalk4.cyan("Enter custom model name: "))).trim();
3376
+ if (!custom) return;
3377
+ targetModel = custom;
3378
+ } else if (choice) {
3379
+ targetModel = choice;
3380
+ }
3381
+ const saveDefault = (await ask(chalk4.cyan(`Save ${typedProvider.toUpperCase()} and ${targetModel} as your default in ~/.sandal/config.json? (y/N): `))).trim().toLowerCase();
3382
+ if (saveDefault === "y" || saveDefault === "yes") {
3383
+ writeConfig({
3384
+ defaultProvider: typedProvider,
3385
+ defaultModel: targetModel
3386
+ });
3387
+ console.log(chalk4.green("Saved default provider and model to ~/.sandal/config.json."));
3388
+ }
2780
3389
  try {
2781
3390
  this.model = createChatModel({
2782
- provider: p,
2783
- model: defaultModel,
3391
+ provider: typedProvider,
3392
+ model: targetModel,
2784
3393
  apiKey: key
2785
3394
  });
2786
- this.provider = p;
2787
- this.modelName = defaultModel;
3395
+ this.provider = typedProvider;
3396
+ this.modelName = targetModel;
2788
3397
  this.apiKey = key;
2789
3398
  this.initAgent();
2790
3399
  console.log(
2791
- chalk3.green(`
2792
- Switched provider to ${p.toUpperCase()} with model ${defaultModel}.
2793
- `)
3400
+ chalk4.green(
3401
+ `
3402
+ Switched provider to ${chalk4.bold(typedProvider.toUpperCase())} with model ${chalk4.bold(targetModel)}.
3403
+ `
3404
+ )
2794
3405
  );
2795
3406
  } catch (err) {
2796
- console.log(chalk3.red(`Failed to switch provider: ${err.message}
3407
+ console.log(chalk4.red(`Failed to switch provider: ${err.message}
2797
3408
  `));
2798
3409
  }
2799
3410
  }
2800
3411
  async handleKeyCommand(arg, ask) {
2801
3412
  const sub = arg.toLowerCase().trim();
2802
3413
  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();
3414
+ const confirmAns = (await ask(chalk4.yellow(`Remove stored API key for provider "${this.provider}"? (y/N): `))).trim().toLowerCase();
2804
3415
  if (confirmAns === "y" || confirmAns === "yes") {
2805
3416
  removeApiKey(this.provider);
2806
3417
  this.apiKey = void 0;
2807
- console.log(chalk3.green(`
3418
+ console.log(chalk4.green(`
2808
3419
  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.
3420
+ console.log(chalk4.gray(`Subsequent requests with this provider will prompt for a key.
2810
3421
  `));
2811
3422
  }
2812
3423
  return;
@@ -2814,10 +3425,10 @@ Removed stored API key for "${this.provider}" from ~/.sandal/config.json.`));
2814
3425
  if (sub.startsWith("set")) {
2815
3426
  let newKey = sub.replace(/^set\s*/, "").trim();
2816
3427
  if (!newKey) {
2817
- newKey = (await ask(chalk3.cyan(`Enter new API key for ${this.provider}: `))).trim();
3428
+ newKey = (await ask(chalk4.cyan(`Enter new API key for ${this.provider}: `))).trim();
2818
3429
  }
2819
3430
  if (!newKey) {
2820
- console.log(chalk3.red("API key cannot be empty."));
3431
+ console.log(chalk4.red("API key cannot be empty."));
2821
3432
  return;
2822
3433
  }
2823
3434
  this.apiKey = newKey;
@@ -2830,46 +3441,46 @@ Removed stored API key for "${this.provider}" from ~/.sandal/config.json.`));
2830
3441
  apiKey: newKey
2831
3442
  });
2832
3443
  this.initAgent();
2833
- console.log(chalk3.green(`
3444
+ console.log(chalk4.green(`
2834
3445
  Updated API key for "${this.provider}" and refreshed model.
2835
3446
  `));
2836
3447
  return;
2837
3448
  }
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)}`);
3449
+ console.log(chalk4.bold.cyan("\nAPI Key Status:"));
3450
+ console.log(` Provider: ${chalk4.blue(this.provider)}`);
3451
+ console.log(` Model: ${chalk4.white(this.modelName)}`);
2841
3452
  console.log(
2842
- ` Active Key: ${this.apiKey ? chalk3.yellow(maskApiKey(this.apiKey)) : chalk3.red("None (not set)")}`
3453
+ ` Active Key: ${this.apiKey ? chalk4.yellow(maskApiKey(this.apiKey)) : chalk4.red("None (not set)")}`
2843
3454
  );
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"));
3455
+ console.log(chalk4.gray("\nUsage:"));
3456
+ console.log(chalk4.gray(" .key remove - Remove stored key from ~/.sandal/config.json"));
3457
+ console.log(chalk4.gray(" .key set <api-key> - Update API key on the fly\n"));
2847
3458
  }
2848
3459
  handleListChats() {
2849
3460
  const list = this.memoryManager.listSessions();
2850
3461
  if (list.length === 0) {
2851
- console.log(chalk3.yellow("\nNo saved chat sessions found.\n"));
3462
+ console.log(chalk4.yellow("\nNo saved chat sessions found.\n"));
2852
3463
  return;
2853
3464
  }
2854
- console.log(chalk3.bold.cyan("\nSaved Chat Sessions:"));
3465
+ console.log(chalk4.bold.cyan("\nSaved Chat Sessions:"));
2855
3466
  list.forEach((s, i) => {
2856
3467
  const isActive = s.id === this.sessionId;
2857
- const activeMarker = isActive ? chalk3.green(" [ACTIVE]") : "";
3468
+ const activeMarker = isActive ? chalk4.green(" [ACTIVE]") : "";
2858
3469
  const dateStr = new Date(s.updatedAt).toLocaleString();
2859
3470
  console.log(
2860
- ` [${i + 1}] ${chalk3.bold(s.title)}${activeMarker}
2861
- ID: ${chalk3.gray(s.id)} | ${chalk3.yellow(s.messageCount + " messages")} | ${chalk3.gray(dateStr)}`
3471
+ ` [${i + 1}] ${chalk4.bold(s.title)}${activeMarker}
3472
+ ID: ${chalk4.gray(s.id)} | ${chalk4.yellow(s.messageCount + " messages")} | ${chalk4.gray(dateStr)}`
2862
3473
  );
2863
3474
  });
2864
- console.log(chalk3.gray("\nCommands: .chat <id|num> to switch, .new to start fresh chat\n"));
3475
+ console.log(chalk4.gray("\nCommands: .chat <id|num> to switch, .new to start fresh chat\n"));
2865
3476
  }
2866
3477
  handleNewChat() {
2867
3478
  const newSession = this.memoryManager.createSession("New Chat", this.adapter.connectionUrl);
2868
3479
  this.sessionId = newSession.id;
2869
3480
  this.initAgent();
2870
- console.log(chalk3.green(`
3481
+ console.log(chalk4.green(`
2871
3482
  Started fresh chat session: ${this.sessionId}`));
2872
- console.log(chalk3.gray("Chat memory is clean for this new session.\n"));
3483
+ console.log(chalk4.gray("Chat memory is clean for this new session.\n"));
2873
3484
  }
2874
3485
  handleSwitchChat(arg) {
2875
3486
  const list = this.memoryManager.listSessions();
@@ -2884,14 +3495,14 @@ Started fresh chat session: ${this.sessionId}`));
2884
3495
  if (found) targetId = found.id;
2885
3496
  }
2886
3497
  if (!targetId) {
2887
- console.log(chalk3.red(`
3498
+ console.log(chalk4.red(`
2888
3499
  Chat session "${arg}" not found. Use .chats to list sessions.
2889
3500
  `));
2890
3501
  return;
2891
3502
  }
2892
3503
  const session = this.memoryManager.getSession(targetId);
2893
3504
  if (!session) {
2894
- console.log(chalk3.red(`
3505
+ console.log(chalk4.red(`
2895
3506
  Could not load chat session "${targetId}".
2896
3507
  `));
2897
3508
  return;
@@ -2899,7 +3510,7 @@ Could not load chat session "${targetId}".
2899
3510
  this.sessionId = session.id;
2900
3511
  this.initAgent();
2901
3512
  console.log(
2902
- chalk3.green(
3513
+ chalk4.green(
2903
3514
  `
2904
3515
  Switched to chat: "${session.title}" (${session.messages.length} messages in memory).
2905
3516
  `
@@ -2909,42 +3520,42 @@ Switched to chat: "${session.title}" (${session.messages.length} messages in mem
2909
3520
  handleShowHistory() {
2910
3521
  const session = this.memoryManager.getSession(this.sessionId);
2911
3522
  if (!session || session.messages.length === 0) {
2912
- console.log(chalk3.yellow(`
3523
+ console.log(chalk4.yellow(`
2913
3524
  No message history in current chat (${this.sessionId}).
2914
3525
  `));
2915
3526
  return;
2916
3527
  }
2917
- console.log(chalk3.bold.cyan(`
3528
+ console.log(chalk4.bold.cyan(`
2918
3529
  Chat History: "${session.title}" [${this.sessionId}]:`));
2919
- console.log(chalk3.gray("\u2500".repeat(60)));
3530
+ console.log(chalk4.gray("\u2500".repeat(60)));
2920
3531
  for (const msg of session.messages) {
2921
3532
  const time = new Date(msg.timestamp).toLocaleTimeString();
2922
3533
  if (msg.role === "user") {
2923
- console.log(`${chalk3.bold.blue("\u{1F464} User")} ${chalk3.gray(`(${time})`)}:
3534
+ console.log(`${chalk4.bold.blue("\u{1F464} User")} ${chalk4.gray(`(${time})`)}:
2924
3535
  ${msg.content}`);
2925
3536
  } else if (msg.role === "assistant") {
2926
3537
  if (msg.query) {
2927
- console.log(` ${chalk3.gray(`[Query: ${msg.query}]`)}`);
3538
+ console.log(` ${chalk4.gray(`[Query: ${msg.query}]`)}`);
2928
3539
  }
2929
- console.log(`${chalk3.bold.green("\u{1F916} Assistant")} ${chalk3.gray(`(${time})`)}:
3540
+ console.log(`${chalk4.bold.green("\u{1F916} Assistant")} ${chalk4.gray(`(${time})`)}:
2930
3541
  ${msg.content}`);
2931
3542
  }
2932
- console.log(chalk3.gray("\u2500".repeat(60)));
3543
+ console.log(chalk4.gray("\u2500".repeat(60)));
2933
3544
  }
2934
3545
  console.log("");
2935
3546
  }
2936
3547
  handleClearChat() {
2937
3548
  this.memoryManager.clearSession(this.sessionId);
2938
3549
  this.initAgent();
2939
- console.log(chalk3.green(`
3550
+ console.log(chalk4.green(`
2940
3551
  Chat memory for session [${this.sessionId}] has been cleared.
2941
3552
  `));
2942
3553
  }
2943
3554
  handleRenderMarkdown(arg) {
2944
3555
  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"));
3556
+ console.log(chalk4.yellow("Usage: .md <markdown text or file path>"));
3557
+ console.log(chalk4.gray("Example: .md # Hello World"));
3558
+ console.log(chalk4.gray("Example: .md ./README.md"));
2948
3559
  return;
2949
3560
  }
2950
3561
  try {
@@ -2960,21 +3571,21 @@ Chat memory for session [${this.sessionId}] has been cleared.
2960
3571
  renderRowsTable(queryResult) {
2961
3572
  const rows = queryResult.rows;
2962
3573
  if (!rows || rows.length === 0) {
2963
- console.log(chalk3.gray("(0 rows returned)"));
3574
+ console.log(chalk4.gray("(0 rows returned)"));
2964
3575
  return;
2965
3576
  }
2966
3577
  const fields = queryResult.fields && queryResult.fields.length > 0 ? queryResult.fields : Object.keys(rows[0] || {});
2967
3578
  if (fields.length === 0) return;
2968
3579
  const displayRows = rows.slice(0, 25);
2969
- const table = new Table({
2970
- head: fields.map((f) => chalk3.cyan.bold(f)),
3580
+ const table = new Table2({
3581
+ head: fields.map((f) => chalk4.cyan.bold(f)),
2971
3582
  style: { head: [], border: ["gray"] }
2972
3583
  });
2973
3584
  for (const r of displayRows) {
2974
3585
  table.push(
2975
3586
  fields.map((f) => {
2976
3587
  const val = r[f];
2977
- if (val === null || val === void 0) return chalk3.gray("NULL");
3588
+ if (val === null || val === void 0) return chalk4.gray("NULL");
2978
3589
  if (typeof val === "object") {
2979
3590
  try {
2980
3591
  return JSON.stringify(val);
@@ -2988,55 +3599,55 @@ Chat memory for session [${this.sessionId}] has been cleared.
2988
3599
  }
2989
3600
  console.log(table.toString());
2990
3601
  if (rows.length > 25) {
2991
- console.log(chalk3.yellow(`Showing first 25 of ${rows.length.toLocaleString()} rows.`));
3602
+ console.log(chalk4.yellow(`Showing first 25 of ${rows.length.toLocaleString()} rows.`));
2992
3603
  }
2993
3604
  console.log(
2994
- chalk3.gray(
3605
+ chalk4.gray(
2995
3606
  `Total rows: ${queryResult.rowCount ?? rows.length} | Execution duration: ${queryResult.durationMs}ms
2996
3607
  `
2997
3608
  )
2998
3609
  );
2999
3610
  }
3000
3611
  async handleListEntities() {
3001
- const spinner = ora(chalk3.blue("Inspecting schema...")).start();
3612
+ const spinner = ora(chalk4.blue("Inspecting schema...")).start();
3002
3613
  try {
3003
3614
  const schema = await this.adapter.inspectSchema(false);
3004
3615
  spinner.stop();
3005
3616
  if (schema.type === "postgres") {
3006
3617
  const tables = schema.tables || [];
3007
3618
  if (tables.length === 0) {
3008
- console.log(chalk3.yellow("No tables found in public schema."));
3619
+ console.log(chalk4.yellow("No tables found in public schema."));
3009
3620
  return;
3010
3621
  }
3011
- console.log(chalk3.bold.cyan(`
3622
+ console.log(chalk4.bold.cyan(`
3012
3623
  Tables in "${schema.databaseName}":`));
3013
3624
  for (const t of tables) {
3014
3625
  console.log(
3015
- ` \u2022 ${chalk3.bold(t.name)} ${chalk3.gray(`(~${t.approximateRowCount ?? 0} rows, ${t.columns.length} columns)`)}`
3626
+ ` \u2022 ${chalk4.bold(t.name)} ${chalk4.gray(`(~${t.approximateRowCount ?? 0} rows, ${t.columns.length} columns)`)}`
3016
3627
  );
3017
3628
  }
3018
3629
  console.log("");
3019
3630
  } else {
3020
3631
  const collections = schema.collections || [];
3021
3632
  if (collections.length === 0) {
3022
- console.log(chalk3.yellow("No collections found."));
3633
+ console.log(chalk4.yellow("No collections found."));
3023
3634
  return;
3024
3635
  }
3025
- console.log(chalk3.bold.cyan(`
3636
+ console.log(chalk4.bold.cyan(`
3026
3637
  Collections in "${schema.databaseName}":`));
3027
3638
  for (const c of collections) {
3028
3639
  console.log(
3029
- ` \u2022 ${chalk3.bold(c.name)} ${chalk3.gray(`(${c.documentCount ?? 0} docs, ${c.fields.length} sampled fields)`)}`
3640
+ ` \u2022 ${chalk4.bold(c.name)} ${chalk4.gray(`(${c.documentCount ?? 0} docs, ${c.fields.length} sampled fields)`)}`
3030
3641
  );
3031
3642
  }
3032
3643
  console.log("");
3033
3644
  }
3034
3645
  } catch (err) {
3035
- spinner.fail(chalk3.red(`Failed to list entities: ${err.message}`));
3646
+ spinner.fail(chalk4.red(`Failed to list entities: ${err.message}`));
3036
3647
  }
3037
3648
  }
3038
3649
  async handleShowSchema(entityName) {
3039
- const spinner = ora(chalk3.blue("Fetching schema details...")).start();
3650
+ const spinner = ora(chalk4.blue("Fetching schema details...")).start();
3040
3651
  try {
3041
3652
  const schema = await this.adapter.inspectSchema(false);
3042
3653
  spinner.stop();
@@ -3049,14 +3660,14 @@ Collections in "${schema.databaseName}":`));
3049
3660
  (t) => t.name.toLowerCase() === entityName.toLowerCase()
3050
3661
  );
3051
3662
  if (!table) {
3052
- console.log(chalk3.red(`Table "${entityName}" not found.`));
3663
+ console.log(chalk4.red(`Table "${entityName}" not found.`));
3053
3664
  return;
3054
3665
  }
3055
- console.log(chalk3.bold.cyan(`
3666
+ console.log(chalk4.bold.cyan(`
3056
3667
  Schema for table: ${table.schema}.${table.name}`));
3057
- const tableDetails = new Table({
3668
+ const tableDetails = new Table2({
3058
3669
  head: ["Column", "Data Type", "Nullable", "Primary Key", "Default"].map(
3059
- (h) => chalk3.cyan.bold(h)
3670
+ (h) => chalk4.cyan.bold(h)
3060
3671
  )
3061
3672
  });
3062
3673
  for (const c of table.columns) {
@@ -3064,8 +3675,8 @@ Schema for table: ${table.schema}.${table.name}`));
3064
3675
  c.name,
3065
3676
  c.dataType,
3066
3677
  c.isNullable ? "YES" : "NO",
3067
- c.isPrimaryKey ? chalk3.green("YES") : "NO",
3068
- c.defaultValue ?? chalk3.gray("NULL")
3678
+ c.isPrimaryKey ? chalk4.green("YES") : "NO",
3679
+ c.defaultValue ?? chalk4.gray("NULL")
3069
3680
  ]);
3070
3681
  }
3071
3682
  console.log(tableDetails.toString());
@@ -3074,13 +3685,13 @@ Schema for table: ${table.schema}.${table.name}`));
3074
3685
  (c) => c.name.toLowerCase() === entityName.toLowerCase()
3075
3686
  );
3076
3687
  if (!collection) {
3077
- console.log(chalk3.red(`Collection "${entityName}" not found.`));
3688
+ console.log(chalk4.red(`Collection "${entityName}" not found.`));
3078
3689
  return;
3079
3690
  }
3080
- console.log(chalk3.bold.cyan(`
3691
+ console.log(chalk4.bold.cyan(`
3081
3692
  Schema for collection: ${collection.name}`));
3082
- const colDetails = new Table({
3083
- head: ["Field", "Inferred Types"].map((h) => chalk3.cyan.bold(h))
3693
+ const colDetails = new Table2({
3694
+ head: ["Field", "Inferred Types"].map((h) => chalk4.cyan.bold(h))
3084
3695
  });
3085
3696
  for (const f of collection.fields) {
3086
3697
  colDetails.push([f.name, f.types.join(", ")]);
@@ -3088,22 +3699,22 @@ Schema for collection: ${collection.name}`));
3088
3699
  console.log(colDetails.toString());
3089
3700
  }
3090
3701
  } catch (err) {
3091
- spinner.fail(chalk3.red(`Failed to inspect schema: ${err.message}`));
3702
+ spinner.fail(chalk4.red(`Failed to inspect schema: ${err.message}`));
3092
3703
  }
3093
3704
  }
3094
3705
  printWelcomeBanner() {
3095
3706
  const banner = [
3096
- chalk3.bold.cyan("SANDAL: Safe Agentic Natural-language Database Access Layer"),
3097
- chalk3.gray("LangGraph + Multi-Provider AI (Gemini, OpenAI, Anthropic)"),
3707
+ chalk4.bold.cyan("SANDAL: Safe Agentic Natural-language Database Access Layer"),
3708
+ chalk4.gray("LangGraph + Multi-Provider AI (Gemini, OpenAI, Anthropic)"),
3098
3709
  "",
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")}`,
3710
+ `${chalk4.bold("Database:")} ${chalk4.green(this.adapter.type.toUpperCase())} (${this.adapter.getMaskedUrl()})`,
3711
+ `${chalk4.bold("LLM Provider:")} ${chalk4.blue(this.provider)} [${chalk4.white(this.modelName)}]`,
3712
+ `${chalk4.bold("Chat Session:")} ${chalk4.yellow(this.sessionId)}`,
3713
+ `${chalk4.bold("Strict Mode:")} ${this.strictMode ? chalk4.green("ON (Full wipes blocked)") : chalk4.yellow("OFF")}`,
3714
+ `${chalk4.bold("Markdown:")} ${this.renderMarkdown ? chalk4.green("ENABLED") : chalk4.gray("DISABLED")}`,
3104
3715
  "",
3105
- chalk3.gray("Type your natural language request or SQL/Mongo query."),
3106
- chalk3.gray("Commands: .connect, .model, .chats, .new, .history, .key, .help, exit")
3716
+ chalk4.gray("Type your natural language request or SQL/Mongo query."),
3717
+ chalk4.gray("Commands: .connect, .model, .chats, .new, .history, .key, .help, exit")
3107
3718
  ].join("\n");
3108
3719
  console.log(
3109
3720
  boxen2(banner, {
@@ -3118,26 +3729,26 @@ Schema for collection: ${collection.name}`));
3118
3729
  console.log(
3119
3730
  boxen2(
3120
3731
  [
3121
- chalk3.bold.cyan("SANDAL REPL Commands:"),
3732
+ chalk4.bold.cyan("SANDAL REPL Commands:"),
3122
3733
  "",
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`,
3734
+ `${chalk4.bold(".tables / .collections")} List all tables or collections with row counts`,
3735
+ `${chalk4.bold(".schema [name]")} Show columns, types, and indexes for a table`,
3736
+ `${chalk4.bold(".connect / .switch")} Switch database connection (shows saved connections)`,
3737
+ `${chalk4.bold(".model [name]")} Change active LLM model on the fly`,
3738
+ `${chalk4.bold(".provider [name]")} Switch LLM provider (google, openai, anthropic)`,
3739
+ `${chalk4.bold(".key [remove | set]")} View, remove, or update API key`,
3740
+ `${chalk4.bold(".chats")} List saved chat sessions`,
3741
+ `${chalk4.bold(".chat <id | num>")} Switch to another chat session`,
3742
+ `${chalk4.bold(".new")} Start a fresh chat session with clear memory`,
3743
+ `${chalk4.bold(".history")} Show conversation history for current chat`,
3744
+ `${chalk4.bold(".clear-chat")} Clear memory for current chat`,
3745
+ `${chalk4.bold(".md [file | text]")} Render markdown file or text in terminal`,
3746
+ `${chalk4.bold(".refresh")} Force refresh cached schema introspection`,
3747
+ `${chalk4.bold(".clear")} Clear terminal screen`,
3748
+ `${chalk4.bold(".help")} Display this command reference`,
3749
+ `${chalk4.bold("exit / quit")} Gracefully disconnect and exit`,
3139
3750
  "",
3140
- chalk3.bold.yellow("Example Natural Language Requests:"),
3751
+ chalk4.bold.yellow("Example Natural Language Requests:"),
3141
3752
  ' "Show top 10 users signed up in the last 30 days"',
3142
3753
  ' "Now count how many of them are active" (multi-turn follow-up)',
3143
3754
  ' "Calculate total revenue grouped by product category"',
@@ -3171,7 +3782,7 @@ Schema for collection: ${collection.name}`));
3171
3782
  } catch {
3172
3783
  }
3173
3784
  }
3174
- console.log(chalk3.cyan("\nDatabase connection closed. Goodbye! \u{1F44B}\n"));
3785
+ console.log(chalk4.cyan("\nDatabase connection closed. Goodbye! \u{1F44B}\n"));
3175
3786
  }
3176
3787
  };
3177
3788
 
@@ -3199,7 +3810,18 @@ program.name("sandal-db").description("SANDAL - Safe Agentic Natural-language Da
3199
3810
  "--markdown",
3200
3811
  "Render LLM answers formatted as terminal markdown (default: true)",
3201
3812
  true
3202
- ).option("--no-markdown", "Disable terminal markdown rendering").action(async (cliDbUrl, options) => {
3813
+ ).option("--no-markdown", "Disable terminal markdown rendering").option(
3814
+ "--select-model",
3815
+ "Interactively choose a model from the list of available models"
3816
+ ).option(
3817
+ "--models [provider]",
3818
+ "List all publicly available models in the terminal (google | openai | anthropic)"
3819
+ ).action(async (cliDbUrl, options) => {
3820
+ if (options.models !== void 0) {
3821
+ const prov = typeof options.models === "string" && options.models.trim() ? options.models.toLowerCase() : options.provider ? options.provider.toLowerCase() : void 0;
3822
+ console.log(renderModelsTable(prov));
3823
+ return;
3824
+ }
3203
3825
  try {
3204
3826
  await runCli({
3205
3827
  cliDbUrl,
@@ -3209,10 +3831,11 @@ program.name("sandal-db").description("SANDAL - Safe Agentic Natural-language Da
3209
3831
  strictMode: options.strict !== false,
3210
3832
  allowFullWipe: Boolean(options.allowFullWipe),
3211
3833
  rowThreshold: parseInt(options.threshold, 10) || 50,
3212
- renderMarkdown: options.markdown !== false
3834
+ renderMarkdown: options.markdown !== false,
3835
+ selectModel: Boolean(options.selectModel)
3213
3836
  });
3214
3837
  } catch (err) {
3215
- console.error(chalk4.red(`
3838
+ console.error(chalk5.red(`
3216
3839
  Error: ${err.message}`));
3217
3840
  process.exit(1);
3218
3841
  }
@@ -3223,6 +3846,12 @@ var configCmd = program.command("config").description(
3223
3846
  "--set-provider <provider>",
3224
3847
  "Set default LLM provider (google | openai | anthropic)"
3225
3848
  ).option("--set-model <model>", "Set default model name").option(
3849
+ "--select-model",
3850
+ "Interactively select default model from the available models list"
3851
+ ).option(
3852
+ "--models [provider]",
3853
+ "List all publicly available models in the terminal (google | openai | anthropic)"
3854
+ ).option(
3226
3855
  "--set-markdown <boolean>",
3227
3856
  "Enable or disable default terminal markdown rendering (true | false)"
3228
3857
  ).option(
@@ -3237,7 +3866,22 @@ var configCmd = program.command("config").description(
3237
3866
  ).option(
3238
3867
  "--show",
3239
3868
  "Display current stored configuration (with masked secrets)"
3240
- ).option("--path", "Print the path to the configuration file").action((opts) => {
3869
+ ).option("--path", "Print the path to the configuration file").action(async (opts) => {
3870
+ if (opts.models !== void 0) {
3871
+ const prov = typeof opts.models === "string" && opts.models.trim() ? opts.models.toLowerCase() : void 0;
3872
+ console.log(renderModelsTable(prov));
3873
+ return;
3874
+ }
3875
+ if (opts.selectModel) {
3876
+ const cfg = readConfig();
3877
+ const prov = opts.setProvider || cfg.defaultProvider || "google";
3878
+ const chosen = await promptModelSelection(prov, cfg.defaultModel);
3879
+ writeConfig({ defaultModel: chosen });
3880
+ console.log(chalk5.green(`
3881
+ Updated default model to: ${chosen} (${prov})
3882
+ `));
3883
+ return;
3884
+ }
3241
3885
  if (opts.path) {
3242
3886
  console.log(getConfigFilePath());
3243
3887
  return;
@@ -3245,9 +3889,9 @@ var configCmd = program.command("config").description(
3245
3889
  if (opts.connections) {
3246
3890
  const list = getSavedConnections();
3247
3891
  if (list.length === 0) {
3248
- console.log(chalk4.yellow("\nNo saved database connections found.\n"));
3892
+ console.log(chalk5.yellow("\nNo saved database connections found.\n"));
3249
3893
  } else {
3250
- console.log(chalk4.bold.cyan("\nSaved Database Connections:"));
3894
+ console.log(chalk5.bold.cyan("\nSaved Database Connections:"));
3251
3895
  list.forEach((url, i) => {
3252
3896
  console.log(` [${i + 1}] ${maskUrl(url)}`);
3253
3897
  });
@@ -3258,47 +3902,47 @@ var configCmd = program.command("config").description(
3258
3902
  if (opts.removeConnection) {
3259
3903
  const removed = removeSavedConnection(opts.removeConnection);
3260
3904
  if (removed) {
3261
- console.log(chalk4.green(`Removed connection from saved history.`));
3905
+ console.log(chalk5.green(`Removed connection from saved history.`));
3262
3906
  } else {
3263
- console.log(chalk4.red(`Connection "${opts.removeConnection}" not found in history.`));
3907
+ console.log(chalk5.red(`Connection "${opts.removeConnection}" not found in history.`));
3264
3908
  }
3265
3909
  return;
3266
3910
  }
3267
3911
  if (opts.removeKey !== void 0) {
3268
3912
  const prov = typeof opts.removeKey === "string" && opts.removeKey.trim() ? opts.removeKey.toLowerCase() : "all";
3269
3913
  removeApiKey(prov);
3270
- console.log(chalk4.green(`Removed API key for "${prov}" from ~/.sandal/config.json.`));
3914
+ console.log(chalk5.green(`Removed API key for "${prov}" from ~/.sandal/config.json.`));
3271
3915
  return;
3272
3916
  }
3273
3917
  if (opts.show) {
3274
3918
  const cfg = readConfig();
3275
3919
  const saved = getSavedConnections();
3276
3920
  console.log(
3277
- chalk4.bold.cyan("\nSaved Configuration (~/.sandal/config.json):")
3921
+ chalk5.bold.cyan("\nSaved Configuration (~/.sandal/config.json):")
3278
3922
  );
3279
3923
  console.log(
3280
- ` Database URL: ${cfg.dbUrl ? chalk4.green(maskUrl(cfg.dbUrl)) : chalk4.gray("Not set")}`
3924
+ ` Database URL: ${cfg.dbUrl ? chalk5.green(maskUrl(cfg.dbUrl)) : chalk5.gray("Not set")}`
3281
3925
  );
3282
3926
  console.log(
3283
- ` Default Provider: ${cfg.defaultProvider ? chalk4.blue(cfg.defaultProvider) : chalk4.gray("Not set (defaults to google)")}`
3927
+ ` Default Provider: ${cfg.defaultProvider ? chalk5.blue(cfg.defaultProvider) : chalk5.gray("Not set (defaults to google)")}`
3284
3928
  );
3285
3929
  console.log(
3286
- ` Default Model: ${cfg.defaultModel ? chalk4.white(cfg.defaultModel) : chalk4.gray("Default for provider")}`
3930
+ ` Default Model: ${cfg.defaultModel ? chalk5.white(cfg.defaultModel) : chalk5.gray("Default for provider")}`
3287
3931
  );
3288
3932
  console.log(
3289
- ` Markdown Rendering: ${cfg.renderMarkdown !== false ? chalk4.green("enabled") : chalk4.yellow("disabled")}`
3933
+ ` Markdown Rendering: ${cfg.renderMarkdown !== false ? chalk5.green("enabled") : chalk5.yellow("disabled")}`
3290
3934
  );
3291
3935
  console.log(
3292
- ` Saved Connections: ${saved.length > 0 ? chalk4.cyan(`${saved.length} connection(s)`) : chalk4.gray("None")}`
3936
+ ` Saved Connections: ${saved.length > 0 ? chalk5.cyan(`${saved.length} connection(s)`) : chalk5.gray("None")}`
3293
3937
  );
3294
3938
  console.log(
3295
- ` Gemini Key: ${cfg.geminiApiKey ? chalk4.yellow(maskApiKey(cfg.geminiApiKey)) : chalk4.gray("Not set")}`
3939
+ ` Gemini Key: ${cfg.geminiApiKey ? chalk5.yellow(maskApiKey(cfg.geminiApiKey)) : chalk5.gray("Not set")}`
3296
3940
  );
3297
3941
  console.log(
3298
- ` OpenAI Key: ${cfg.openaiApiKey ? chalk4.yellow(maskApiKey(cfg.openaiApiKey)) : chalk4.gray("Not set")}`
3942
+ ` OpenAI Key: ${cfg.openaiApiKey ? chalk5.yellow(maskApiKey(cfg.openaiApiKey)) : chalk5.gray("Not set")}`
3299
3943
  );
3300
3944
  console.log(
3301
- ` Anthropic Key: ${cfg.anthropicApiKey ? chalk4.yellow(maskApiKey(cfg.anthropicApiKey)) : chalk4.gray("Not set")}
3945
+ ` Anthropic Key: ${cfg.anthropicApiKey ? chalk5.yellow(maskApiKey(cfg.anthropicApiKey)) : chalk5.gray("Not set")}
3302
3946
  `
3303
3947
  );
3304
3948
  return;
@@ -3309,44 +3953,64 @@ var configCmd = program.command("config").description(
3309
3953
  updates.dbUrl = opts.setDb;
3310
3954
  addSavedConnection(opts.setDb);
3311
3955
  console.log(
3312
- chalk4.green(`Updated default database URL: ${maskUrl(opts.setDb)}`)
3956
+ chalk5.green(`Updated default database URL: ${maskUrl(opts.setDb)}`)
3313
3957
  );
3314
3958
  }
3315
3959
  if (opts.setProvider) {
3316
3960
  const p = opts.setProvider.toLowerCase();
3317
3961
  if (!["google", "openai", "anthropic"].includes(p)) {
3318
3962
  console.error(
3319
- chalk4.red("Invalid provider. Expected google, openai, or anthropic.")
3963
+ chalk5.red("Invalid provider. Expected google, openai, or anthropic.")
3320
3964
  );
3321
3965
  process.exit(1);
3322
3966
  }
3323
3967
  updates.defaultProvider = p;
3324
- console.log(chalk4.green(`Updated default provider: ${p}`));
3968
+ console.log(chalk5.green(`Updated default provider: ${p}`));
3969
+ const existingKey = getStoredApiKeyForProvider(p);
3970
+ if (!existingKey && !opts.setKey && !opts.setGemini && !opts.setOpenai && !opts.setAnthropic) {
3971
+ console.log(chalk5.yellow(`
3972
+ No stored API key found for ${p.toUpperCase()}.`));
3973
+ const newKey = await input2({
3974
+ message: `Enter API key for ${p.toUpperCase()}:`,
3975
+ validate: (val) => val.trim().length > 0 ? true : "API key cannot be empty."
3976
+ });
3977
+ if (p === "google") updates.geminiApiKey = newKey.trim();
3978
+ else if (p === "openai") updates.openaiApiKey = newKey.trim();
3979
+ else if (p === "anthropic") updates.anthropicApiKey = newKey.trim();
3980
+ console.log(chalk5.green(`Saved API key for ${p.toUpperCase()}.`));
3981
+ }
3982
+ if (!opts.setModel) {
3983
+ console.log(chalk5.cyan(`
3984
+ Choose default model for ${p.toUpperCase()}:`));
3985
+ const chosenModel = await promptModelSelection(p);
3986
+ updates.defaultModel = chosenModel;
3987
+ console.log(chalk5.green(`Updated default model for ${p}: ${chosenModel}`));
3988
+ }
3325
3989
  }
3326
3990
  if (opts.setModel) {
3327
3991
  updates.defaultModel = opts.setModel;
3328
- console.log(chalk4.green(`Updated default model: ${opts.setModel}`));
3992
+ console.log(chalk5.green(`Updated default model: ${opts.setModel}`));
3329
3993
  }
3330
3994
  if (opts.setMarkdown !== void 0) {
3331
3995
  const val = opts.setMarkdown.toLowerCase() === "true" || opts.setMarkdown === "1" || opts.setMarkdown === "yes";
3332
3996
  updates.renderMarkdown = val;
3333
3997
  console.log(
3334
- chalk4.green(
3998
+ chalk5.green(
3335
3999
  `Updated default markdown rendering: ${val ? "enabled" : "disabled"}.`
3336
4000
  )
3337
4001
  );
3338
4002
  }
3339
4003
  if (opts.setGemini) {
3340
4004
  updates.geminiApiKey = opts.setGemini;
3341
- console.log(chalk4.green("Updated Gemini API key."));
4005
+ console.log(chalk5.green("Updated Gemini API key."));
3342
4006
  }
3343
4007
  if (opts.setOpenai) {
3344
4008
  updates.openaiApiKey = opts.setOpenai;
3345
- console.log(chalk4.green("Updated OpenAI API key."));
4009
+ console.log(chalk5.green("Updated OpenAI API key."));
3346
4010
  }
3347
4011
  if (opts.setAnthropic) {
3348
4012
  updates.anthropicApiKey = opts.setAnthropic;
3349
- console.log(chalk4.green("Updated Anthropic API key."));
4013
+ console.log(chalk5.green("Updated Anthropic API key."));
3350
4014
  }
3351
4015
  if (opts.setKey) {
3352
4016
  const current = readConfig();
@@ -3354,11 +4018,11 @@ var configCmd = program.command("config").description(
3354
4018
  if (prov === "google") updates.geminiApiKey = opts.setKey;
3355
4019
  else if (prov === "openai") updates.openaiApiKey = opts.setKey;
3356
4020
  else if (prov === "anthropic") updates.anthropicApiKey = opts.setKey;
3357
- console.log(chalk4.green(`Updated API key for provider "${prov}".`));
4021
+ console.log(chalk5.green(`Updated API key for provider "${prov}".`));
3358
4022
  }
3359
4023
  if (Object.keys(updates).length > 0) {
3360
4024
  writeConfig(updates);
3361
- console.log(chalk4.gray(`Saved to ${getConfigFilePath()}`));
4025
+ console.log(chalk5.gray(`Saved to ${getConfigFilePath()}`));
3362
4026
  } else {
3363
4027
  configCmd.help();
3364
4028
  }
@@ -3374,13 +4038,13 @@ program.command("markdown [fileOrText]").alias("md").description("Render markdow
3374
4038
  content = Buffer.concat(chunks).toString("utf-8");
3375
4039
  } else {
3376
4040
  console.log(
3377
- chalk4.yellow(
4041
+ chalk5.yellow(
3378
4042
  "\nUsage: sandal md <file | text> or pipe markdown to sandal md"
3379
4043
  )
3380
4044
  );
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"));
4045
+ console.log(chalk5.gray("Example: sandal md README.md"));
4046
+ console.log(chalk5.gray('Example: sandal md "# Hello World"'));
4047
+ console.log(chalk5.gray("Example: cat notes.md | sandal md\n"));
3384
4048
  return;
3385
4049
  }
3386
4050
  } else {
@@ -3410,11 +4074,11 @@ async function runCli(opts) {
3410
4074
  let dbUrl = resolved.dbUrl;
3411
4075
  if (!dbUrl) {
3412
4076
  console.log(
3413
- chalk4.yellow(
4077
+ chalk5.yellow(
3414
4078
  "\nNo database connection URL found in CLI flags, environment, or config."
3415
4079
  )
3416
4080
  );
3417
- dbUrl = await input({
4081
+ dbUrl = await input2({
3418
4082
  message: "Enter your database URL (postgres://... or mongodb://...):",
3419
4083
  validate: (val) => {
3420
4084
  try {
@@ -3440,34 +4104,55 @@ async function runCli(opts) {
3440
4104
  }
3441
4105
  let provider = resolved.provider;
3442
4106
  let apiKey = resolved.apiKey;
4107
+ const isProviderExplicitlyChanged = Boolean(
4108
+ opts.cliProvider && opts.cliProvider.toLowerCase() !== config.defaultProvider
4109
+ );
3443
4110
  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({
4111
+ if (opts.cliProvider) {
4112
+ console.log(
4113
+ chalk5.yellow(`
4114
+ No API key found for ${provider.toUpperCase()} in environment or config.`)
4115
+ );
4116
+ } else {
4117
+ console.log(chalk5.yellow("\nNo API key found in environment or config."));
4118
+ provider = await select2({
4119
+ message: "Select your LLM provider:",
4120
+ choices: [
4121
+ { name: "Google Gemini (GEMINI_API_KEY)", value: "google" },
4122
+ { name: "OpenAI (OPENAI_API_KEY)", value: "openai" },
4123
+ { name: "Anthropic (ANTHROPIC_API_KEY)", value: "anthropic" }
4124
+ ]
4125
+ });
4126
+ }
4127
+ apiKey = await input2({
3454
4128
  message: `Enter your ${provider.toUpperCase()} API key:`,
3455
4129
  validate: (val) => val.trim().length > 0 ? true : "API key cannot be empty."
3456
4130
  });
4131
+ let chosenModel = opts.cliModel;
4132
+ if (!chosenModel) {
4133
+ chosenModel = await promptModelSelection(provider);
4134
+ }
3457
4135
  const saveKey = await confirm({
3458
- message: "Save this API key to ~/.sandal/config.json?",
4136
+ message: "Save this API key and default model to ~/.sandal/config.json?",
3459
4137
  default: true
3460
4138
  });
3461
4139
  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" });
4140
+ const updates = {
4141
+ defaultProvider: provider,
4142
+ defaultModel: chosenModel
4143
+ };
4144
+ if (provider === "google") updates.geminiApiKey = apiKey;
4145
+ else if (provider === "openai") updates.openaiApiKey = apiKey;
4146
+ else if (provider === "anthropic") updates.anthropicApiKey = apiKey;
4147
+ writeConfig(updates);
3468
4148
  }
3469
4149
  }
3470
- const modelName = opts.cliModel || resolved.model || getDefaultModelForProvider(provider);
4150
+ let modelName = opts.cliModel;
4151
+ if (opts.selectModel || isProviderExplicitlyChanged && !opts.cliModel) {
4152
+ modelName = await promptModelSelection(provider, resolved.model);
4153
+ } else if (!modelName) {
4154
+ modelName = resolved.model || getDefaultModelForProvider(provider);
4155
+ }
3471
4156
  const dbSpinner = ora2(
3472
4157
  `Connecting to database (${maskUrl(dbUrl)})...`
3473
4158
  ).start();
@@ -3475,13 +4160,13 @@ async function runCli(opts) {
3475
4160
  try {
3476
4161
  await adapter.connect();
3477
4162
  dbSpinner.succeed(
3478
- chalk4.green(
4163
+ chalk5.green(
3479
4164
  `Connected to ${adapter.type.toUpperCase()} database: ${adapter.databaseName}`
3480
4165
  )
3481
4166
  );
3482
4167
  addSavedConnection(dbUrl);
3483
4168
  } catch (err) {
3484
- dbSpinner.fail(chalk4.red(`Failed to connect to database: ${err.message}`));
4169
+ dbSpinner.fail(chalk5.red(`Failed to connect to database: ${err.message}`));
3485
4170
  process.exit(1);
3486
4171
  }
3487
4172
  const model = createChatModel({