@teckedd-code2save/datafy 0.17.0

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.
@@ -0,0 +1,2188 @@
1
+ // src/tools/builtin-tools.ts
2
+ var BUILTIN_TOOL_EXECUTE_SQL = "execute_sql";
3
+ var BUILTIN_TOOL_SEARCH_OBJECTS = "search_objects";
4
+ var BUILTIN_TOOL_GENERATE_CODE = "generate_code";
5
+ var BUILTIN_TOOL_REDIS_COMMAND = "redis_command";
6
+ var BUILTIN_TOOL_ELASTICSEARCH_SEARCH = "elasticsearch_search";
7
+ var BUILTIN_TOOLS = [
8
+ BUILTIN_TOOL_EXECUTE_SQL,
9
+ BUILTIN_TOOL_SEARCH_OBJECTS,
10
+ BUILTIN_TOOL_GENERATE_CODE,
11
+ BUILTIN_TOOL_REDIS_COMMAND,
12
+ BUILTIN_TOOL_ELASTICSEARCH_SEARCH
13
+ ];
14
+
15
+ // src/connectors/interface.ts
16
+ var _ConnectorRegistry = class _ConnectorRegistry {
17
+ /**
18
+ * Register a new connector
19
+ */
20
+ static register(connector) {
21
+ _ConnectorRegistry.connectors.set(connector.id, connector);
22
+ }
23
+ /**
24
+ * Get a connector by ID
25
+ */
26
+ static getConnector(id) {
27
+ return _ConnectorRegistry.connectors.get(id) || null;
28
+ }
29
+ /**
30
+ * Get connector for a DSN string
31
+ * Tries to find a connector that can handle the given DSN format
32
+ */
33
+ static getConnectorForDSN(dsn) {
34
+ for (const connector of _ConnectorRegistry.connectors.values()) {
35
+ if (connector.dsnParser.isValidDSN(dsn)) {
36
+ return connector;
37
+ }
38
+ }
39
+ return null;
40
+ }
41
+ /**
42
+ * Get all available connector IDs
43
+ */
44
+ static getAvailableConnectors() {
45
+ return Array.from(_ConnectorRegistry.connectors.keys());
46
+ }
47
+ /**
48
+ * Get sample DSN for a specific connector
49
+ */
50
+ static getSampleDSN(connectorType) {
51
+ const connector = _ConnectorRegistry.getConnector(connectorType);
52
+ if (!connector) return null;
53
+ return connector.dsnParser.getSampleDSN();
54
+ }
55
+ /**
56
+ * Get all available sample DSNs
57
+ */
58
+ static getAllSampleDSNs() {
59
+ const samples = {};
60
+ for (const [id, connector] of _ConnectorRegistry.connectors.entries()) {
61
+ samples[id] = connector.dsnParser.getSampleDSN();
62
+ }
63
+ return samples;
64
+ }
65
+ };
66
+ _ConnectorRegistry.connectors = /* @__PURE__ */ new Map();
67
+ var ConnectorRegistry = _ConnectorRegistry;
68
+
69
+ // src/utils/ssh-tunnel.ts
70
+ import { Client } from "ssh2";
71
+ import { readFileSync as readFileSync2 } from "fs";
72
+ import { createServer } from "net";
73
+
74
+ // src/utils/ssh-config-parser.ts
75
+ import { readFileSync, realpathSync, statSync } from "fs";
76
+ import { homedir } from "os";
77
+ import { join } from "path";
78
+ import SSHConfig from "ssh-config";
79
+ var DEFAULT_SSH_KEYS = [
80
+ "~/.ssh/id_rsa",
81
+ "~/.ssh/id_ed25519",
82
+ "~/.ssh/id_ecdsa",
83
+ "~/.ssh/id_dsa"
84
+ ];
85
+ function expandTilde(filePath) {
86
+ if (filePath.startsWith("~/")) {
87
+ return join(homedir(), filePath.substring(2));
88
+ }
89
+ return filePath;
90
+ }
91
+ function resolveSymlink(filePath) {
92
+ const expandedPath = expandTilde(filePath);
93
+ try {
94
+ return realpathSync(expandedPath);
95
+ } catch {
96
+ return expandedPath;
97
+ }
98
+ }
99
+ function isFile(filePath) {
100
+ try {
101
+ const stat = statSync(filePath);
102
+ return stat.isFile();
103
+ } catch {
104
+ return false;
105
+ }
106
+ }
107
+ function findDefaultSSHKey() {
108
+ for (const keyPath of DEFAULT_SSH_KEYS) {
109
+ const resolvedPath = resolveSymlink(keyPath);
110
+ if (isFile(resolvedPath)) {
111
+ return resolvedPath;
112
+ }
113
+ }
114
+ return void 0;
115
+ }
116
+ function parseSSHConfig(hostAlias, configPath) {
117
+ const sshConfigPath = resolveSymlink(configPath);
118
+ if (!isFile(sshConfigPath)) {
119
+ return null;
120
+ }
121
+ try {
122
+ const configContent = readFileSync(sshConfigPath, "utf8");
123
+ const config = SSHConfig.parse(configContent);
124
+ const hostConfig = config.compute(hostAlias);
125
+ if (!hostConfig || !hostConfig.HostName && !hostConfig.User) {
126
+ return null;
127
+ }
128
+ const sshConfig = {};
129
+ if (hostConfig.HostName) {
130
+ sshConfig.host = hostConfig.HostName;
131
+ } else {
132
+ sshConfig.host = hostAlias;
133
+ }
134
+ if (hostConfig.Port) {
135
+ sshConfig.port = parseInt(hostConfig.Port, 10);
136
+ }
137
+ if (hostConfig.User) {
138
+ sshConfig.username = hostConfig.User;
139
+ }
140
+ if (hostConfig.IdentityFile) {
141
+ const identityFile = Array.isArray(hostConfig.IdentityFile) ? hostConfig.IdentityFile[0] : hostConfig.IdentityFile;
142
+ const resolvedPath = resolveSymlink(identityFile);
143
+ if (isFile(resolvedPath)) {
144
+ sshConfig.privateKey = resolvedPath;
145
+ }
146
+ }
147
+ if (!sshConfig.privateKey) {
148
+ const defaultKey = findDefaultSSHKey();
149
+ if (defaultKey) {
150
+ sshConfig.privateKey = defaultKey;
151
+ }
152
+ }
153
+ if (hostConfig.ProxyJump) {
154
+ sshConfig.proxyJump = hostConfig.ProxyJump;
155
+ }
156
+ if (hostConfig.ProxyCommand) {
157
+ console.error("Warning: ProxyCommand in SSH config is not supported by DBHub. Use ProxyJump instead.");
158
+ }
159
+ if (!sshConfig.host || !sshConfig.username) {
160
+ return null;
161
+ }
162
+ return sshConfig;
163
+ } catch (error) {
164
+ console.error(`Error parsing SSH config: ${error instanceof Error ? error.message : String(error)}`);
165
+ return null;
166
+ }
167
+ }
168
+ function looksLikeSSHAlias(host) {
169
+ if (host.includes(".")) {
170
+ return false;
171
+ }
172
+ if (/^[\d:]+$/.test(host)) {
173
+ return false;
174
+ }
175
+ if (/^[0-9a-fA-F:]+$/.test(host) && host.includes(":")) {
176
+ return false;
177
+ }
178
+ return true;
179
+ }
180
+ function validatePort(port, jumpHostStr) {
181
+ if (isNaN(port) || port <= 0 || port > 65535) {
182
+ throw new Error(`Invalid port number in "${jumpHostStr}": port must be between 1 and 65535`);
183
+ }
184
+ }
185
+ function parseJumpHost(jumpHostStr) {
186
+ let username;
187
+ let host;
188
+ let port = 22;
189
+ let remaining = jumpHostStr.trim();
190
+ if (!remaining) {
191
+ throw new Error("Jump host string cannot be empty");
192
+ }
193
+ const atIndex = remaining.indexOf("@");
194
+ if (atIndex !== -1) {
195
+ const extractedUsername = remaining.substring(0, atIndex).trim();
196
+ if (extractedUsername) {
197
+ username = extractedUsername;
198
+ }
199
+ remaining = remaining.substring(atIndex + 1);
200
+ }
201
+ if (remaining.startsWith("[")) {
202
+ const closeBracket = remaining.indexOf("]");
203
+ if (closeBracket !== -1) {
204
+ host = remaining.substring(1, closeBracket);
205
+ const afterBracket = remaining.substring(closeBracket + 1);
206
+ if (afterBracket.startsWith(":")) {
207
+ const parsedPort = parseInt(afterBracket.substring(1), 10);
208
+ validatePort(parsedPort, jumpHostStr);
209
+ port = parsedPort;
210
+ }
211
+ } else {
212
+ throw new Error(`Invalid ProxyJump host "${jumpHostStr}": missing closing bracket in IPv6 address`);
213
+ }
214
+ } else {
215
+ const lastColon = remaining.lastIndexOf(":");
216
+ if (lastColon !== -1) {
217
+ const potentialPort = remaining.substring(lastColon + 1);
218
+ if (/^\d+$/.test(potentialPort)) {
219
+ host = remaining.substring(0, lastColon);
220
+ const parsedPort = parseInt(potentialPort, 10);
221
+ validatePort(parsedPort, jumpHostStr);
222
+ port = parsedPort;
223
+ } else {
224
+ host = remaining;
225
+ }
226
+ } else {
227
+ host = remaining;
228
+ }
229
+ }
230
+ if (!host) {
231
+ throw new Error(`Invalid jump host format: "${jumpHostStr}" - host cannot be empty`);
232
+ }
233
+ return { host, port, username };
234
+ }
235
+ function parseJumpHosts(proxyJump) {
236
+ if (!proxyJump || proxyJump.trim() === "" || proxyJump.toLowerCase() === "none") {
237
+ return [];
238
+ }
239
+ return proxyJump.split(",").map((s) => s.trim()).filter((s) => s.length > 0).map(parseJumpHost);
240
+ }
241
+
242
+ // src/utils/ssh-tunnel.ts
243
+ var SSHTunnel = class {
244
+ constructor() {
245
+ this.sshClients = [];
246
+ // All SSH clients in the chain
247
+ this.localServer = null;
248
+ this.tunnelInfo = null;
249
+ this.isConnected = false;
250
+ }
251
+ /**
252
+ * Establish an SSH tunnel, optionally through jump hosts (ProxyJump).
253
+ * @param config SSH connection configuration
254
+ * @param options Tunnel options including target host and port
255
+ * @returns Promise resolving to tunnel information including local port
256
+ */
257
+ async establish(config, options) {
258
+ if (this.isConnected) {
259
+ throw new Error("SSH tunnel is already established");
260
+ }
261
+ this.isConnected = true;
262
+ try {
263
+ const jumpHosts = config.proxyJump ? parseJumpHosts(config.proxyJump) : [];
264
+ let privateKeyBuffer;
265
+ if (config.privateKey) {
266
+ try {
267
+ const resolvedKeyPath = resolveSymlink(config.privateKey);
268
+ privateKeyBuffer = readFileSync2(resolvedKeyPath);
269
+ } catch (error) {
270
+ throw new Error(`Failed to read private key file: ${error instanceof Error ? error.message : String(error)}`);
271
+ }
272
+ }
273
+ if (!config.password && !privateKeyBuffer) {
274
+ throw new Error("Either password or privateKey must be provided for SSH authentication");
275
+ }
276
+ const finalClient = await this.establishChain(jumpHosts, config, privateKeyBuffer);
277
+ return await this.createLocalTunnel(finalClient, options);
278
+ } catch (error) {
279
+ this.cleanup();
280
+ throw error;
281
+ }
282
+ }
283
+ /**
284
+ * Establish a chain of SSH connections through jump hosts.
285
+ * @returns The final SSH client connected to the target host
286
+ */
287
+ async establishChain(jumpHosts, targetConfig, privateKey) {
288
+ let previousStream;
289
+ for (let i = 0; i < jumpHosts.length; i++) {
290
+ const jumpHost = jumpHosts[i];
291
+ const nextHost = i + 1 < jumpHosts.length ? jumpHosts[i + 1] : { host: targetConfig.host, port: targetConfig.port || 22 };
292
+ let client = null;
293
+ let forwardStream;
294
+ try {
295
+ client = await this.connectToHost(
296
+ {
297
+ host: jumpHost.host,
298
+ port: jumpHost.port,
299
+ username: jumpHost.username || targetConfig.username
300
+ },
301
+ targetConfig.password,
302
+ privateKey,
303
+ targetConfig.passphrase,
304
+ previousStream,
305
+ `jump host ${i + 1}`
306
+ );
307
+ console.error(` \u2192 Forwarding through ${jumpHost.host}:${jumpHost.port} to ${nextHost.host}:${nextHost.port}`);
308
+ forwardStream = await this.forwardTo(client, nextHost.host, nextHost.port);
309
+ } catch (error) {
310
+ if (client) {
311
+ try {
312
+ client.end();
313
+ } catch {
314
+ }
315
+ }
316
+ throw error;
317
+ }
318
+ this.sshClients.push(client);
319
+ previousStream = forwardStream;
320
+ }
321
+ const finalClient = await this.connectToHost(
322
+ {
323
+ host: targetConfig.host,
324
+ port: targetConfig.port || 22,
325
+ username: targetConfig.username
326
+ },
327
+ targetConfig.password,
328
+ privateKey,
329
+ targetConfig.passphrase,
330
+ previousStream,
331
+ jumpHosts.length > 0 ? "target host" : void 0
332
+ );
333
+ this.sshClients.push(finalClient);
334
+ return finalClient;
335
+ }
336
+ /**
337
+ * Connect to a single SSH host.
338
+ */
339
+ connectToHost(hostInfo, password, privateKey, passphrase, sock, label) {
340
+ return new Promise((resolve, reject) => {
341
+ const client = new Client();
342
+ const sshConfig = {
343
+ host: hostInfo.host,
344
+ port: hostInfo.port,
345
+ username: hostInfo.username
346
+ };
347
+ if (password) {
348
+ sshConfig.password = password;
349
+ }
350
+ if (privateKey) {
351
+ sshConfig.privateKey = privateKey;
352
+ if (passphrase) {
353
+ sshConfig.passphrase = passphrase;
354
+ }
355
+ }
356
+ if (sock) {
357
+ sshConfig.sock = sock;
358
+ }
359
+ const onError = (err) => {
360
+ client.removeListener("ready", onReady);
361
+ client.destroy();
362
+ reject(new Error(`SSH connection error${label ? ` (${label})` : ""}: ${err.message}`));
363
+ };
364
+ const onReady = () => {
365
+ client.removeListener("error", onError);
366
+ const desc = label || `${hostInfo.host}:${hostInfo.port}`;
367
+ console.error(`SSH connection established: ${desc}`);
368
+ resolve(client);
369
+ };
370
+ client.on("error", onError);
371
+ client.on("ready", onReady);
372
+ client.connect(sshConfig);
373
+ });
374
+ }
375
+ /**
376
+ * Forward a connection through an SSH client to a target host.
377
+ */
378
+ forwardTo(client, targetHost, targetPort) {
379
+ return new Promise((resolve, reject) => {
380
+ client.forwardOut("127.0.0.1", 0, targetHost, targetPort, (err, stream) => {
381
+ if (err) {
382
+ reject(new Error(`SSH forward error: ${err.message}`));
383
+ return;
384
+ }
385
+ resolve(stream);
386
+ });
387
+ });
388
+ }
389
+ /**
390
+ * Create the local server that tunnels connections to the database.
391
+ */
392
+ createLocalTunnel(sshClient, options) {
393
+ return new Promise((resolve, reject) => {
394
+ let settled = false;
395
+ this.localServer = createServer((localSocket) => {
396
+ sshClient.forwardOut(
397
+ "127.0.0.1",
398
+ 0,
399
+ options.targetHost,
400
+ options.targetPort,
401
+ (err, stream) => {
402
+ if (err) {
403
+ console.error("SSH forward error:", err);
404
+ localSocket.end();
405
+ return;
406
+ }
407
+ localSocket.pipe(stream).pipe(localSocket);
408
+ stream.on("error", (err2) => {
409
+ console.error("SSH stream error:", err2);
410
+ localSocket.end();
411
+ });
412
+ localSocket.on("error", (err2) => {
413
+ console.error("Local socket error:", err2);
414
+ stream.end();
415
+ });
416
+ }
417
+ );
418
+ });
419
+ this.localServer.on("error", (err) => {
420
+ if (!settled) {
421
+ settled = true;
422
+ reject(new Error(`Local server error: ${err.message}`));
423
+ } else {
424
+ console.error("Local server error after tunnel established:", err);
425
+ this.cleanup();
426
+ }
427
+ });
428
+ const localPort = options.localPort || 0;
429
+ this.localServer.listen(localPort, "127.0.0.1", () => {
430
+ const address = this.localServer.address();
431
+ if (!address || typeof address === "string") {
432
+ if (!settled) {
433
+ settled = true;
434
+ reject(new Error("Failed to get local server address"));
435
+ }
436
+ return;
437
+ }
438
+ this.tunnelInfo = {
439
+ localPort: address.port,
440
+ targetHost: options.targetHost,
441
+ targetPort: options.targetPort
442
+ };
443
+ console.error(`SSH tunnel established: localhost:${address.port} \u2192 ${options.targetHost}:${options.targetPort}`);
444
+ settled = true;
445
+ resolve(this.tunnelInfo);
446
+ });
447
+ });
448
+ }
449
+ /**
450
+ * Close the SSH tunnel and clean up resources
451
+ */
452
+ async close() {
453
+ if (!this.isConnected) {
454
+ return;
455
+ }
456
+ return new Promise((resolve) => {
457
+ this.cleanup();
458
+ console.error("SSH tunnel closed");
459
+ resolve();
460
+ });
461
+ }
462
+ /**
463
+ * Clean up resources. Closes all SSH clients in reverse order (innermost first).
464
+ */
465
+ cleanup() {
466
+ if (this.localServer) {
467
+ this.localServer.close();
468
+ this.localServer = null;
469
+ }
470
+ for (let i = this.sshClients.length - 1; i >= 0; i--) {
471
+ try {
472
+ this.sshClients[i].end();
473
+ } catch {
474
+ }
475
+ }
476
+ this.sshClients = [];
477
+ this.tunnelInfo = null;
478
+ this.isConnected = false;
479
+ }
480
+ /**
481
+ * Get current tunnel information
482
+ */
483
+ getTunnelInfo() {
484
+ return this.tunnelInfo;
485
+ }
486
+ /**
487
+ * Check if tunnel is connected
488
+ */
489
+ getIsConnected() {
490
+ return this.isConnected;
491
+ }
492
+ };
493
+
494
+ // src/config/toml-loader.ts
495
+ import fs2 from "fs";
496
+ import path2 from "path";
497
+ import { homedir as homedir3 } from "os";
498
+ import toml from "@iarna/toml";
499
+
500
+ // src/config/env.ts
501
+ import dotenv from "dotenv";
502
+ import path from "path";
503
+ import fs from "fs";
504
+ import { fileURLToPath } from "url";
505
+ import { homedir as homedir2 } from "os";
506
+
507
+ // src/utils/safe-url.ts
508
+ var SafeURL = class {
509
+ /**
510
+ * Parse a URL and handle special characters in passwords
511
+ * This is a safe alternative to the URL constructor
512
+ *
513
+ * @param urlString - The DSN string to parse
514
+ */
515
+ constructor(urlString) {
516
+ this.protocol = "";
517
+ this.hostname = "";
518
+ this.port = "";
519
+ this.pathname = "";
520
+ this.username = "";
521
+ this.password = "";
522
+ this.searchParams = /* @__PURE__ */ new Map();
523
+ if (!urlString || urlString.trim() === "") {
524
+ throw new Error("URL string cannot be empty");
525
+ }
526
+ try {
527
+ const protocolSeparator = urlString.indexOf("://");
528
+ if (protocolSeparator !== -1) {
529
+ this.protocol = urlString.substring(0, protocolSeparator + 1);
530
+ urlString = urlString.substring(protocolSeparator + 3);
531
+ } else {
532
+ throw new Error('Invalid URL format: missing protocol (e.g., "mysql://")');
533
+ }
534
+ const questionMarkIndex = urlString.indexOf("?");
535
+ let queryParams = "";
536
+ if (questionMarkIndex !== -1) {
537
+ queryParams = urlString.substring(questionMarkIndex + 1);
538
+ urlString = urlString.substring(0, questionMarkIndex);
539
+ queryParams.split("&").forEach((pair) => {
540
+ const parts = pair.split("=");
541
+ if (parts.length === 2 && parts[0] && parts[1]) {
542
+ this.searchParams.set(parts[0], decodeURIComponent(parts[1]));
543
+ }
544
+ });
545
+ }
546
+ const atIndex = urlString.indexOf("@");
547
+ if (atIndex !== -1) {
548
+ const auth = urlString.substring(0, atIndex);
549
+ urlString = urlString.substring(atIndex + 1);
550
+ const colonIndex2 = auth.indexOf(":");
551
+ if (colonIndex2 !== -1) {
552
+ this.username = auth.substring(0, colonIndex2);
553
+ this.password = auth.substring(colonIndex2 + 1);
554
+ this.username = decodeURIComponent(this.username);
555
+ this.password = decodeURIComponent(this.password);
556
+ } else {
557
+ this.username = auth;
558
+ }
559
+ }
560
+ const pathSeparatorIndex = urlString.indexOf("/");
561
+ if (pathSeparatorIndex !== -1) {
562
+ this.pathname = urlString.substring(pathSeparatorIndex);
563
+ urlString = urlString.substring(0, pathSeparatorIndex);
564
+ }
565
+ const colonIndex = urlString.indexOf(":");
566
+ if (colonIndex !== -1) {
567
+ this.hostname = urlString.substring(0, colonIndex);
568
+ this.port = urlString.substring(colonIndex + 1);
569
+ } else {
570
+ this.hostname = urlString;
571
+ }
572
+ if (this.protocol === "") {
573
+ throw new Error("Invalid URL: protocol is required");
574
+ }
575
+ } catch (error) {
576
+ throw new Error(`Failed to parse URL: ${error instanceof Error ? error.message : String(error)}`);
577
+ }
578
+ }
579
+ /**
580
+ * Helper method to safely get a parameter from query string
581
+ *
582
+ * @param name - The parameter name to retrieve
583
+ * @returns The parameter value or null if not found
584
+ */
585
+ getSearchParam(name) {
586
+ return this.searchParams.has(name) ? this.searchParams.get(name) : null;
587
+ }
588
+ /**
589
+ * Helper method to iterate over all parameters
590
+ *
591
+ * @param callback - Function to call for each parameter
592
+ */
593
+ forEachSearchParam(callback) {
594
+ this.searchParams.forEach((value, key) => callback(value, key));
595
+ }
596
+ };
597
+
598
+ // src/utils/dsn-obfuscate.ts
599
+ function parseConnectionInfoFromDSN(dsn) {
600
+ if (!dsn) {
601
+ return null;
602
+ }
603
+ try {
604
+ const type = getDatabaseTypeFromDSN(dsn);
605
+ if (typeof type === "undefined") {
606
+ return null;
607
+ }
608
+ if (type === "sqlite") {
609
+ const prefix = "sqlite:///";
610
+ if (dsn.length > prefix.length) {
611
+ const rawPath = dsn.substring(prefix.length);
612
+ const firstChar = rawPath[0];
613
+ const isWindowsDrive = rawPath.length > 1 && rawPath[1] === ":";
614
+ const isSpecialPath = firstChar === ":" || firstChar === "." || firstChar === "~" || isWindowsDrive;
615
+ return {
616
+ type,
617
+ database: isSpecialPath ? rawPath : "/" + rawPath
618
+ };
619
+ }
620
+ return { type };
621
+ }
622
+ const url = new SafeURL(dsn);
623
+ const info = { type };
624
+ if (url.hostname) {
625
+ info.host = url.hostname;
626
+ }
627
+ if (url.port) {
628
+ info.port = parseInt(url.port, 10);
629
+ }
630
+ if (url.pathname && url.pathname.length > 1) {
631
+ info.database = url.pathname.substring(1);
632
+ }
633
+ if (url.username) {
634
+ info.user = url.username;
635
+ }
636
+ return info;
637
+ } catch {
638
+ return null;
639
+ }
640
+ }
641
+ function obfuscateDSNPassword(dsn) {
642
+ if (!dsn) {
643
+ return dsn;
644
+ }
645
+ try {
646
+ const type = getDatabaseTypeFromDSN(dsn);
647
+ if (type === "sqlite") {
648
+ return dsn;
649
+ }
650
+ const url = new SafeURL(dsn);
651
+ if (!url.password) {
652
+ return dsn;
653
+ }
654
+ const obfuscatedPassword = "*".repeat(Math.min(url.password.length, 8));
655
+ const protocol = dsn.split(":")[0];
656
+ let result;
657
+ if (url.username) {
658
+ result = `${protocol}://${url.username}:${obfuscatedPassword}@${url.hostname}`;
659
+ } else {
660
+ result = `${protocol}://${obfuscatedPassword}@${url.hostname}`;
661
+ }
662
+ if (url.port) {
663
+ result += `:${url.port}`;
664
+ }
665
+ result += url.pathname;
666
+ if (url.searchParams.size > 0) {
667
+ const params = [];
668
+ url.forEachSearchParam((value, key) => {
669
+ params.push(`${encodeURIComponent(key)}=${encodeURIComponent(value)}`);
670
+ });
671
+ result += `?${params.join("&")}`;
672
+ }
673
+ return result;
674
+ } catch {
675
+ return dsn;
676
+ }
677
+ }
678
+ function getDatabaseTypeFromDSN(dsn) {
679
+ if (!dsn) {
680
+ return void 0;
681
+ }
682
+ const protocol = dsn.split(":")[0];
683
+ return protocolToConnectorType(protocol);
684
+ }
685
+ function protocolToConnectorType(protocol) {
686
+ const mapping = {
687
+ "postgres": "postgres",
688
+ "postgresql": "postgres",
689
+ "mysql": "mysql",
690
+ "mariadb": "mariadb",
691
+ "sqlserver": "sqlserver",
692
+ "sqlite": "sqlite"
693
+ };
694
+ return mapping[protocol];
695
+ }
696
+ function getDefaultPortForType(type) {
697
+ const ports = {
698
+ "postgres": 5432,
699
+ "mysql": 3306,
700
+ "mariadb": 3306,
701
+ "sqlserver": 1433,
702
+ "sqlite": void 0
703
+ };
704
+ return ports[type];
705
+ }
706
+
707
+ // src/config/env.ts
708
+ var __filename = fileURLToPath(import.meta.url);
709
+ var __dirname = path.dirname(__filename);
710
+ function parseCommandLineArgs() {
711
+ const args = process.argv.slice(2);
712
+ const parsedManually = {};
713
+ for (let i = 0; i < args.length; i++) {
714
+ const arg = args[i];
715
+ if (arg.startsWith("--")) {
716
+ const parts = arg.substring(2).split("=");
717
+ const key = parts[0];
718
+ if (key === "readonly") {
719
+ console.error("\nERROR: --readonly flag is no longer supported.");
720
+ console.error("Use dbhub.toml with [[tools]] configuration instead:\n");
721
+ console.error(" [[sources]]");
722
+ console.error(' id = "default"');
723
+ console.error(' dsn = "..."\n');
724
+ console.error(" [[tools]]");
725
+ console.error(' name = "execute_sql"');
726
+ console.error(' source = "default"');
727
+ console.error(" readonly = true\n");
728
+ console.error("See https://dbhub.ai/tools/execute-sql#read-only-mode for details.\n");
729
+ process.exit(1);
730
+ }
731
+ if (key === "max-rows") {
732
+ console.error("\nERROR: --max-rows flag is no longer supported.");
733
+ console.error("Use dbhub.toml with [[tools]] configuration instead:\n");
734
+ console.error(" [[sources]]");
735
+ console.error(' id = "default"');
736
+ console.error(' dsn = "..."\n');
737
+ console.error(" [[tools]]");
738
+ console.error(' name = "execute_sql"');
739
+ console.error(' source = "default"');
740
+ console.error(" max_rows = 1000\n");
741
+ console.error("See https://dbhub.ai/tools/execute-sql#row-limiting for details.\n");
742
+ process.exit(1);
743
+ }
744
+ const value = parts.length > 1 ? parts.slice(1).join("=") : void 0;
745
+ if (value) {
746
+ parsedManually[key] = value;
747
+ } else if (i + 1 < args.length && !args[i + 1].startsWith("--")) {
748
+ parsedManually[key] = args[i + 1];
749
+ i++;
750
+ } else {
751
+ parsedManually[key] = "true";
752
+ }
753
+ }
754
+ }
755
+ return parsedManually;
756
+ }
757
+ function loadEnvFiles() {
758
+ const isDevelopment = process.env.NODE_ENV === "development" || process.argv[1]?.includes("tsx");
759
+ const envFileNames = isDevelopment ? [".env.local", ".env"] : [".env"];
760
+ const envPaths = [];
761
+ for (const fileName of envFileNames) {
762
+ envPaths.push(
763
+ fileName,
764
+ // Current working directory
765
+ path.join(__dirname, "..", "..", fileName),
766
+ // Two levels up (src/config -> src -> root)
767
+ path.join(process.cwd(), fileName)
768
+ // Explicit current working directory
769
+ );
770
+ }
771
+ for (const envPath of envPaths) {
772
+ console.error(`Checking for env file: ${envPath}`);
773
+ if (fs.existsSync(envPath)) {
774
+ dotenv.config({ path: envPath });
775
+ if (process.env.READONLY !== void 0) {
776
+ console.error("\nERROR: READONLY environment variable is no longer supported.");
777
+ console.error("Use dbhub.toml with [[tools]] configuration instead:\n");
778
+ console.error(" [[sources]]");
779
+ console.error(' id = "default"');
780
+ console.error(' dsn = "..."\n');
781
+ console.error(" [[tools]]");
782
+ console.error(' name = "execute_sql"');
783
+ console.error(' source = "default"');
784
+ console.error(" readonly = true\n");
785
+ console.error("See https://dbhub.ai/tools/execute-sql#read-only-mode for details.\n");
786
+ process.exit(1);
787
+ }
788
+ if (process.env.MAX_ROWS !== void 0) {
789
+ console.error("\nERROR: MAX_ROWS environment variable is no longer supported.");
790
+ console.error("Use dbhub.toml with [[tools]] configuration instead:\n");
791
+ console.error(" [[sources]]");
792
+ console.error(' id = "default"');
793
+ console.error(' dsn = "..."\n');
794
+ console.error(" [[tools]]");
795
+ console.error(' name = "execute_sql"');
796
+ console.error(' source = "default"');
797
+ console.error(" max_rows = 1000\n");
798
+ console.error("See https://dbhub.ai/tools/execute-sql#row-limiting for details.\n");
799
+ process.exit(1);
800
+ }
801
+ return path.basename(envPath);
802
+ }
803
+ }
804
+ return null;
805
+ }
806
+ function isDemoMode() {
807
+ const args = parseCommandLineArgs();
808
+ return args.demo === "true";
809
+ }
810
+ function buildDSNFromEnvParams() {
811
+ const dbType = process.env.DB_TYPE;
812
+ const dbHost = process.env.DB_HOST;
813
+ const dbUser = process.env.DB_USER;
814
+ const dbPassword = process.env.DB_PASSWORD;
815
+ const dbName = process.env.DB_NAME;
816
+ const dbPort = process.env.DB_PORT;
817
+ if (dbType?.toLowerCase() === "sqlite") {
818
+ if (!dbName) {
819
+ return null;
820
+ }
821
+ } else {
822
+ if (!dbType || !dbHost || !dbUser || !dbPassword || !dbName) {
823
+ return null;
824
+ }
825
+ }
826
+ const supportedTypes = ["postgres", "postgresql", "mysql", "mariadb", "sqlserver", "sqlite"];
827
+ if (!supportedTypes.includes(dbType.toLowerCase())) {
828
+ throw new Error(`Unsupported DB_TYPE: ${dbType}. Supported types: ${supportedTypes.join(", ")}`);
829
+ }
830
+ let port = dbPort;
831
+ if (!port) {
832
+ switch (dbType.toLowerCase()) {
833
+ case "postgres":
834
+ case "postgresql":
835
+ port = "5432";
836
+ break;
837
+ case "mysql":
838
+ case "mariadb":
839
+ port = "3306";
840
+ break;
841
+ case "sqlserver":
842
+ port = "1433";
843
+ break;
844
+ case "sqlite":
845
+ return {
846
+ dsn: `sqlite:///${dbName}`,
847
+ source: "individual environment variables"
848
+ };
849
+ default:
850
+ throw new Error(`Unknown database type for port determination: ${dbType}`);
851
+ }
852
+ }
853
+ const user = dbUser;
854
+ const password = dbPassword;
855
+ const dbNameStr = dbName;
856
+ const encodedUser = encodeURIComponent(user);
857
+ const encodedPassword = encodeURIComponent(password);
858
+ const encodedDbName = encodeURIComponent(dbNameStr);
859
+ const protocol = dbType.toLowerCase() === "postgresql" ? "postgres" : dbType.toLowerCase();
860
+ const dsn = `${protocol}://${encodedUser}:${encodedPassword}@${dbHost}:${port}/${encodedDbName}`;
861
+ return {
862
+ dsn,
863
+ source: "individual environment variables"
864
+ };
865
+ }
866
+ function resolveDSN() {
867
+ const args = parseCommandLineArgs();
868
+ if (isDemoMode()) {
869
+ return {
870
+ dsn: "sqlite:///:memory:",
871
+ source: "demo mode",
872
+ isDemo: true
873
+ };
874
+ }
875
+ if (args.dsn) {
876
+ return { dsn: args.dsn, source: "command line argument" };
877
+ }
878
+ if (process.env.DSN) {
879
+ return { dsn: process.env.DSN, source: "environment variable" };
880
+ }
881
+ const envParamsResult = buildDSNFromEnvParams();
882
+ if (envParamsResult) {
883
+ return envParamsResult;
884
+ }
885
+ const loadedEnvFile = loadEnvFiles();
886
+ if (loadedEnvFile && process.env.DSN) {
887
+ return { dsn: process.env.DSN, source: `${loadedEnvFile} file` };
888
+ }
889
+ if (loadedEnvFile) {
890
+ const envFileParamsResult = buildDSNFromEnvParams();
891
+ if (envFileParamsResult) {
892
+ return {
893
+ dsn: envFileParamsResult.dsn,
894
+ source: `${loadedEnvFile} file (individual parameters)`
895
+ };
896
+ }
897
+ }
898
+ return null;
899
+ }
900
+ function resolveTransport() {
901
+ const args = parseCommandLineArgs();
902
+ if (args.transport) {
903
+ const type = args.transport === "http" ? "http" : "stdio";
904
+ return { type, source: "command line argument" };
905
+ }
906
+ if (process.env.TRANSPORT) {
907
+ const type = process.env.TRANSPORT === "http" ? "http" : "stdio";
908
+ return { type, source: "environment variable" };
909
+ }
910
+ return { type: "stdio", source: "default" };
911
+ }
912
+ function resolvePort() {
913
+ const args = parseCommandLineArgs();
914
+ if (args.port) {
915
+ const port = parseInt(args.port, 10);
916
+ return { port, source: "command line argument" };
917
+ }
918
+ if (process.env.PORT) {
919
+ const port = parseInt(process.env.PORT, 10);
920
+ return { port, source: "environment variable" };
921
+ }
922
+ return { port: 8080, source: "default" };
923
+ }
924
+ function redactDSN(dsn) {
925
+ try {
926
+ const url = new URL(dsn);
927
+ if (url.password) {
928
+ url.password = "*******";
929
+ }
930
+ return url.toString();
931
+ } catch (error) {
932
+ return dsn.replace(/\/\/([^:]+):([^@]+)@/, "//$1:***@");
933
+ }
934
+ }
935
+ function resolveId() {
936
+ const args = parseCommandLineArgs();
937
+ if (args.id) {
938
+ return { id: args.id, source: "command line argument" };
939
+ }
940
+ if (process.env.ID) {
941
+ return { id: process.env.ID, source: "environment variable" };
942
+ }
943
+ return null;
944
+ }
945
+ function resolveSSHConfig() {
946
+ const args = parseCommandLineArgs();
947
+ const hasSSHArgs = args["ssh-host"] || process.env.SSH_HOST;
948
+ if (!hasSSHArgs) {
949
+ return null;
950
+ }
951
+ let config = {};
952
+ let sources = [];
953
+ let sshConfigHost;
954
+ if (args["ssh-host"]) {
955
+ sshConfigHost = args["ssh-host"];
956
+ config.host = args["ssh-host"];
957
+ sources.push("ssh-host from command line");
958
+ } else if (process.env.SSH_HOST) {
959
+ sshConfigHost = process.env.SSH_HOST;
960
+ config.host = process.env.SSH_HOST;
961
+ sources.push("SSH_HOST from environment");
962
+ }
963
+ if (sshConfigHost && looksLikeSSHAlias(sshConfigHost)) {
964
+ const sshConfigPath = path.join(homedir2(), ".ssh", "config");
965
+ console.error(`Attempting to parse SSH config for host '${sshConfigHost}' from: ${sshConfigPath}`);
966
+ const sshConfigData = parseSSHConfig(sshConfigHost, sshConfigPath);
967
+ if (sshConfigData) {
968
+ config = { ...sshConfigData };
969
+ sources.push(`SSH config for host '${sshConfigHost}'`);
970
+ }
971
+ }
972
+ if (args["ssh-port"]) {
973
+ config.port = parseInt(args["ssh-port"], 10);
974
+ sources.push("ssh-port from command line");
975
+ } else if (process.env.SSH_PORT) {
976
+ config.port = parseInt(process.env.SSH_PORT, 10);
977
+ sources.push("SSH_PORT from environment");
978
+ }
979
+ if (args["ssh-user"]) {
980
+ config.username = args["ssh-user"];
981
+ sources.push("ssh-user from command line");
982
+ } else if (process.env.SSH_USER) {
983
+ config.username = process.env.SSH_USER;
984
+ sources.push("SSH_USER from environment");
985
+ }
986
+ if (args["ssh-password"]) {
987
+ config.password = args["ssh-password"];
988
+ sources.push("ssh-password from command line");
989
+ } else if (process.env.SSH_PASSWORD) {
990
+ config.password = process.env.SSH_PASSWORD;
991
+ sources.push("SSH_PASSWORD from environment");
992
+ }
993
+ if (args["ssh-key"]) {
994
+ config.privateKey = args["ssh-key"];
995
+ if (config.privateKey.startsWith("~/")) {
996
+ config.privateKey = path.join(process.env.HOME || "", config.privateKey.substring(2));
997
+ }
998
+ sources.push("ssh-key from command line");
999
+ } else if (process.env.SSH_KEY) {
1000
+ config.privateKey = process.env.SSH_KEY;
1001
+ if (config.privateKey.startsWith("~/")) {
1002
+ config.privateKey = path.join(process.env.HOME || "", config.privateKey.substring(2));
1003
+ }
1004
+ sources.push("SSH_KEY from environment");
1005
+ }
1006
+ if (args["ssh-passphrase"]) {
1007
+ config.passphrase = args["ssh-passphrase"];
1008
+ sources.push("ssh-passphrase from command line");
1009
+ } else if (process.env.SSH_PASSPHRASE) {
1010
+ config.passphrase = process.env.SSH_PASSPHRASE;
1011
+ sources.push("SSH_PASSPHRASE from environment");
1012
+ }
1013
+ if (args["ssh-proxy-jump"]) {
1014
+ config.proxyJump = args["ssh-proxy-jump"];
1015
+ sources.push("ssh-proxy-jump from command line");
1016
+ } else if (process.env.SSH_PROXY_JUMP) {
1017
+ config.proxyJump = process.env.SSH_PROXY_JUMP;
1018
+ sources.push("SSH_PROXY_JUMP from environment");
1019
+ }
1020
+ if (!config.host || !config.username) {
1021
+ throw new Error("SSH tunnel configuration requires at least --ssh-host and --ssh-user");
1022
+ }
1023
+ if (!config.password && !config.privateKey) {
1024
+ throw new Error("SSH tunnel configuration requires either --ssh-password or --ssh-key for authentication");
1025
+ }
1026
+ return {
1027
+ config,
1028
+ source: sources.join(", ")
1029
+ };
1030
+ }
1031
+ async function resolveSourceConfigs() {
1032
+ if (!isDemoMode()) {
1033
+ const tomlConfig = loadTomlConfig();
1034
+ if (tomlConfig) {
1035
+ const idData = resolveId();
1036
+ if (idData) {
1037
+ throw new Error(
1038
+ "The --id flag cannot be used with TOML configuration. TOML config defines source IDs directly. Either remove the --id flag or use command-line DSN configuration instead."
1039
+ );
1040
+ }
1041
+ return tomlConfig;
1042
+ }
1043
+ }
1044
+ const dsnResult = resolveDSN();
1045
+ if (dsnResult) {
1046
+ let dsnUrl;
1047
+ try {
1048
+ dsnUrl = new SafeURL(dsnResult.dsn);
1049
+ } catch (error) {
1050
+ throw new Error(
1051
+ `Invalid DSN format: ${dsnResult.dsn}. Expected format: protocol://[user[:password]@]host[:port]/database`
1052
+ );
1053
+ }
1054
+ const protocol = dsnUrl.protocol.replace(":", "");
1055
+ let dbType;
1056
+ if (protocol === "postgresql" || protocol === "postgres") {
1057
+ dbType = "postgres";
1058
+ } else if (protocol === "mysql") {
1059
+ dbType = "mysql";
1060
+ } else if (protocol === "mariadb") {
1061
+ dbType = "mariadb";
1062
+ } else if (protocol === "sqlserver") {
1063
+ dbType = "sqlserver";
1064
+ } else if (protocol === "sqlite") {
1065
+ dbType = "sqlite";
1066
+ } else {
1067
+ throw new Error(`Unsupported database type in DSN: ${protocol}`);
1068
+ }
1069
+ const idData = resolveId();
1070
+ const sourceId = idData?.id || "default";
1071
+ const source = {
1072
+ id: sourceId,
1073
+ type: dbType,
1074
+ dsn: dsnResult.dsn
1075
+ };
1076
+ const connectionInfo = parseConnectionInfoFromDSN(dsnResult.dsn);
1077
+ if (connectionInfo) {
1078
+ if (connectionInfo.host) {
1079
+ source.host = connectionInfo.host;
1080
+ }
1081
+ if (connectionInfo.port !== void 0) {
1082
+ source.port = connectionInfo.port;
1083
+ }
1084
+ if (connectionInfo.database) {
1085
+ source.database = connectionInfo.database;
1086
+ }
1087
+ if (connectionInfo.user) {
1088
+ source.user = connectionInfo.user;
1089
+ }
1090
+ }
1091
+ const sshResult = resolveSSHConfig();
1092
+ if (sshResult) {
1093
+ source.ssh_host = sshResult.config.host;
1094
+ source.ssh_port = sshResult.config.port;
1095
+ source.ssh_user = sshResult.config.username;
1096
+ source.ssh_password = sshResult.config.password;
1097
+ source.ssh_key = sshResult.config.privateKey;
1098
+ source.ssh_passphrase = sshResult.config.passphrase;
1099
+ }
1100
+ if (dsnResult.isDemo) {
1101
+ const { getSqliteInMemorySetupSql } = await import("./demo-loader-PSMTLZ2T.js");
1102
+ source.init_script = getSqliteInMemorySetupSql();
1103
+ }
1104
+ return {
1105
+ sources: [source],
1106
+ tools: [],
1107
+ source: dsnResult.isDemo ? "demo mode" : dsnResult.source
1108
+ };
1109
+ }
1110
+ return null;
1111
+ }
1112
+
1113
+ // src/config/toml-loader.ts
1114
+ function loadTomlConfig() {
1115
+ const configPath = resolveTomlConfigPath();
1116
+ if (!configPath) {
1117
+ return null;
1118
+ }
1119
+ try {
1120
+ const fileContent = fs2.readFileSync(configPath, "utf-8");
1121
+ const parsedToml = toml.parse(fileContent);
1122
+ if (!Array.isArray(parsedToml.sources)) {
1123
+ throw new Error(
1124
+ `Configuration file ${configPath}: must contain a [[sources]] array. Use [[sources]] syntax for array of tables in TOML.`
1125
+ );
1126
+ }
1127
+ const sources = processSourceConfigs(parsedToml.sources, configPath);
1128
+ validateTomlConfig({ ...parsedToml, sources }, configPath);
1129
+ return {
1130
+ sources,
1131
+ tools: parsedToml.tools,
1132
+ source: path2.basename(configPath)
1133
+ };
1134
+ } catch (error) {
1135
+ if (error instanceof Error) {
1136
+ throw new Error(
1137
+ `Failed to load TOML configuration from ${configPath}: ${error.message}`
1138
+ );
1139
+ }
1140
+ throw error;
1141
+ }
1142
+ }
1143
+ function resolveTomlConfigPath() {
1144
+ const args = parseCommandLineArgs();
1145
+ if (args.config) {
1146
+ const configPath = expandHomeDir(args.config);
1147
+ if (!fs2.existsSync(configPath)) {
1148
+ throw new Error(
1149
+ `Configuration file specified by --config flag not found: ${configPath}`
1150
+ );
1151
+ }
1152
+ return configPath;
1153
+ }
1154
+ const defaultConfigPath = path2.join(process.cwd(), "dbhub.toml");
1155
+ if (fs2.existsSync(defaultConfigPath)) {
1156
+ return defaultConfigPath;
1157
+ }
1158
+ return null;
1159
+ }
1160
+ function validateTomlConfig(config, configPath) {
1161
+ if (!config.sources) {
1162
+ throw new Error(
1163
+ `Configuration file ${configPath} must contain a [[sources]] array. Example:
1164
+
1165
+ [[sources]]
1166
+ id = "my_db"
1167
+ dsn = "postgres://..."`
1168
+ );
1169
+ }
1170
+ if (config.sources.length === 0) {
1171
+ throw new Error(
1172
+ `Configuration file ${configPath}: sources array cannot be empty. Please define at least one source with [[sources]].`
1173
+ );
1174
+ }
1175
+ const ids = /* @__PURE__ */ new Set();
1176
+ const duplicates = [];
1177
+ for (const source of config.sources) {
1178
+ if (!source.id) {
1179
+ throw new Error(
1180
+ `Configuration file ${configPath}: each source must have an 'id' field. Example: [[sources]]
1181
+ id = "my_db"`
1182
+ );
1183
+ }
1184
+ if (ids.has(source.id)) {
1185
+ duplicates.push(source.id);
1186
+ } else {
1187
+ ids.add(source.id);
1188
+ }
1189
+ }
1190
+ if (duplicates.length > 0) {
1191
+ throw new Error(
1192
+ `Configuration file ${configPath}: duplicate source IDs found: ${duplicates.join(", ")}. Each source must have a unique 'id' field.`
1193
+ );
1194
+ }
1195
+ for (const source of config.sources) {
1196
+ validateSourceConfig(source, configPath);
1197
+ }
1198
+ if (config.tools) {
1199
+ validateToolsConfig(config.tools, config.sources, configPath);
1200
+ }
1201
+ }
1202
+ function validateToolsConfig(tools, sources, configPath) {
1203
+ const toolSourcePairs = /* @__PURE__ */ new Set();
1204
+ for (const tool of tools) {
1205
+ if (!tool.name) {
1206
+ throw new Error(
1207
+ `Configuration file ${configPath}: all tools must have a 'name' field`
1208
+ );
1209
+ }
1210
+ if (!tool.source) {
1211
+ throw new Error(
1212
+ `Configuration file ${configPath}: tool '${tool.name}' must have a 'source' field`
1213
+ );
1214
+ }
1215
+ const pairKey = `${tool.name}:${tool.source}`;
1216
+ if (toolSourcePairs.has(pairKey)) {
1217
+ throw new Error(
1218
+ `Configuration file ${configPath}: duplicate tool configuration found for '${tool.name}' on source '${tool.source}'`
1219
+ );
1220
+ }
1221
+ toolSourcePairs.add(pairKey);
1222
+ if (!sources.some((s) => s.id === tool.source)) {
1223
+ throw new Error(
1224
+ `Configuration file ${configPath}: tool '${tool.name}' references unknown source '${tool.source}'`
1225
+ );
1226
+ }
1227
+ const isBuiltin = BUILTIN_TOOLS.includes(tool.name);
1228
+ const isExecuteSql = tool.name === BUILTIN_TOOL_EXECUTE_SQL;
1229
+ if (isBuiltin) {
1230
+ if (tool.description || tool.statement || tool.parameters) {
1231
+ throw new Error(
1232
+ `Configuration file ${configPath}: built-in tool '${tool.name}' cannot have description, statement, or parameters fields`
1233
+ );
1234
+ }
1235
+ if (!isExecuteSql && (tool.readonly !== void 0 || tool.max_rows !== void 0)) {
1236
+ throw new Error(
1237
+ `Configuration file ${configPath}: tool '${tool.name}' cannot have readonly or max_rows fields (these are only valid for ${BUILTIN_TOOL_EXECUTE_SQL} tool)`
1238
+ );
1239
+ }
1240
+ } else {
1241
+ if (!tool.description || !tool.statement) {
1242
+ throw new Error(
1243
+ `Configuration file ${configPath}: custom tool '${tool.name}' must have 'description' and 'statement' fields`
1244
+ );
1245
+ }
1246
+ }
1247
+ if (tool.max_rows !== void 0) {
1248
+ if (typeof tool.max_rows !== "number" || tool.max_rows <= 0) {
1249
+ throw new Error(
1250
+ `Configuration file ${configPath}: tool '${tool.name}' has invalid max_rows. Must be a positive integer.`
1251
+ );
1252
+ }
1253
+ }
1254
+ if (tool.readonly !== void 0 && typeof tool.readonly !== "boolean") {
1255
+ throw new Error(
1256
+ `Configuration file ${configPath}: tool '${tool.name}' has invalid readonly. Must be a boolean (true or false).`
1257
+ );
1258
+ }
1259
+ }
1260
+ }
1261
+ function validateSourceConfig(source, configPath) {
1262
+ const hasConnectionParams = source.type && (source.type === "sqlite" ? source.database : source.host);
1263
+ if (!source.dsn && !hasConnectionParams) {
1264
+ throw new Error(
1265
+ `Configuration file ${configPath}: source '${source.id}' must have either:
1266
+ - 'dsn' field (e.g., dsn = "postgres://user:pass@host:5432/dbname")
1267
+ - OR connection parameters (type, host, database, user, password)
1268
+ - For SQLite: type = "sqlite" and database path`
1269
+ );
1270
+ }
1271
+ if (source.type) {
1272
+ const validTypes = ["postgres", "mysql", "mariadb", "sqlserver", "sqlite", "redis", "elasticsearch"];
1273
+ if (!validTypes.includes(source.type)) {
1274
+ throw new Error(
1275
+ `Configuration file ${configPath}: source '${source.id}' has invalid type '${source.type}'. Valid types: ${validTypes.join(", ")}`
1276
+ );
1277
+ }
1278
+ }
1279
+ if (source.connection_timeout !== void 0) {
1280
+ if (typeof source.connection_timeout !== "number" || source.connection_timeout <= 0) {
1281
+ throw new Error(
1282
+ `Configuration file ${configPath}: source '${source.id}' has invalid connection_timeout. Must be a positive number (in seconds).`
1283
+ );
1284
+ }
1285
+ }
1286
+ if (source.query_timeout !== void 0) {
1287
+ if (typeof source.query_timeout !== "number" || source.query_timeout <= 0) {
1288
+ throw new Error(
1289
+ `Configuration file ${configPath}: source '${source.id}' has invalid query_timeout. Must be a positive number (in seconds).`
1290
+ );
1291
+ }
1292
+ }
1293
+ if (source.ssh_port !== void 0) {
1294
+ if (typeof source.ssh_port !== "number" || source.ssh_port <= 0 || source.ssh_port > 65535) {
1295
+ throw new Error(
1296
+ `Configuration file ${configPath}: source '${source.id}' has invalid ssh_port. Must be between 1 and 65535.`
1297
+ );
1298
+ }
1299
+ }
1300
+ if (source.sslmode !== void 0) {
1301
+ if (source.type === "sqlite") {
1302
+ throw new Error(
1303
+ `Configuration file ${configPath}: source '${source.id}' has sslmode but SQLite does not support SSL. Remove the sslmode field for SQLite sources.`
1304
+ );
1305
+ }
1306
+ const validSslModes = ["disable", "require"];
1307
+ if (!validSslModes.includes(source.sslmode)) {
1308
+ throw new Error(
1309
+ `Configuration file ${configPath}: source '${source.id}' has invalid sslmode '${source.sslmode}'. Valid values: ${validSslModes.join(", ")}`
1310
+ );
1311
+ }
1312
+ }
1313
+ if (source.authentication !== void 0) {
1314
+ if (source.type !== "sqlserver") {
1315
+ throw new Error(
1316
+ `Configuration file ${configPath}: source '${source.id}' has authentication but it is only supported for SQL Server.`
1317
+ );
1318
+ }
1319
+ const validAuthMethods = ["ntlm", "azure-active-directory-access-token"];
1320
+ if (!validAuthMethods.includes(source.authentication)) {
1321
+ throw new Error(
1322
+ `Configuration file ${configPath}: source '${source.id}' has invalid authentication '${source.authentication}'. Valid values: ${validAuthMethods.join(", ")}`
1323
+ );
1324
+ }
1325
+ if (source.authentication === "ntlm" && !source.domain) {
1326
+ throw new Error(
1327
+ `Configuration file ${configPath}: source '${source.id}' uses NTLM authentication but 'domain' is not specified.`
1328
+ );
1329
+ }
1330
+ }
1331
+ if (source.domain !== void 0) {
1332
+ if (source.type !== "sqlserver") {
1333
+ throw new Error(
1334
+ `Configuration file ${configPath}: source '${source.id}' has domain but it is only supported for SQL Server.`
1335
+ );
1336
+ }
1337
+ if (source.authentication === void 0) {
1338
+ throw new Error(
1339
+ `Configuration file ${configPath}: source '${source.id}' has domain but authentication is not set. Add authentication = "ntlm" to use Windows domain authentication.`
1340
+ );
1341
+ }
1342
+ if (source.authentication !== "ntlm") {
1343
+ throw new Error(
1344
+ `Configuration file ${configPath}: source '${source.id}' has domain but authentication is set to '${source.authentication}'. Domain is only valid with authentication = "ntlm".`
1345
+ );
1346
+ }
1347
+ }
1348
+ if (source.readonly !== void 0) {
1349
+ throw new Error(
1350
+ `Configuration file ${configPath}: source '${source.id}' has 'readonly' field, but readonly must be configured per-tool, not per-source. Move 'readonly' to [[tools]] configuration instead.`
1351
+ );
1352
+ }
1353
+ if (source.max_rows !== void 0) {
1354
+ throw new Error(
1355
+ `Configuration file ${configPath}: source '${source.id}' has 'max_rows' field, but max_rows must be configured per-tool, not per-source. Move 'max_rows' to [[tools]] configuration instead.`
1356
+ );
1357
+ }
1358
+ }
1359
+ function processSourceConfigs(sources, configPath) {
1360
+ return sources.map((source) => {
1361
+ const processed = { ...source };
1362
+ if (processed.ssh_key) {
1363
+ processed.ssh_key = expandHomeDir(processed.ssh_key);
1364
+ }
1365
+ if (processed.type === "sqlite" && processed.database) {
1366
+ processed.database = expandHomeDir(processed.database);
1367
+ }
1368
+ if (processed.dsn && processed.dsn.startsWith("sqlite:///~")) {
1369
+ processed.dsn = `sqlite:///${expandHomeDir(processed.dsn.substring(11))}`;
1370
+ }
1371
+ if (processed.dsn) {
1372
+ const connectionInfo = parseConnectionInfoFromDSN(processed.dsn);
1373
+ if (connectionInfo) {
1374
+ if (!processed.type && connectionInfo.type) {
1375
+ processed.type = connectionInfo.type;
1376
+ }
1377
+ if (!processed.host && connectionInfo.host) {
1378
+ processed.host = connectionInfo.host;
1379
+ }
1380
+ if (processed.port === void 0 && connectionInfo.port !== void 0) {
1381
+ processed.port = connectionInfo.port;
1382
+ }
1383
+ if (!processed.database && connectionInfo.database) {
1384
+ processed.database = connectionInfo.database;
1385
+ }
1386
+ if (!processed.user && connectionInfo.user) {
1387
+ processed.user = connectionInfo.user;
1388
+ }
1389
+ }
1390
+ }
1391
+ return processed;
1392
+ });
1393
+ }
1394
+ function expandHomeDir(filePath) {
1395
+ if (filePath.startsWith("~/")) {
1396
+ return path2.join(homedir3(), filePath.substring(2));
1397
+ }
1398
+ return filePath;
1399
+ }
1400
+ function buildDSNFromSource(source) {
1401
+ if (source.dsn) {
1402
+ return source.dsn;
1403
+ }
1404
+ if (!source.type) {
1405
+ throw new Error(
1406
+ `Source '${source.id}': 'type' field is required when 'dsn' is not provided`
1407
+ );
1408
+ }
1409
+ if (source.type === "sqlite") {
1410
+ if (!source.database) {
1411
+ throw new Error(
1412
+ `Source '${source.id}': 'database' field is required for SQLite`
1413
+ );
1414
+ }
1415
+ return `sqlite:///${source.database}`;
1416
+ }
1417
+ if (source.type === "redis") {
1418
+ const host = source.host || "localhost";
1419
+ const port2 = source.port || 6379;
1420
+ const db = source.database ? parseInt(source.database, 10) : 0;
1421
+ let dsn2 = `redis://${host}:${port2}/${db}`;
1422
+ if (source.password) {
1423
+ dsn2 = `redis://:${encodeURIComponent(source.password)}@${host}:${port2}/${db}`;
1424
+ }
1425
+ if (source.user) {
1426
+ dsn2 = `redis://${encodeURIComponent(source.user)}:${source.password ? encodeURIComponent(source.password) : ""}@${host}:${port2}/${db}`;
1427
+ }
1428
+ return dsn2;
1429
+ }
1430
+ if (source.type === "elasticsearch") {
1431
+ const host = source.host || "localhost";
1432
+ const port2 = source.port || 9200;
1433
+ let dsn2 = `elasticsearch://${host}:${port2}`;
1434
+ if (source.user && source.password) {
1435
+ dsn2 = `elasticsearch://${encodeURIComponent(source.user)}:${encodeURIComponent(source.password)}@${host}:${port2}`;
1436
+ }
1437
+ if (source.index_pattern) {
1438
+ dsn2 += `?index_pattern=${encodeURIComponent(source.index_pattern)}`;
1439
+ }
1440
+ return dsn2;
1441
+ }
1442
+ const passwordRequired = source.authentication !== "azure-active-directory-access-token";
1443
+ if (!source.host || !source.user || !source.database) {
1444
+ throw new Error(
1445
+ `Source '${source.id}': missing required connection parameters. Required: type, host, user, database`
1446
+ );
1447
+ }
1448
+ if (passwordRequired && !source.password) {
1449
+ throw new Error(
1450
+ `Source '${source.id}': password is required. (Password is optional only for azure-active-directory-access-token authentication)`
1451
+ );
1452
+ }
1453
+ const port = source.port || getDefaultPortForType(source.type);
1454
+ if (!port) {
1455
+ throw new Error(`Source '${source.id}': unable to determine port`);
1456
+ }
1457
+ const encodedUser = encodeURIComponent(source.user);
1458
+ const encodedPassword = source.password ? encodeURIComponent(source.password) : "";
1459
+ const encodedDatabase = encodeURIComponent(source.database);
1460
+ let dsn = `${source.type}://${encodedUser}:${encodedPassword}@${source.host}:${port}/${encodedDatabase}`;
1461
+ const queryParams = [];
1462
+ if (source.type === "sqlserver") {
1463
+ if (source.instanceName) {
1464
+ queryParams.push(`instanceName=${encodeURIComponent(source.instanceName)}`);
1465
+ }
1466
+ if (source.authentication) {
1467
+ queryParams.push(`authentication=${encodeURIComponent(source.authentication)}`);
1468
+ }
1469
+ if (source.domain) {
1470
+ queryParams.push(`domain=${encodeURIComponent(source.domain)}`);
1471
+ }
1472
+ }
1473
+ if (source.sslmode && source.type !== "sqlite") {
1474
+ queryParams.push(`sslmode=${source.sslmode}`);
1475
+ }
1476
+ if (queryParams.length > 0) {
1477
+ dsn += `?${queryParams.join("&")}`;
1478
+ }
1479
+ return dsn;
1480
+ }
1481
+
1482
+ // src/connectors/manager.ts
1483
+ var managerInstance = null;
1484
+ var ConnectorManager = class {
1485
+ // Prevent race conditions
1486
+ constructor() {
1487
+ // Maps for multi-source support
1488
+ this.connectors = /* @__PURE__ */ new Map();
1489
+ this.sshTunnels = /* @__PURE__ */ new Map();
1490
+ this.sourceConfigs = /* @__PURE__ */ new Map();
1491
+ // Store original source configs
1492
+ this.sourceIds = [];
1493
+ // Ordered list of source IDs (first is default)
1494
+ // Lazy connection support
1495
+ this.lazySources = /* @__PURE__ */ new Map();
1496
+ // Sources pending lazy connection
1497
+ this.pendingConnections = /* @__PURE__ */ new Map();
1498
+ if (!managerInstance) {
1499
+ managerInstance = this;
1500
+ }
1501
+ }
1502
+ /**
1503
+ * Initialize and connect to multiple databases using source configurations
1504
+ * This is the new multi-source connection method
1505
+ */
1506
+ async connectWithSources(sources) {
1507
+ if (sources.length === 0) {
1508
+ throw new Error("No sources provided");
1509
+ }
1510
+ const eagerSources = sources.filter((s) => !s.lazy);
1511
+ const lazySources = sources.filter((s) => s.lazy);
1512
+ if (eagerSources.length > 0) {
1513
+ console.error(`Connecting to ${eagerSources.length} database source(s)...`);
1514
+ }
1515
+ for (const source of eagerSources) {
1516
+ await this.connectSource(source);
1517
+ }
1518
+ for (const source of lazySources) {
1519
+ this.registerLazySource(source);
1520
+ }
1521
+ }
1522
+ /**
1523
+ * Register a lazy source without establishing connection
1524
+ * Connection will be established on first use via ensureConnected()
1525
+ */
1526
+ registerLazySource(source) {
1527
+ const sourceId = source.id;
1528
+ const dsn = buildDSNFromSource(source);
1529
+ console.error(` - ${sourceId}: ${redactDSN(dsn)} (lazy, will connect on first use)`);
1530
+ this.lazySources.set(sourceId, source);
1531
+ this.sourceConfigs.set(sourceId, source);
1532
+ this.sourceIds.push(sourceId);
1533
+ }
1534
+ /**
1535
+ * Ensure a source is connected (handles lazy connection on demand)
1536
+ * Safe to call multiple times - uses promise-based deduplication so concurrent calls share the same connection attempt
1537
+ */
1538
+ async ensureConnected(sourceId) {
1539
+ const id = sourceId || this.sourceIds[0];
1540
+ if (this.connectors.has(id)) {
1541
+ return;
1542
+ }
1543
+ const lazySource = this.lazySources.get(id);
1544
+ if (!lazySource) {
1545
+ if (sourceId) {
1546
+ throw new Error(
1547
+ `Source '${sourceId}' not found. Available sources: ${this.sourceIds.join(", ")}`
1548
+ );
1549
+ } else {
1550
+ throw new Error("No sources configured. Call connectWithSources() first.");
1551
+ }
1552
+ }
1553
+ const pending = this.pendingConnections.get(id);
1554
+ if (pending) {
1555
+ return pending;
1556
+ }
1557
+ const connectionPromise = (async () => {
1558
+ try {
1559
+ console.error(`Lazy connecting to source '${id}'...`);
1560
+ await this.connectSource(lazySource);
1561
+ this.lazySources.delete(id);
1562
+ } finally {
1563
+ this.pendingConnections.delete(id);
1564
+ }
1565
+ })();
1566
+ this.pendingConnections.set(id, connectionPromise);
1567
+ return connectionPromise;
1568
+ }
1569
+ /**
1570
+ * Static method to ensure a source is connected (for tool handlers)
1571
+ */
1572
+ static async ensureConnected(sourceId) {
1573
+ if (!managerInstance) {
1574
+ throw new Error("ConnectorManager not initialized");
1575
+ }
1576
+ return managerInstance.ensureConnected(sourceId);
1577
+ }
1578
+ /**
1579
+ * Connect to a single source (helper for connectWithSources)
1580
+ */
1581
+ async connectSource(source) {
1582
+ const sourceId = source.id;
1583
+ const dsn = buildDSNFromSource(source);
1584
+ console.error(` - ${sourceId}: ${redactDSN(dsn)}`);
1585
+ let actualDSN = dsn;
1586
+ if (source.ssh_host) {
1587
+ if (!source.ssh_user) {
1588
+ throw new Error(
1589
+ `Source '${sourceId}': SSH tunnel requires ssh_user`
1590
+ );
1591
+ }
1592
+ const sshConfig = {
1593
+ host: source.ssh_host,
1594
+ port: source.ssh_port || 22,
1595
+ username: source.ssh_user,
1596
+ password: source.ssh_password,
1597
+ privateKey: source.ssh_key,
1598
+ passphrase: source.ssh_passphrase,
1599
+ proxyJump: source.ssh_proxy_jump
1600
+ };
1601
+ if (!sshConfig.password && !sshConfig.privateKey) {
1602
+ throw new Error(
1603
+ `Source '${sourceId}': SSH tunnel requires either ssh_password or ssh_key`
1604
+ );
1605
+ }
1606
+ const url = new URL(dsn);
1607
+ const targetHost = url.hostname;
1608
+ const targetPort = parseInt(url.port) || this.getDefaultPort(dsn);
1609
+ const tunnel = new SSHTunnel();
1610
+ const tunnelInfo = await tunnel.establish(sshConfig, {
1611
+ targetHost,
1612
+ targetPort
1613
+ });
1614
+ url.hostname = "127.0.0.1";
1615
+ url.port = tunnelInfo.localPort.toString();
1616
+ actualDSN = url.toString();
1617
+ this.sshTunnels.set(sourceId, tunnel);
1618
+ console.error(
1619
+ ` SSH tunnel established through localhost:${tunnelInfo.localPort}`
1620
+ );
1621
+ }
1622
+ const connectorPrototype = ConnectorRegistry.getConnectorForDSN(actualDSN);
1623
+ if (!connectorPrototype) {
1624
+ throw new Error(
1625
+ `Source '${sourceId}': No connector found for DSN: ${actualDSN}`
1626
+ );
1627
+ }
1628
+ const connector = connectorPrototype.clone();
1629
+ connector.sourceId = sourceId;
1630
+ const config = {};
1631
+ if (source.connection_timeout !== void 0) {
1632
+ config.connectionTimeoutSeconds = source.connection_timeout;
1633
+ }
1634
+ if (source.query_timeout !== void 0 && connector.id !== "sqlite") {
1635
+ config.queryTimeoutSeconds = source.query_timeout;
1636
+ }
1637
+ if (source.readonly !== void 0) {
1638
+ config.readonly = source.readonly;
1639
+ }
1640
+ await connector.connect(actualDSN, source.init_script, config);
1641
+ this.connectors.set(sourceId, connector);
1642
+ if (!this.sourceIds.includes(sourceId)) {
1643
+ this.sourceIds.push(sourceId);
1644
+ }
1645
+ this.sourceConfigs.set(sourceId, source);
1646
+ }
1647
+ /**
1648
+ * Close all database connections
1649
+ */
1650
+ async disconnect() {
1651
+ for (const [sourceId, connector] of this.connectors.entries()) {
1652
+ try {
1653
+ await connector.disconnect();
1654
+ console.error(`Disconnected from source '${sourceId || "(default)"}'`);
1655
+ } catch (error) {
1656
+ console.error(`Error disconnecting from source '${sourceId}':`, error);
1657
+ }
1658
+ }
1659
+ for (const [sourceId, tunnel] of this.sshTunnels.entries()) {
1660
+ try {
1661
+ await tunnel.close();
1662
+ } catch (error) {
1663
+ console.error(`Error closing SSH tunnel for source '${sourceId}':`, error);
1664
+ }
1665
+ }
1666
+ this.connectors.clear();
1667
+ this.sshTunnels.clear();
1668
+ this.sourceConfigs.clear();
1669
+ this.lazySources.clear();
1670
+ this.pendingConnections.clear();
1671
+ this.sourceIds = [];
1672
+ }
1673
+ /**
1674
+ * Get a connector by source ID
1675
+ * If sourceId is not provided, returns the default (first) connector
1676
+ */
1677
+ getConnector(sourceId) {
1678
+ const id = sourceId || this.sourceIds[0];
1679
+ const connector = this.connectors.get(id);
1680
+ if (!connector) {
1681
+ if (sourceId) {
1682
+ throw new Error(
1683
+ `Source '${sourceId}' not found. Available sources: ${this.sourceIds.join(", ")}`
1684
+ );
1685
+ } else {
1686
+ throw new Error("No sources connected. Call connectWithSources() first.");
1687
+ }
1688
+ }
1689
+ return connector;
1690
+ }
1691
+ /**
1692
+ * Get all available connector types
1693
+ */
1694
+ static getAvailableConnectors() {
1695
+ return ConnectorRegistry.getAvailableConnectors();
1696
+ }
1697
+ /**
1698
+ * Get sample DSNs for all available connectors
1699
+ */
1700
+ static getAllSampleDSNs() {
1701
+ return ConnectorRegistry.getAllSampleDSNs();
1702
+ }
1703
+ /**
1704
+ * Get the current active connector instance
1705
+ * This is used by resource and tool handlers
1706
+ * @param sourceId - Optional source ID. If not provided, returns default (first) connector
1707
+ */
1708
+ static getCurrentConnector(sourceId) {
1709
+ if (!managerInstance) {
1710
+ throw new Error("ConnectorManager not initialized");
1711
+ }
1712
+ return managerInstance.getConnector(sourceId);
1713
+ }
1714
+ /**
1715
+ * Get all available source IDs
1716
+ */
1717
+ getSourceIds() {
1718
+ return [...this.sourceIds];
1719
+ }
1720
+ /** Get all available source IDs */
1721
+ static getAvailableSourceIds() {
1722
+ if (!managerInstance) {
1723
+ throw new Error("ConnectorManager not initialized");
1724
+ }
1725
+ return managerInstance.getSourceIds();
1726
+ }
1727
+ /**
1728
+ * Get source configuration by ID
1729
+ * @param sourceId - Source ID. If not provided, returns default (first) source config
1730
+ */
1731
+ getSourceConfig(sourceId) {
1732
+ if (this.sourceIds.length === 0) {
1733
+ return null;
1734
+ }
1735
+ const id = sourceId || this.sourceIds[0];
1736
+ return this.sourceConfigs.get(id) || null;
1737
+ }
1738
+ /**
1739
+ * Get all source configurations
1740
+ */
1741
+ getAllSourceConfigs() {
1742
+ return this.sourceIds.map((id) => this.sourceConfigs.get(id));
1743
+ }
1744
+ /**
1745
+ * Get source configuration by ID (static method for external access)
1746
+ */
1747
+ static getSourceConfig(sourceId) {
1748
+ if (!managerInstance) {
1749
+ throw new Error("ConnectorManager not initialized");
1750
+ }
1751
+ return managerInstance.getSourceConfig(sourceId);
1752
+ }
1753
+ /**
1754
+ * Get all source configurations (static method for external access)
1755
+ */
1756
+ static getAllSourceConfigs() {
1757
+ if (!managerInstance) {
1758
+ throw new Error("ConnectorManager not initialized");
1759
+ }
1760
+ return managerInstance.getAllSourceConfigs();
1761
+ }
1762
+ /**
1763
+ * Get default port for a database based on DSN protocol
1764
+ */
1765
+ getDefaultPort(dsn) {
1766
+ const type = getDatabaseTypeFromDSN(dsn);
1767
+ if (!type) {
1768
+ return 0;
1769
+ }
1770
+ return getDefaultPortForType(type) ?? 0;
1771
+ }
1772
+ };
1773
+
1774
+ // src/utils/sql-parser.ts
1775
+ function stripCommentsAndStrings(sql) {
1776
+ let result = "";
1777
+ let i = 0;
1778
+ while (i < sql.length) {
1779
+ if (sql[i] === "-" && sql[i + 1] === "-") {
1780
+ while (i < sql.length && sql[i] !== "\n") {
1781
+ i++;
1782
+ }
1783
+ result += " ";
1784
+ continue;
1785
+ }
1786
+ if (sql[i] === "/" && sql[i + 1] === "*") {
1787
+ i += 2;
1788
+ while (i < sql.length && !(sql[i] === "*" && sql[i + 1] === "/")) {
1789
+ i++;
1790
+ }
1791
+ i += 2;
1792
+ result += " ";
1793
+ continue;
1794
+ }
1795
+ if (sql[i] === "'") {
1796
+ i++;
1797
+ while (i < sql.length) {
1798
+ if (sql[i] === "'" && sql[i + 1] === "'") {
1799
+ i += 2;
1800
+ } else if (sql[i] === "'") {
1801
+ i++;
1802
+ break;
1803
+ } else {
1804
+ i++;
1805
+ }
1806
+ }
1807
+ result += " ";
1808
+ continue;
1809
+ }
1810
+ if (sql[i] === '"') {
1811
+ i++;
1812
+ while (i < sql.length) {
1813
+ if (sql[i] === '"' && sql[i + 1] === '"') {
1814
+ i += 2;
1815
+ } else if (sql[i] === '"') {
1816
+ i++;
1817
+ break;
1818
+ } else {
1819
+ i++;
1820
+ }
1821
+ }
1822
+ result += " ";
1823
+ continue;
1824
+ }
1825
+ result += sql[i];
1826
+ i++;
1827
+ }
1828
+ return result;
1829
+ }
1830
+
1831
+ // src/utils/parameter-mapper.ts
1832
+ var PARAMETER_STYLES = {
1833
+ postgres: "numbered",
1834
+ // $1, $2, $3
1835
+ mysql: "positional",
1836
+ // ?, ?, ?
1837
+ mariadb: "positional",
1838
+ // ?, ?, ?
1839
+ sqlserver: "named",
1840
+ // @p1, @p2, @p3
1841
+ sqlite: "positional"
1842
+ // ?, ?, ?
1843
+ };
1844
+ function detectParameterStyle(statement) {
1845
+ const cleanedSQL = stripCommentsAndStrings(statement);
1846
+ if (/\$\d+/.test(cleanedSQL)) {
1847
+ return "numbered";
1848
+ }
1849
+ if (/@p\d+/.test(cleanedSQL)) {
1850
+ return "named";
1851
+ }
1852
+ if (/\?/.test(cleanedSQL)) {
1853
+ return "positional";
1854
+ }
1855
+ return "none";
1856
+ }
1857
+ function validateParameterStyle(statement, connectorType) {
1858
+ const detectedStyle = detectParameterStyle(statement);
1859
+ const expectedStyle = PARAMETER_STYLES[connectorType];
1860
+ if (detectedStyle === "none") {
1861
+ return;
1862
+ }
1863
+ if (detectedStyle !== expectedStyle) {
1864
+ const examples = {
1865
+ numbered: "$1, $2, $3",
1866
+ positional: "?, ?, ?",
1867
+ named: "@p1, @p2, @p3"
1868
+ };
1869
+ throw new Error(
1870
+ `Invalid parameter syntax for ${connectorType}. Expected ${expectedStyle} style (${examples[expectedStyle]}), but found ${detectedStyle} style in statement.`
1871
+ );
1872
+ }
1873
+ }
1874
+ function countParameters(statement) {
1875
+ const style = detectParameterStyle(statement);
1876
+ const cleanedSQL = stripCommentsAndStrings(statement);
1877
+ switch (style) {
1878
+ case "numbered": {
1879
+ const matches = cleanedSQL.match(/\$\d+/g);
1880
+ if (!matches) return 0;
1881
+ const numbers = matches.map((m) => parseInt(m.slice(1), 10));
1882
+ const uniqueIndices = Array.from(new Set(numbers)).sort((a, b) => a - b);
1883
+ const maxIndex = Math.max(...uniqueIndices);
1884
+ for (let i = 1; i <= maxIndex; i++) {
1885
+ if (!uniqueIndices.includes(i)) {
1886
+ throw new Error(
1887
+ `Non-sequential numbered parameters detected. Found placeholders: ${uniqueIndices.map((n) => `$${n}`).join(", ")}. Parameters must be sequential starting from $1 (missing $${i}).`
1888
+ );
1889
+ }
1890
+ }
1891
+ return maxIndex;
1892
+ }
1893
+ case "named": {
1894
+ const matches = cleanedSQL.match(/@p\d+/g);
1895
+ if (!matches) return 0;
1896
+ const numbers = matches.map((m) => parseInt(m.slice(2), 10));
1897
+ const uniqueIndices = Array.from(new Set(numbers)).sort((a, b) => a - b);
1898
+ const maxIndex = Math.max(...uniqueIndices);
1899
+ for (let i = 1; i <= maxIndex; i++) {
1900
+ if (!uniqueIndices.includes(i)) {
1901
+ throw new Error(
1902
+ `Non-sequential named parameters detected. Found placeholders: ${uniqueIndices.map((n) => `@p${n}`).join(", ")}. Parameters must be sequential starting from @p1 (missing @p${i}).`
1903
+ );
1904
+ }
1905
+ }
1906
+ return maxIndex;
1907
+ }
1908
+ case "positional": {
1909
+ return (cleanedSQL.match(/\?/g) || []).length;
1910
+ }
1911
+ default:
1912
+ return 0;
1913
+ }
1914
+ }
1915
+ function validateParameters(statement, parameters, connectorType) {
1916
+ validateParameterStyle(statement, connectorType);
1917
+ const paramCount = countParameters(statement);
1918
+ const definedCount = parameters?.length || 0;
1919
+ if (paramCount !== definedCount) {
1920
+ throw new Error(
1921
+ `Parameter count mismatch: SQL statement has ${paramCount} parameter(s), but ${definedCount} parameter(s) defined in tool configuration.`
1922
+ );
1923
+ }
1924
+ }
1925
+ function mapArgumentsToArray(parameters, args) {
1926
+ if (!parameters || parameters.length === 0) {
1927
+ return [];
1928
+ }
1929
+ return parameters.map((param) => {
1930
+ const value = args[param.name];
1931
+ if (value !== void 0) {
1932
+ return value;
1933
+ }
1934
+ if (param.default !== void 0) {
1935
+ return param.default;
1936
+ }
1937
+ if (param.required !== false) {
1938
+ throw new Error(
1939
+ `Required parameter '${param.name}' is missing and has no default value.`
1940
+ );
1941
+ }
1942
+ return null;
1943
+ });
1944
+ }
1945
+
1946
+ // src/tools/registry.ts
1947
+ function isSqlDatabase(connectorType) {
1948
+ return ["postgres", "mysql", "mariadb", "sqlite", "sqlserver"].includes(connectorType);
1949
+ }
1950
+ function isRedisDatabase(connectorType) {
1951
+ return connectorType === "redis";
1952
+ }
1953
+ function isElasticsearchDatabase(connectorType) {
1954
+ return connectorType === "elasticsearch";
1955
+ }
1956
+ var ToolRegistry = class {
1957
+ constructor(config) {
1958
+ this.toolsBySource = this.buildRegistry(config);
1959
+ }
1960
+ /**
1961
+ * Check if a tool name is a built-in tool
1962
+ */
1963
+ isBuiltinTool(toolName) {
1964
+ return BUILTIN_TOOLS.includes(toolName);
1965
+ }
1966
+ /**
1967
+ * Validate a custom tool parameter definition
1968
+ */
1969
+ validateParameter(toolName, param) {
1970
+ if (!param.name || param.name.trim() === "") {
1971
+ throw new Error(`Tool '${toolName}' has parameter missing 'name' field`);
1972
+ }
1973
+ if (!param.type) {
1974
+ throw new Error(
1975
+ `Tool '${toolName}', parameter '${param.name}' missing 'type' field`
1976
+ );
1977
+ }
1978
+ const validTypes = ["string", "integer", "float", "boolean", "array"];
1979
+ if (!validTypes.includes(param.type)) {
1980
+ throw new Error(
1981
+ `Tool '${toolName}', parameter '${param.name}' has invalid type '${param.type}'. Valid types: ${validTypes.join(", ")}`
1982
+ );
1983
+ }
1984
+ if (!param.description || param.description.trim() === "") {
1985
+ throw new Error(
1986
+ `Tool '${toolName}', parameter '${param.name}' missing 'description' field`
1987
+ );
1988
+ }
1989
+ if (param.allowed_values) {
1990
+ if (!Array.isArray(param.allowed_values)) {
1991
+ throw new Error(
1992
+ `Tool '${toolName}', parameter '${param.name}': allowed_values must be an array`
1993
+ );
1994
+ }
1995
+ if (param.allowed_values.length === 0) {
1996
+ throw new Error(
1997
+ `Tool '${toolName}', parameter '${param.name}': allowed_values cannot be empty`
1998
+ );
1999
+ }
2000
+ }
2001
+ if (param.default !== void 0 && param.allowed_values) {
2002
+ if (!param.allowed_values.includes(param.default)) {
2003
+ throw new Error(
2004
+ `Tool '${toolName}', parameter '${param.name}': default value '${param.default}' is not in allowed_values: ${param.allowed_values.join(", ")}`
2005
+ );
2006
+ }
2007
+ }
2008
+ }
2009
+ /**
2010
+ * Validate a custom tool configuration
2011
+ */
2012
+ validateCustomTool(toolConfig, availableSources) {
2013
+ if (!toolConfig.name || toolConfig.name.trim() === "") {
2014
+ throw new Error("Tool definition missing required field: name");
2015
+ }
2016
+ if (!toolConfig.description || toolConfig.description.trim() === "") {
2017
+ throw new Error(
2018
+ `Tool '${toolConfig.name}' missing required field: description`
2019
+ );
2020
+ }
2021
+ if (!toolConfig.source || toolConfig.source.trim() === "") {
2022
+ throw new Error(
2023
+ `Tool '${toolConfig.name}' missing required field: source`
2024
+ );
2025
+ }
2026
+ if (!toolConfig.statement || toolConfig.statement.trim() === "") {
2027
+ throw new Error(
2028
+ `Tool '${toolConfig.name}' missing required field: statement`
2029
+ );
2030
+ }
2031
+ if (!availableSources.includes(toolConfig.source)) {
2032
+ throw new Error(
2033
+ `Tool '${toolConfig.name}' references unknown source '${toolConfig.source}'. Available sources: ${availableSources.join(", ")}`
2034
+ );
2035
+ }
2036
+ for (const builtinName of BUILTIN_TOOLS) {
2037
+ if (toolConfig.name === builtinName || toolConfig.name.startsWith(`${builtinName}_`)) {
2038
+ throw new Error(
2039
+ `Tool name '${toolConfig.name}' conflicts with built-in tool naming pattern. Custom tools cannot use names starting with: ${BUILTIN_TOOLS.join(", ")}`
2040
+ );
2041
+ }
2042
+ }
2043
+ const sourceConfig = ConnectorManager.getSourceConfig(toolConfig.source);
2044
+ const connectorType = sourceConfig.type;
2045
+ try {
2046
+ validateParameters(
2047
+ toolConfig.statement,
2048
+ toolConfig.parameters,
2049
+ connectorType
2050
+ );
2051
+ } catch (error) {
2052
+ throw new Error(
2053
+ `Tool '${toolConfig.name}' validation failed: ${error.message}`
2054
+ );
2055
+ }
2056
+ if (toolConfig.parameters) {
2057
+ for (const param of toolConfig.parameters) {
2058
+ this.validateParameter(toolConfig.name, param);
2059
+ }
2060
+ }
2061
+ }
2062
+ /**
2063
+ * Build the internal registry mapping sources to their enabled tools
2064
+ */
2065
+ buildRegistry(config) {
2066
+ const registry = /* @__PURE__ */ new Map();
2067
+ const availableSources = config.sources.map((s) => s.id);
2068
+ const customToolNames = /* @__PURE__ */ new Set();
2069
+ for (const tool of config.tools || []) {
2070
+ if (!this.isBuiltinTool(tool.name)) {
2071
+ this.validateCustomTool(tool, availableSources);
2072
+ if (customToolNames.has(tool.name)) {
2073
+ throw new Error(
2074
+ `Duplicate tool name '${tool.name}'. Tool names must be unique.`
2075
+ );
2076
+ }
2077
+ customToolNames.add(tool.name);
2078
+ }
2079
+ const existing = registry.get(tool.source) || [];
2080
+ existing.push(tool);
2081
+ registry.set(tool.source, existing);
2082
+ }
2083
+ for (const source of config.sources) {
2084
+ if (!registry.has(source.id)) {
2085
+ const defaultTools = [];
2086
+ if (isSqlDatabase(source.type)) {
2087
+ defaultTools.push({ name: "execute_sql", source: source.id });
2088
+ defaultTools.push({ name: "search_objects", source: source.id });
2089
+ } else if (isRedisDatabase(source.type)) {
2090
+ defaultTools.push({ name: "redis_command", source: source.id });
2091
+ } else if (isElasticsearchDatabase(source.type)) {
2092
+ defaultTools.push({ name: "elasticsearch_search", source: source.id });
2093
+ }
2094
+ registry.set(source.id, defaultTools);
2095
+ }
2096
+ }
2097
+ return registry;
2098
+ }
2099
+ /**
2100
+ * Get all enabled tool configs for a specific source
2101
+ */
2102
+ getEnabledToolConfigs(sourceId) {
2103
+ return this.toolsBySource.get(sourceId) || [];
2104
+ }
2105
+ /**
2106
+ * Get built-in tool configuration for a specific source
2107
+ * Returns undefined if tool is not enabled or not a built-in
2108
+ */
2109
+ getBuiltinToolConfig(toolName, sourceId) {
2110
+ if (!this.isBuiltinTool(toolName)) {
2111
+ return void 0;
2112
+ }
2113
+ const tools = this.getEnabledToolConfigs(sourceId);
2114
+ return tools.find((t) => t.name === toolName);
2115
+ }
2116
+ /**
2117
+ * Get all unique tools across all sources (for tools/list response)
2118
+ * Returns the union of all enabled tools
2119
+ */
2120
+ getAllTools() {
2121
+ const seen = /* @__PURE__ */ new Set();
2122
+ const result = [];
2123
+ for (const tools of this.toolsBySource.values()) {
2124
+ for (const tool of tools) {
2125
+ if (!seen.has(tool.name)) {
2126
+ seen.add(tool.name);
2127
+ result.push(tool);
2128
+ }
2129
+ }
2130
+ }
2131
+ return result;
2132
+ }
2133
+ /**
2134
+ * Get all custom tools (non-builtin) across all sources
2135
+ */
2136
+ getCustomTools() {
2137
+ return this.getAllTools().filter((tool) => !this.isBuiltinTool(tool.name));
2138
+ }
2139
+ /**
2140
+ * Get all built-in tool names that are enabled across any source
2141
+ */
2142
+ getEnabledBuiltinToolNames() {
2143
+ const enabledBuiltins = /* @__PURE__ */ new Set();
2144
+ for (const tools of this.toolsBySource.values()) {
2145
+ for (const tool of tools) {
2146
+ if (this.isBuiltinTool(tool.name)) {
2147
+ enabledBuiltins.add(tool.name);
2148
+ }
2149
+ }
2150
+ }
2151
+ return Array.from(enabledBuiltins);
2152
+ }
2153
+ };
2154
+ var globalRegistry = null;
2155
+ function initializeToolRegistry(config) {
2156
+ globalRegistry = new ToolRegistry(config);
2157
+ }
2158
+ function getToolRegistry() {
2159
+ if (!globalRegistry) {
2160
+ throw new Error(
2161
+ "Tool registry not initialized. Call initializeToolRegistry first."
2162
+ );
2163
+ }
2164
+ return globalRegistry;
2165
+ }
2166
+
2167
+ export {
2168
+ ConnectorRegistry,
2169
+ SafeURL,
2170
+ parseConnectionInfoFromDSN,
2171
+ obfuscateDSNPassword,
2172
+ getDatabaseTypeFromDSN,
2173
+ getDefaultPortForType,
2174
+ stripCommentsAndStrings,
2175
+ isDemoMode,
2176
+ resolveTransport,
2177
+ resolvePort,
2178
+ resolveSourceConfigs,
2179
+ BUILTIN_TOOL_EXECUTE_SQL,
2180
+ BUILTIN_TOOL_SEARCH_OBJECTS,
2181
+ BUILTIN_TOOL_REDIS_COMMAND,
2182
+ BUILTIN_TOOL_ELASTICSEARCH_SEARCH,
2183
+ ConnectorManager,
2184
+ mapArgumentsToArray,
2185
+ ToolRegistry,
2186
+ initializeToolRegistry,
2187
+ getToolRegistry
2188
+ };