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