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