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