@blackbox_ai/blackbox-cli-core 0.0.7 → 0.8.1
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 +11 -183
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/src/blackbox/blackboxOAuth2.js +17 -1
- package/dist/src/blackbox/blackboxOAuth2.js.map +1 -1
- package/dist/src/code_assist/oauth2.js +15 -3
- package/dist/src/code_assist/oauth2.js.map +1 -1
- package/dist/src/config/blackboxModels.d.ts +3 -2
- package/dist/src/config/blackboxModels.js +262 -33
- package/dist/src/config/blackboxModels.js.map +1 -1
- package/dist/src/config/config.d.ts +65 -0
- package/dist/src/config/config.js +282 -17
- package/dist/src/config/config.js.map +1 -1
- package/dist/src/config/models.d.ts +1 -1
- package/dist/src/config/models.js +1 -1
- package/dist/src/config/models.js.map +1 -1
- package/dist/src/config/multiAgentModels.d.ts +63 -0
- package/dist/src/config/multiAgentModels.js +194 -0
- package/dist/src/config/multiAgentModels.js.map +1 -0
- package/dist/src/core/client.js +11 -5
- package/dist/src/core/client.js.map +1 -1
- package/dist/src/core/contentGenerator.d.ts +1 -0
- package/dist/src/core/contentGenerator.js +57 -7
- package/dist/src/core/contentGenerator.js.map +1 -1
- package/dist/src/core/coreToolScheduler.js +2 -2
- package/dist/src/core/coreToolScheduler.js.map +1 -1
- package/dist/src/core/encryptedClientFactory.d.ts +17 -0
- package/dist/src/core/encryptedClientFactory.js +92 -0
- package/dist/src/core/encryptedClientFactory.js.map +1 -0
- package/dist/src/core/encryptedContentGenerator.d.ts +47 -0
- package/dist/src/core/encryptedContentGenerator.js +445 -0
- package/dist/src/core/encryptedContentGenerator.js.map +1 -0
- package/dist/src/core/encryptedGeminiClient.d.ts +59 -0
- package/dist/src/core/encryptedGeminiClient.js +177 -0
- package/dist/src/core/encryptedGeminiClient.js.map +1 -0
- package/dist/src/core/encryptedGeminiClientBridge.d.ts +107 -0
- package/dist/src/core/encryptedGeminiClientBridge.js +808 -0
- package/dist/src/core/encryptedGeminiClientBridge.js.map +1 -0
- package/dist/src/core/encryptedGeminiClientWrapper.d.ts +129 -0
- package/dist/src/core/encryptedGeminiClientWrapper.js +305 -0
- package/dist/src/core/encryptedGeminiClientWrapper.js.map +1 -0
- package/dist/src/core/encryptedTurn.d.ts +40 -0
- package/dist/src/core/encryptedTurn.js +114 -0
- package/dist/src/core/encryptedTurn.js.map +1 -0
- package/dist/src/core/logger.d.ts +21 -0
- package/dist/src/core/logger.js +110 -0
- package/dist/src/core/logger.js.map +1 -1
- package/dist/src/core/openaiContentGenerator/constants.d.ts +2 -0
- package/dist/src/core/openaiContentGenerator/constants.js +2 -0
- package/dist/src/core/openaiContentGenerator/constants.js.map +1 -1
- package/dist/src/core/openaiContentGenerator/converter.d.ts +16 -1
- package/dist/src/core/openaiContentGenerator/converter.js +135 -4
- package/dist/src/core/openaiContentGenerator/converter.js.map +1 -1
- package/dist/src/core/openaiContentGenerator/pipeline.js +22 -8
- package/dist/src/core/openaiContentGenerator/pipeline.js.map +1 -1
- package/dist/src/core/openaiContentGenerator/pipeline.test.js +51 -0
- package/dist/src/core/openaiContentGenerator/pipeline.test.js.map +1 -1
- package/dist/src/core/openaiContentGenerator/provider/default.js +10 -1
- package/dist/src/core/openaiContentGenerator/provider/default.js.map +1 -1
- package/dist/src/core/prompts.d.ts +18 -1
- package/dist/src/core/prompts.js +388 -459
- package/dist/src/core/prompts.js.map +1 -1
- package/dist/src/core/tokenLimits.d.ts +1 -0
- package/dist/src/core/tokenLimits.js +37 -2
- package/dist/src/core/tokenLimits.js.map +1 -1
- package/dist/src/core/tokenLimits.test.js +36 -1
- package/dist/src/core/tokenLimits.test.js.map +1 -1
- package/dist/src/encrypt/attestation.d.ts +5 -0
- package/dist/src/encrypt/attestation.js +100 -0
- package/dist/src/encrypt/attestation.js.map +1 -0
- package/dist/src/encrypt/client.d.ts +14 -0
- package/dist/src/encrypt/client.js +132 -0
- package/dist/src/encrypt/client.js.map +1 -0
- package/dist/src/encrypt/config.d.ts +22 -0
- package/dist/src/encrypt/config.js +43 -0
- package/dist/src/encrypt/config.js.map +1 -0
- package/dist/src/encrypt/crypto-utils.d.ts +57 -0
- package/dist/src/encrypt/crypto-utils.js +257 -0
- package/dist/src/encrypt/crypto-utils.js.map +1 -0
- package/dist/src/encrypt/history-manager.d.ts +43 -0
- package/dist/src/encrypt/history-manager.js +164 -0
- package/dist/src/encrypt/history-manager.js.map +1 -0
- package/dist/src/encrypt/minimax-template.d.ts +73 -0
- package/dist/src/encrypt/minimax-template.js +276 -0
- package/dist/src/encrypt/minimax-template.js.map +1 -0
- package/dist/src/encrypt/sessions.d.ts +17 -0
- package/dist/src/encrypt/sessions.js +221 -0
- package/dist/src/encrypt/sessions.js.map +1 -0
- package/dist/src/encrypt/streaming-client.d.ts +29 -0
- package/dist/src/encrypt/streaming-client.js +232 -0
- package/dist/src/encrypt/streaming-client.js.map +1 -0
- package/dist/src/encrypt/tool-formatter.d.ts +36 -0
- package/dist/src/encrypt/tool-formatter.js +353 -0
- package/dist/src/encrypt/tool-formatter.js.map +1 -0
- package/dist/src/encrypt/tool-parser.d.ts +93 -0
- package/dist/src/encrypt/tool-parser.js +567 -0
- package/dist/src/encrypt/tool-parser.js.map +1 -0
- package/dist/src/encrypt/types.d.ts +81 -0
- package/dist/src/encrypt/types.js +2 -0
- package/dist/src/encrypt/types.js.map +1 -0
- package/dist/src/generated/git-commit.d.ts +3 -3
- package/dist/src/generated/git-commit.js +3 -3
- package/dist/src/ide/ide-client.js +9 -19
- package/dist/src/ide/ide-client.js.map +1 -1
- package/dist/src/index.d.ts +15 -0
- package/dist/src/index.js +15 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/mcp/oauth-provider.js +2 -6
- package/dist/src/mcp/oauth-provider.js.map +1 -1
- package/dist/src/mcp/oauth-token-storage.d.ts +7 -0
- package/dist/src/mcp/oauth-token-storage.js +24 -0
- package/dist/src/mcp/oauth-token-storage.js.map +1 -1
- package/dist/src/services/EncryptedChatService.d.ts +80 -0
- package/dist/src/services/EncryptedChatService.js +202 -0
- package/dist/src/services/EncryptedChatService.js.map +1 -0
- package/dist/src/services/StatsHistoryService.d.ts +131 -0
- package/dist/src/services/StatsHistoryService.js +427 -0
- package/dist/src/services/StatsHistoryService.js.map +1 -0
- package/dist/src/services/checkpointApiService.d.ts +101 -0
- package/dist/src/services/checkpointApiService.js +215 -0
- package/dist/src/services/checkpointApiService.js.map +1 -0
- package/dist/src/services/environmentSanitization.d.ts +24 -0
- package/dist/src/services/environmentSanitization.js +152 -0
- package/dist/src/services/environmentSanitization.js.map +1 -0
- package/dist/src/telemetry/blackbox-logger/blackbox-logger.d.ts +2 -6
- package/dist/src/telemetry/blackbox-logger/blackbox-logger.js +29 -135
- package/dist/src/telemetry/blackbox-logger/blackbox-logger.js.map +1 -1
- package/dist/src/telemetry/blackbox-logger/blackbox-logger.test.js +1 -1
- package/dist/src/telemetry/blackbox-logger/blackbox-logger.test.js.map +1 -1
- package/dist/src/telemetry/uiTelemetry.d.ts +8 -0
- package/dist/src/telemetry/uiTelemetry.js +17 -0
- package/dist/src/telemetry/uiTelemetry.js.map +1 -1
- package/dist/src/tools/browser-interactive.d.ts +63 -0
- package/dist/src/tools/browser-interactive.js +394 -0
- package/dist/src/tools/browser-interactive.js.map +1 -0
- package/dist/src/tools/browser_use.d.ts +23 -2
- package/dist/src/tools/browser_use.js +424 -43
- package/dist/src/tools/browser_use.js.map +1 -1
- package/dist/src/tools/data-file-constants.d.ts +17 -0
- package/dist/src/tools/data-file-constants.js +30 -0
- package/dist/src/tools/data-file-constants.js.map +1 -0
- package/dist/src/tools/edit.js +44 -7
- package/dist/src/tools/edit.js.map +1 -1
- package/dist/src/tools/exitPlanMode.js +1 -1
- package/dist/src/tools/exitPlanMode.js.map +1 -1
- package/dist/src/tools/ls.js +40 -6
- package/dist/src/tools/ls.js.map +1 -1
- package/dist/src/tools/ls.test.js +4 -4
- package/dist/src/tools/ls.test.js.map +1 -1
- package/dist/src/tools/mcp-client-manager.d.ts +28 -2
- package/dist/src/tools/mcp-client-manager.js +62 -4
- package/dist/src/tools/mcp-client-manager.js.map +1 -1
- package/dist/src/tools/mcp-client.d.ts +5 -3
- package/dist/src/tools/mcp-client.js +39 -11
- package/dist/src/tools/mcp-client.js.map +1 -1
- package/dist/src/tools/mcp-tool.d.ts +3 -1
- package/dist/src/tools/mcp-tool.js +37 -9
- package/dist/src/tools/mcp-tool.js.map +1 -1
- package/dist/src/tools/memoryTool.d.ts +14 -4
- package/dist/src/tools/memoryTool.js +98 -39
- package/dist/src/tools/memoryTool.js.map +1 -1
- package/dist/src/tools/read-data-file.d.ts +31 -0
- package/dist/src/tools/read-data-file.js +469 -0
- package/dist/src/tools/read-data-file.js.map +1 -0
- package/dist/src/tools/read-file.js +64 -5
- package/dist/src/tools/read-file.js.map +1 -1
- package/dist/src/tools/read-file.test.js +40 -6
- package/dist/src/tools/read-file.test.js.map +1 -1
- package/dist/src/tools/shell.d.ts +3 -1
- package/dist/src/tools/shell.js +25 -4
- package/dist/src/tools/shell.js.map +1 -1
- package/dist/src/tools/skill.d.ts +34 -0
- package/dist/src/tools/skill.js +143 -0
- package/dist/src/tools/skill.js.map +1 -0
- package/dist/src/tools/sql_db.d.ts +101 -0
- package/dist/src/tools/sql_db.js +1033 -0
- package/dist/src/tools/sql_db.js.map +1 -0
- package/dist/src/tools/sql_db_configure.d.ts +18 -0
- package/dist/src/tools/sql_db_configure.js +96 -0
- package/dist/src/tools/sql_db_configure.js.map +1 -0
- package/dist/src/tools/taskCompletion.d.ts +29 -0
- package/dist/src/tools/taskCompletion.js +231 -0
- package/dist/src/tools/taskCompletion.js.map +1 -0
- package/dist/src/tools/todoWrite.js +0 -142
- package/dist/src/tools/todoWrite.js.map +1 -1
- package/dist/src/tools/tool-error.d.ts +3 -1
- package/dist/src/tools/tool-error.js +3 -0
- package/dist/src/tools/tool-error.js.map +1 -1
- package/dist/src/tools/tool-names.d.ts +8 -0
- package/dist/src/tools/tool-names.js +8 -0
- package/dist/src/tools/tool-names.js.map +1 -1
- package/dist/src/tools/tool-registry.d.ts +22 -0
- package/dist/src/tools/tool-registry.js +41 -1
- package/dist/src/tools/tool-registry.js.map +1 -1
- package/dist/src/tools/tools.d.ts +18 -2
- package/dist/src/tools/tools.js +3 -0
- package/dist/src/tools/tools.js.map +1 -1
- package/dist/src/tools/web-fetch.js +24 -4
- package/dist/src/tools/web-fetch.js.map +1 -1
- package/dist/src/tools/web-search.js +160 -2
- package/dist/src/tools/web-search.js.map +1 -1
- package/dist/src/tools/workspace-error-helper.d.ts +9 -0
- package/dist/src/tools/workspace-error-helper.js +43 -0
- package/dist/src/tools/workspace-error-helper.js.map +1 -0
- package/dist/src/tools/workspace-error-helper.test.js +85 -0
- package/dist/src/tools/workspace-error-helper.test.js.map +1 -0
- package/dist/src/tools/write-file.js +42 -7
- package/dist/src/tools/write-file.js.map +1 -1
- package/dist/src/utils/environmentContext.js +3 -1
- package/dist/src/utils/environmentContext.js.map +1 -1
- package/dist/src/utils/environmentContext.test.js +3 -2
- package/dist/src/utils/environmentContext.test.js.map +1 -1
- package/dist/src/utils/fetch.d.ts +3 -1
- package/dist/src/utils/fetch.js +35 -2
- package/dist/src/utils/fetch.js.map +1 -1
- package/dist/src/utils/fileUtils.js +30 -3
- package/dist/src/utils/fileUtils.js.map +1 -1
- package/dist/src/utils/filesearch/fileSearch.d.ts +2 -0
- package/dist/src/utils/filesearch/fileSearch.js +38 -7
- package/dist/src/utils/filesearch/fileSearch.js.map +1 -1
- package/dist/src/utils/git-worktree-utils.d.ts +56 -0
- package/dist/src/utils/git-worktree-utils.js +176 -0
- package/dist/src/utils/git-worktree-utils.js.map +1 -0
- package/dist/src/utils/imageCompression.d.ts +34 -0
- package/dist/src/utils/imageCompression.js +170 -0
- package/dist/src/utils/imageCompression.js.map +1 -0
- package/dist/src/utils/messageTruncator.d.ts +51 -0
- package/dist/src/utils/messageTruncator.js +346 -0
- package/dist/src/utils/messageTruncator.js.map +1 -0
- package/dist/src/utils/pathReader.js +26 -6
- package/dist/src/utils/pathReader.js.map +1 -1
- package/dist/src/utils/skill.d.ts +65 -0
- package/dist/src/utils/skill.js +241 -0
- package/dist/src/utils/skill.js.map +1 -0
- package/dist/src/utils/textCleaning.d.ts +51 -0
- package/dist/src/utils/textCleaning.js +327 -0
- package/dist/src/utils/textCleaning.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +19 -6
- package/dist/src/tools/mcp-client-manager.test.js +0 -39
- package/dist/src/tools/mcp-client-manager.test.js.map +0 -1
- package/dist/src/tools/mcp-client.test.d.ts +0 -6
- package/dist/src/tools/mcp-client.test.js +0 -454
- package/dist/src/tools/mcp-client.test.js.map +0 -1
- package/dist/src/tools/mcp-tool.test.d.ts +0 -6
- package/dist/src/tools/mcp-tool.test.js +0 -576
- package/dist/src/tools/mcp-tool.test.js.map +0 -1
- package/dist/src/tools/memoryTool.test.d.ts +0 -6
- package/dist/src/tools/memoryTool.test.js +0 -420
- package/dist/src/tools/memoryTool.test.js.map +0 -1
- package/dist/src/tools/tool-registry.test.d.ts +0 -6
- package/dist/src/tools/tool-registry.test.js +0 -332
- package/dist/src/tools/tool-registry.test.js.map +0 -1
- /package/dist/src/tools/{mcp-client-manager.test.d.ts → workspace-error-helper.test.d.ts} +0 -0
|
@@ -0,0 +1,1033 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2025 Google LLC
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
import pg from 'pg';
|
|
7
|
+
import { MongoClient } from 'mongodb';
|
|
8
|
+
import { createClient } from 'redis';
|
|
9
|
+
import { ToolNames } from './tool-names.js';
|
|
10
|
+
import { ToolErrorType } from './tool-error.js';
|
|
11
|
+
import { BaseDeclarativeTool, BaseToolInvocation, Kind, } from './tools.js';
|
|
12
|
+
async function loadMysqlPromiseModule() {
|
|
13
|
+
try {
|
|
14
|
+
// eslint-disable-next-line import/no-internal-modules
|
|
15
|
+
const mod = await import('mysql2/promise');
|
|
16
|
+
return mod.default ?? mod;
|
|
17
|
+
}
|
|
18
|
+
catch (_error) {
|
|
19
|
+
const message = _error instanceof Error ? _error.message : String(_error);
|
|
20
|
+
throw new Error(`MySQL driver failed to load (${message}). ` +
|
|
21
|
+
`If you don't need MySQL, ignore this. Otherwise reinstall dependencies (mysql2 requires lru.min and seq-queue).`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
const CAUTION_PROMPT = `Carefully understand the database and the fields. Make sure you have an accurate understanding of the fields and the values.
|
|
25
|
+
If required, run further queries with 'sql_db_query' to have a full understanding. Run as many query as needed to confirm the results and provide accurate information to the user.
|
|
26
|
+
|
|
27
|
+
Note: To avoid timeout. Unless explicitly asked, Do not run global count operation.`;
|
|
28
|
+
/**
|
|
29
|
+
* Singleton database session manager (supports PostgreSQL, MySQL, MongoDB, and Redis)
|
|
30
|
+
*/
|
|
31
|
+
export class SqlSessionManager {
|
|
32
|
+
pgPool;
|
|
33
|
+
mysqlPool;
|
|
34
|
+
mongoClient;
|
|
35
|
+
mongoDb;
|
|
36
|
+
redisClient;
|
|
37
|
+
connectionString;
|
|
38
|
+
dbType;
|
|
39
|
+
static instance;
|
|
40
|
+
constructor() { }
|
|
41
|
+
static getInstance() {
|
|
42
|
+
if (!SqlSessionManager.instance) {
|
|
43
|
+
SqlSessionManager.instance = new SqlSessionManager();
|
|
44
|
+
}
|
|
45
|
+
return SqlSessionManager.instance;
|
|
46
|
+
}
|
|
47
|
+
async connect(connectionString, dbType, includeSchema = true, mongoDbName) {
|
|
48
|
+
if (dbType === 'mongodb' && !mongoDbName) {
|
|
49
|
+
throw new Error('MongoDB database name is required. Get the database name from the user if its not mentioned.Then, try again.');
|
|
50
|
+
}
|
|
51
|
+
console.log(`Connecting to ${dbType.toUpperCase()} database...`);
|
|
52
|
+
if (this.connectionString === connectionString && this.dbType === dbType) {
|
|
53
|
+
console.log('Reusing existing connection pool');
|
|
54
|
+
if (!includeSchema) {
|
|
55
|
+
return {
|
|
56
|
+
success: true,
|
|
57
|
+
message: 'Already connected to database',
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
// Fetch fresh schema
|
|
61
|
+
try {
|
|
62
|
+
const schema = await this.fetchSchema(mongoDbName);
|
|
63
|
+
return {
|
|
64
|
+
success: true,
|
|
65
|
+
message: 'Already connected to database',
|
|
66
|
+
schema,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
return {
|
|
71
|
+
success: false,
|
|
72
|
+
message: 'Connected but failed to fetch schema',
|
|
73
|
+
error: error instanceof Error ? error.message : String(error),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// Close existing connection if different
|
|
78
|
+
if (this.pgPool || this.mysqlPool || this.mongoClient || this.redisClient) {
|
|
79
|
+
await this.disconnect();
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
this.dbType = dbType;
|
|
83
|
+
this.connectionString = connectionString;
|
|
84
|
+
if (dbType === 'postgres') {
|
|
85
|
+
this.pgPool = new pg.Pool({
|
|
86
|
+
connectionString,
|
|
87
|
+
max: 10,
|
|
88
|
+
idleTimeoutMillis: 30000,
|
|
89
|
+
connectionTimeoutMillis: 10000,
|
|
90
|
+
});
|
|
91
|
+
// Test connection
|
|
92
|
+
const client = await this.pgPool.connect();
|
|
93
|
+
try {
|
|
94
|
+
await client.query('SELECT NOW()');
|
|
95
|
+
console.log('PostgreSQL connection successful');
|
|
96
|
+
}
|
|
97
|
+
finally {
|
|
98
|
+
client.release();
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
else if (dbType === 'mysql') {
|
|
102
|
+
// Parse connection string for MySQL
|
|
103
|
+
const url = new URL(connectionString);
|
|
104
|
+
const mysql = (await loadMysqlPromiseModule());
|
|
105
|
+
this.mysqlPool = mysql.createPool({
|
|
106
|
+
host: url.hostname,
|
|
107
|
+
port: url.port ? parseInt(url.port, 10) : 3306,
|
|
108
|
+
user: url.username,
|
|
109
|
+
password: url.password,
|
|
110
|
+
database: url.pathname.slice(1), // Remove leading /
|
|
111
|
+
waitForConnections: true,
|
|
112
|
+
connectionLimit: 10,
|
|
113
|
+
queueLimit: 0,
|
|
114
|
+
connectTimeout: 10000,
|
|
115
|
+
});
|
|
116
|
+
// Test connection
|
|
117
|
+
const connection = await this.mysqlPool.getConnection();
|
|
118
|
+
try {
|
|
119
|
+
await connection.query('SELECT NOW()');
|
|
120
|
+
console.log('MySQL connection successful');
|
|
121
|
+
}
|
|
122
|
+
finally {
|
|
123
|
+
connection.release();
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
else if (dbType === 'mongodb') {
|
|
127
|
+
// Parse connection string for MongoDB
|
|
128
|
+
this.mongoClient = new MongoClient(connectionString);
|
|
129
|
+
await this.mongoClient.connect();
|
|
130
|
+
// Get database name from connection string or use default
|
|
131
|
+
const url = new URL(connectionString);
|
|
132
|
+
const dbName = mongoDbName || url.pathname.slice(1);
|
|
133
|
+
this.mongoDb = this.mongoClient.db(dbName);
|
|
134
|
+
// Test connection
|
|
135
|
+
await this.mongoDb.command({ ping: 1 });
|
|
136
|
+
console.log('MongoDB connection successful');
|
|
137
|
+
}
|
|
138
|
+
else if (dbType === 'redis') {
|
|
139
|
+
const normalizeRedisUrl = (url, useTls) => {
|
|
140
|
+
if (useTls) {
|
|
141
|
+
return url.startsWith('redis://')
|
|
142
|
+
? url.replace(/^redis:\/\//, 'rediss://')
|
|
143
|
+
: url;
|
|
144
|
+
}
|
|
145
|
+
return url.startsWith('rediss://')
|
|
146
|
+
? url.replace(/^rediss:\/\//, 'redis://')
|
|
147
|
+
: url;
|
|
148
|
+
};
|
|
149
|
+
const connectRedis = async (url) => {
|
|
150
|
+
const client = createClient({ url });
|
|
151
|
+
client.on('error', (error) => {
|
|
152
|
+
console.warn(`Redis client error: ${error instanceof Error ? error.message : String(error)}`);
|
|
153
|
+
});
|
|
154
|
+
await client.connect();
|
|
155
|
+
return client;
|
|
156
|
+
};
|
|
157
|
+
const primaryUrl = normalizeRedisUrl(connectionString, true);
|
|
158
|
+
const fallbackUrl = normalizeRedisUrl(connectionString, false);
|
|
159
|
+
try {
|
|
160
|
+
// Try TLS first
|
|
161
|
+
this.redisClient = await connectRedis(primaryUrl);
|
|
162
|
+
}
|
|
163
|
+
catch (_error) {
|
|
164
|
+
if (this.redisClient) {
|
|
165
|
+
await this.redisClient.quit().catch(() => { });
|
|
166
|
+
this.redisClient = undefined;
|
|
167
|
+
}
|
|
168
|
+
// Retry without TLS
|
|
169
|
+
this.redisClient = await connectRedis(fallbackUrl);
|
|
170
|
+
}
|
|
171
|
+
if (!this.redisClient) {
|
|
172
|
+
throw new Error('Failed to initialize Redis client');
|
|
173
|
+
}
|
|
174
|
+
// Test connection
|
|
175
|
+
await this.redisClient.ping();
|
|
176
|
+
console.log('Redis connection successful');
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
throw new Error(`Unsupported database type: ${dbType}`);
|
|
180
|
+
}
|
|
181
|
+
if (!includeSchema) {
|
|
182
|
+
return {
|
|
183
|
+
success: true,
|
|
184
|
+
message: `Successfully connected to ${dbType.toUpperCase()} database`,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
// Fetch schema information
|
|
188
|
+
const schema = await this.fetchSchema(mongoDbName);
|
|
189
|
+
return {
|
|
190
|
+
success: true,
|
|
191
|
+
message: `Successfully connected to ${dbType.toUpperCase()} database`,
|
|
192
|
+
schema,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
catch (error) {
|
|
196
|
+
const errorMessage = `Failed to connect to database: ${error instanceof Error ? error.message : String(error)}`;
|
|
197
|
+
console.error(`[Error] ${errorMessage}`);
|
|
198
|
+
this.pgPool = undefined;
|
|
199
|
+
this.mysqlPool = undefined;
|
|
200
|
+
this.connectionString = undefined;
|
|
201
|
+
this.dbType = undefined;
|
|
202
|
+
return {
|
|
203
|
+
success: false,
|
|
204
|
+
message: errorMessage,
|
|
205
|
+
error: errorMessage,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
async fetchSchema(mongoDbName) {
|
|
210
|
+
if (!this.dbType) {
|
|
211
|
+
throw new Error('Not connected to database');
|
|
212
|
+
}
|
|
213
|
+
if (this.dbType === 'postgres') {
|
|
214
|
+
return this.fetchPostgresSchema();
|
|
215
|
+
}
|
|
216
|
+
else if (this.dbType === 'mysql') {
|
|
217
|
+
return this.fetchMySQLSchema();
|
|
218
|
+
}
|
|
219
|
+
else if (this.dbType === 'mongodb') {
|
|
220
|
+
return this.fetchMongoDBSchema(mongoDbName);
|
|
221
|
+
}
|
|
222
|
+
else if (this.dbType === 'redis') {
|
|
223
|
+
return this.fetchRedisSchema();
|
|
224
|
+
}
|
|
225
|
+
throw new Error(`Unsupported database type for schema fetch: ${this.dbType}`);
|
|
226
|
+
}
|
|
227
|
+
async fetchPostgresSchema() {
|
|
228
|
+
if (!this.pgPool) {
|
|
229
|
+
throw new Error('Not connected to PostgreSQL database');
|
|
230
|
+
}
|
|
231
|
+
const client = await this.pgPool.connect();
|
|
232
|
+
try {
|
|
233
|
+
// Get list of tables
|
|
234
|
+
const tablesResult = await client.query(`SELECT table_name, table_schema
|
|
235
|
+
FROM information_schema.tables
|
|
236
|
+
WHERE table_schema NOT IN ('pg_catalog', 'information_schema')
|
|
237
|
+
ORDER BY table_schema, table_name`);
|
|
238
|
+
const tables = tablesResult.rows;
|
|
239
|
+
// Get columns for each table
|
|
240
|
+
const columns = {};
|
|
241
|
+
for (const table of tables) {
|
|
242
|
+
const key = `${table.table_schema}.${table.table_name}`;
|
|
243
|
+
// Get columns
|
|
244
|
+
const columnsResult = await client.query(`SELECT column_name, data_type, is_nullable
|
|
245
|
+
FROM information_schema.columns
|
|
246
|
+
WHERE table_schema = $1 AND table_name = $2
|
|
247
|
+
ORDER BY ordinal_position`, [table.table_schema, table.table_name]);
|
|
248
|
+
columns[key] = columnsResult.rows;
|
|
249
|
+
}
|
|
250
|
+
return {
|
|
251
|
+
tables,
|
|
252
|
+
columns,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
finally {
|
|
256
|
+
client.release();
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
async fetchMySQLSchema() {
|
|
260
|
+
if (!this.mysqlPool) {
|
|
261
|
+
throw new Error('Not connected to MySQL database');
|
|
262
|
+
}
|
|
263
|
+
const connection = await this.mysqlPool.getConnection();
|
|
264
|
+
try {
|
|
265
|
+
// Get current database
|
|
266
|
+
const [dbResult] = await connection.query('SELECT DATABASE() as db');
|
|
267
|
+
const dbRows = dbResult;
|
|
268
|
+
const currentDb = dbRows[0]?.db || '';
|
|
269
|
+
// Get list of tables
|
|
270
|
+
const [tablesResult] = await connection.query(`SELECT table_name, table_schema
|
|
271
|
+
FROM information_schema.tables
|
|
272
|
+
WHERE table_schema = ?
|
|
273
|
+
ORDER BY table_name`, [currentDb]);
|
|
274
|
+
const tablesRows = tablesResult;
|
|
275
|
+
const tables = tablesRows.map((row) => ({
|
|
276
|
+
table_name: row.table_name || row.TABLE_NAME || '',
|
|
277
|
+
table_schema: row.table_schema || row.TABLE_SCHEMA || '',
|
|
278
|
+
}));
|
|
279
|
+
// Get columns for each table
|
|
280
|
+
const columns = {};
|
|
281
|
+
for (const table of tables) {
|
|
282
|
+
const key = `${table.table_schema}.${table.table_name}`;
|
|
283
|
+
// Get columns
|
|
284
|
+
const [columnsResult] = await connection.query(`SELECT column_name, data_type, is_nullable
|
|
285
|
+
FROM information_schema.columns
|
|
286
|
+
WHERE table_schema = ? AND table_name = ?
|
|
287
|
+
ORDER BY ordinal_position`, [table.table_schema, table.table_name]);
|
|
288
|
+
const columnsRows = columnsResult;
|
|
289
|
+
columns[key] = columnsRows.map((col) => ({
|
|
290
|
+
column_name: col.column_name || col.COLUMN_NAME || '',
|
|
291
|
+
data_type: col.data_type || col.DATA_TYPE || '',
|
|
292
|
+
is_nullable: col.is_nullable || col.IS_NULLABLE || '',
|
|
293
|
+
}));
|
|
294
|
+
}
|
|
295
|
+
return {
|
|
296
|
+
tables,
|
|
297
|
+
columns,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
finally {
|
|
301
|
+
connection.release();
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
async fetchMongoDBSchema(mongoDbName) {
|
|
305
|
+
if (!this.mongoDb) {
|
|
306
|
+
throw new Error('Not connected to MongoDB database');
|
|
307
|
+
}
|
|
308
|
+
try {
|
|
309
|
+
if (!this.mongoClient) {
|
|
310
|
+
throw new Error('MongoDB client is not initialized');
|
|
311
|
+
}
|
|
312
|
+
let dbNames = [];
|
|
313
|
+
if (mongoDbName) {
|
|
314
|
+
dbNames = [mongoDbName];
|
|
315
|
+
}
|
|
316
|
+
else {
|
|
317
|
+
dbNames = [this.mongoDb.databaseName];
|
|
318
|
+
}
|
|
319
|
+
// Sample documents from each collection to infer schema
|
|
320
|
+
const collections = [];
|
|
321
|
+
const sampleDocuments = {};
|
|
322
|
+
for (const dbName of dbNames) {
|
|
323
|
+
const db = this.mongoClient.db(dbName);
|
|
324
|
+
const collectionsResult = await db
|
|
325
|
+
.listCollections({}, { nameOnly: true, authorizedCollections: true })
|
|
326
|
+
.toArray();
|
|
327
|
+
for (const col of collectionsResult) {
|
|
328
|
+
collections.push({
|
|
329
|
+
collection_name: col.name,
|
|
330
|
+
database_name: dbName,
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
for (const col of collectionsResult) {
|
|
334
|
+
const key = `${dbName}.${col.name}`;
|
|
335
|
+
// Sample a document to infer fields
|
|
336
|
+
const sampleDoc = await db.collection(col.name).findOne();
|
|
337
|
+
sampleDocuments[key] = [sampleDoc];
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
return {
|
|
341
|
+
tables: [],
|
|
342
|
+
collections,
|
|
343
|
+
sampleDocuments,
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
catch (error) {
|
|
347
|
+
console.error('Error fetching MongoDB schema:', error);
|
|
348
|
+
throw error;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
async fetchRedisSchema() {
|
|
352
|
+
if (!this.redisClient) {
|
|
353
|
+
throw new Error('Not connected to Redis database');
|
|
354
|
+
}
|
|
355
|
+
try {
|
|
356
|
+
// Redis doesn't have a traditional schema
|
|
357
|
+
// We'll scan for a minimal sample of keys and get their types
|
|
358
|
+
const keyInfo = [];
|
|
359
|
+
const limitPerType = 3;
|
|
360
|
+
const maxScans = 20;
|
|
361
|
+
const maxSamples = limitPerType * 6;
|
|
362
|
+
const typeCounts = {};
|
|
363
|
+
let cursor = 0;
|
|
364
|
+
let scans = 0;
|
|
365
|
+
while (scans < maxScans && keyInfo.length < maxSamples) {
|
|
366
|
+
const scanResult = await this.redisClient.scan(cursor, {
|
|
367
|
+
MATCH: '*',
|
|
368
|
+
COUNT: 200,
|
|
369
|
+
});
|
|
370
|
+
cursor = scanResult.cursor;
|
|
371
|
+
scans += 1;
|
|
372
|
+
for (const key of scanResult.keys) {
|
|
373
|
+
const type = await this.redisClient.type(key);
|
|
374
|
+
const count = typeCounts[type] || 0;
|
|
375
|
+
if (count >= limitPerType) {
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
keyInfo.push({ key, type });
|
|
379
|
+
typeCounts[type] = count + 1;
|
|
380
|
+
if (keyInfo.length >= maxSamples) {
|
|
381
|
+
break;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
if (cursor === 0) {
|
|
385
|
+
break;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
// Group by type
|
|
389
|
+
const typeGroups = {};
|
|
390
|
+
for (const info of keyInfo) {
|
|
391
|
+
typeGroups[info.type] = (typeGroups[info.type] || 0) + 1;
|
|
392
|
+
}
|
|
393
|
+
const redisKeys = [];
|
|
394
|
+
for (const type of Object.keys(typeGroups)) {
|
|
395
|
+
const keys = keyInfo.filter((k) => k.type === type).slice(0, limitPerType).map((k) => k.key);
|
|
396
|
+
for (const key of keys) {
|
|
397
|
+
try {
|
|
398
|
+
let values = [];
|
|
399
|
+
if (type === 'string') {
|
|
400
|
+
const value = await this.redisClient.get(key);
|
|
401
|
+
values = value === null ? [] : [value];
|
|
402
|
+
}
|
|
403
|
+
else if (type === 'hash') {
|
|
404
|
+
const value = await this.redisClient.hGetAll(key);
|
|
405
|
+
const entries = Object.entries(value).slice(0, 3);
|
|
406
|
+
values = [Object.fromEntries(entries)];
|
|
407
|
+
}
|
|
408
|
+
else if (type === 'list') {
|
|
409
|
+
values = await this.redisClient.lRange(key, 0, 2);
|
|
410
|
+
}
|
|
411
|
+
else if (type === 'set') {
|
|
412
|
+
values = (await this.redisClient.sMembers(key)).slice(0, 3);
|
|
413
|
+
}
|
|
414
|
+
else if (type === 'zset') {
|
|
415
|
+
values = await this.redisClient.zRange(key, 0, 2);
|
|
416
|
+
}
|
|
417
|
+
else if (type === 'stream') {
|
|
418
|
+
values = await this.redisClient.xRange(key, '-', '+', { COUNT: 3 });
|
|
419
|
+
}
|
|
420
|
+
redisKeys.push({ key, type, values });
|
|
421
|
+
}
|
|
422
|
+
catch (_) {
|
|
423
|
+
redisKeys.push({ key, type, values: [] });
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
return {
|
|
428
|
+
tables: [],
|
|
429
|
+
redisKeys,
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
catch (error) {
|
|
433
|
+
console.error('Error fetching Redis schema:', error);
|
|
434
|
+
throw error;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
async query(sql) {
|
|
438
|
+
if (!this.dbType) {
|
|
439
|
+
return {
|
|
440
|
+
success: false,
|
|
441
|
+
message: 'Not connected to database. Please connect first using sql_db_connect.',
|
|
442
|
+
error: 'No active database connection',
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
if (this.dbType === 'postgres') {
|
|
446
|
+
return this.queryPostgres(sql);
|
|
447
|
+
}
|
|
448
|
+
else if (this.dbType === 'mysql') {
|
|
449
|
+
return this.queryMySQL(sql);
|
|
450
|
+
}
|
|
451
|
+
else if (this.dbType === 'mongodb') {
|
|
452
|
+
return this.queryMongoDB(sql);
|
|
453
|
+
}
|
|
454
|
+
else if (this.dbType === 'redis') {
|
|
455
|
+
return this.queryRedis(sql);
|
|
456
|
+
}
|
|
457
|
+
return {
|
|
458
|
+
success: false,
|
|
459
|
+
message: `Unsupported database type for queries: ${this.dbType}`,
|
|
460
|
+
error: `Unsupported database type: ${this.dbType}`,
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
async queryPostgres(sql) {
|
|
464
|
+
if (!this.pgPool) {
|
|
465
|
+
return {
|
|
466
|
+
success: false,
|
|
467
|
+
message: 'Not connected to PostgreSQL database.',
|
|
468
|
+
error: 'No active database connection',
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
const client = await this.pgPool.connect();
|
|
472
|
+
try {
|
|
473
|
+
console.log('Executing readonly query on PostgreSQL...');
|
|
474
|
+
// Start a readonly transaction
|
|
475
|
+
await client.query('BEGIN TRANSACTION READ ONLY');
|
|
476
|
+
try {
|
|
477
|
+
const result = await client.query(sql);
|
|
478
|
+
await client.query('COMMIT');
|
|
479
|
+
console.log(`Query executed successfully, returned ${result.rowCount} rows`);
|
|
480
|
+
return {
|
|
481
|
+
success: true,
|
|
482
|
+
message: `Query executed successfully, returned ${result.rowCount || 0} rows`,
|
|
483
|
+
rows: result.rows,
|
|
484
|
+
rowCount: result.rowCount || 0,
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
catch (queryError) {
|
|
488
|
+
await client.query('ROLLBACK').catch(() => { });
|
|
489
|
+
throw queryError;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
catch (error) {
|
|
493
|
+
const errorMessage = `Query execution failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
494
|
+
console.error(`[Error] ${errorMessage}`);
|
|
495
|
+
return {
|
|
496
|
+
success: false,
|
|
497
|
+
message: errorMessage,
|
|
498
|
+
error: errorMessage,
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
finally {
|
|
502
|
+
client.release();
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
async queryMySQL(sql) {
|
|
506
|
+
if (!this.mysqlPool) {
|
|
507
|
+
return {
|
|
508
|
+
success: false,
|
|
509
|
+
message: 'Not connected to MySQL database.',
|
|
510
|
+
error: 'No active database connection',
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
const connection = await this.mysqlPool.getConnection();
|
|
514
|
+
try {
|
|
515
|
+
console.log('Executing readonly query on MySQL...');
|
|
516
|
+
// Start a readonly transaction
|
|
517
|
+
await connection.query('START TRANSACTION READ ONLY');
|
|
518
|
+
try {
|
|
519
|
+
const [rows] = await connection.query(sql);
|
|
520
|
+
await connection.query('COMMIT');
|
|
521
|
+
const rowsArray = Array.isArray(rows) ? rows : [];
|
|
522
|
+
console.log(`Query executed successfully, returned ${rowsArray.length} rows`);
|
|
523
|
+
return {
|
|
524
|
+
success: true,
|
|
525
|
+
message: `Query executed successfully, returned ${rowsArray.length} rows`,
|
|
526
|
+
rows: rowsArray,
|
|
527
|
+
rowCount: rowsArray.length,
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
catch (queryError) {
|
|
531
|
+
await connection.query('ROLLBACK').catch(() => { });
|
|
532
|
+
throw queryError;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
catch (error) {
|
|
536
|
+
const errorMessage = `Query execution failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
537
|
+
console.error(`[Error] ${errorMessage}`);
|
|
538
|
+
return {
|
|
539
|
+
success: false,
|
|
540
|
+
message: errorMessage,
|
|
541
|
+
error: errorMessage,
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
finally {
|
|
545
|
+
connection.release();
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
async queryMongoDB(queryString) {
|
|
549
|
+
if (!this.mongoDb) {
|
|
550
|
+
return {
|
|
551
|
+
success: false,
|
|
552
|
+
message: 'Not connected to MongoDB database.',
|
|
553
|
+
error: 'No active database connection',
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
try {
|
|
557
|
+
console.log('Executing query on MongoDB...');
|
|
558
|
+
// Parse the query string as JSON
|
|
559
|
+
// Expected format: { "collection": "collectionName", "operation": "find", "query": {...}, "options": {...} }
|
|
560
|
+
const queryObj = JSON.parse(queryString);
|
|
561
|
+
if (!queryObj.collection) {
|
|
562
|
+
throw new Error('Missing "collection" field in query');
|
|
563
|
+
}
|
|
564
|
+
const collection = this.mongoDb.collection(queryObj.collection);
|
|
565
|
+
const operation = queryObj.operation || 'find';
|
|
566
|
+
const query = queryObj.query || {};
|
|
567
|
+
const options = queryObj.options || {};
|
|
568
|
+
let rows = [];
|
|
569
|
+
if (operation === 'find') {
|
|
570
|
+
const limit = options.limit || 100; // Default limit
|
|
571
|
+
rows = await collection.find(query).limit(limit).toArray();
|
|
572
|
+
}
|
|
573
|
+
else if (operation === 'aggregate') {
|
|
574
|
+
const pipeline = Array.isArray(queryObj.query)
|
|
575
|
+
? queryObj.query
|
|
576
|
+
: Array.isArray(queryObj.pipeline)
|
|
577
|
+
? queryObj.pipeline
|
|
578
|
+
: [];
|
|
579
|
+
rows = await collection.aggregate(pipeline, options).toArray();
|
|
580
|
+
}
|
|
581
|
+
else if (operation === 'count' || operation === 'countDocuments') {
|
|
582
|
+
const count = await collection.countDocuments(query);
|
|
583
|
+
rows = [{ count }];
|
|
584
|
+
}
|
|
585
|
+
else if (operation === 'distinct') {
|
|
586
|
+
const field = queryObj.field;
|
|
587
|
+
if (!field) {
|
|
588
|
+
throw new Error('Missing "field" for distinct operation');
|
|
589
|
+
}
|
|
590
|
+
const distinctValues = await collection.distinct(field, query);
|
|
591
|
+
rows = distinctValues.map((value) => ({ value }));
|
|
592
|
+
}
|
|
593
|
+
else {
|
|
594
|
+
throw new Error(`Unsupported MongoDB operation: ${operation}`);
|
|
595
|
+
}
|
|
596
|
+
console.log(`Query executed successfully, returned ${rows.length} documents`);
|
|
597
|
+
return {
|
|
598
|
+
success: true,
|
|
599
|
+
message: `Query executed successfully, returned ${rows.length} documents`,
|
|
600
|
+
rows,
|
|
601
|
+
rowCount: rows.length,
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
catch (error) {
|
|
605
|
+
const errorMessage = `Query execution failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
606
|
+
console.error(`[Error] ${errorMessage}`);
|
|
607
|
+
return {
|
|
608
|
+
success: false,
|
|
609
|
+
message: errorMessage,
|
|
610
|
+
error: errorMessage,
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
async queryRedis(commandString) {
|
|
615
|
+
if (!this.redisClient) {
|
|
616
|
+
return {
|
|
617
|
+
success: false,
|
|
618
|
+
message: 'Not connected to Redis database.',
|
|
619
|
+
error: 'No active database connection',
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
try {
|
|
623
|
+
console.log('Executing command on Redis...');
|
|
624
|
+
// Parse the command string as JSON
|
|
625
|
+
// Expected format: { "command": "GET", "args": ["key"] }
|
|
626
|
+
const commandObj = JSON.parse(commandString);
|
|
627
|
+
if (!commandObj.command) {
|
|
628
|
+
throw new Error('Missing "command" field in query');
|
|
629
|
+
}
|
|
630
|
+
const command = commandObj.command.toLowerCase();
|
|
631
|
+
const args = commandObj.args || [];
|
|
632
|
+
let result;
|
|
633
|
+
let rows = [];
|
|
634
|
+
// Execute common Redis commands
|
|
635
|
+
if (command === 'get') {
|
|
636
|
+
result = await this.redisClient.get(args[0]);
|
|
637
|
+
rows = [{ key: args[0], value: result }];
|
|
638
|
+
}
|
|
639
|
+
else if (command === 'mget') {
|
|
640
|
+
result = await this.redisClient.mGet(args);
|
|
641
|
+
rows = args.map((key, index) => ({ key, value: result[index] }));
|
|
642
|
+
}
|
|
643
|
+
else if (command === 'keys') {
|
|
644
|
+
const keys = await this.redisClient.keys(args[0] || '*');
|
|
645
|
+
rows = keys.slice(0, 100).map((key) => ({ key })); // Limit to 100
|
|
646
|
+
}
|
|
647
|
+
else if (command === 'scan') {
|
|
648
|
+
const cursor = parseInt(args[0] || '0', 10);
|
|
649
|
+
const pattern = args[1] || '*';
|
|
650
|
+
const count = parseInt(args[2] || '10', 10);
|
|
651
|
+
const scanResult = await this.redisClient.scan(cursor, { MATCH: pattern, COUNT: count });
|
|
652
|
+
rows = [{ cursor: scanResult.cursor, keys: scanResult.keys }];
|
|
653
|
+
}
|
|
654
|
+
else if (command === 'hgetall') {
|
|
655
|
+
result = await this.redisClient.hGetAll(args[0]);
|
|
656
|
+
rows = Object.entries(result).map(([field, value]) => ({ field, value }));
|
|
657
|
+
}
|
|
658
|
+
else if (command === 'lrange') {
|
|
659
|
+
const start = parseInt(args[1] || '0', 10);
|
|
660
|
+
const stop = parseInt(args[2] || '-1', 10);
|
|
661
|
+
result = await this.redisClient.lRange(args[0], start, stop);
|
|
662
|
+
rows = result.map((value, index) => ({ index: start + index, value }));
|
|
663
|
+
}
|
|
664
|
+
else if (command === 'smembers') {
|
|
665
|
+
result = await this.redisClient.sMembers(args[0]);
|
|
666
|
+
rows = result.map((value) => ({ value }));
|
|
667
|
+
}
|
|
668
|
+
else if (command === 'zrange') {
|
|
669
|
+
const start = parseInt(args[1] || '0', 10);
|
|
670
|
+
const stop = parseInt(args[2] || '-1', 10);
|
|
671
|
+
result = await this.redisClient.zRange(args[0], start, stop);
|
|
672
|
+
rows = result.map((value, index) => ({ index: start + index, value }));
|
|
673
|
+
}
|
|
674
|
+
else if (command === 'ttl') {
|
|
675
|
+
result = await this.redisClient.ttl(args[0]);
|
|
676
|
+
rows = [{ key: args[0], ttl: result }];
|
|
677
|
+
}
|
|
678
|
+
else if (command === 'type') {
|
|
679
|
+
result = await this.redisClient.type(args[0]);
|
|
680
|
+
rows = [{ key: args[0], type: result }];
|
|
681
|
+
}
|
|
682
|
+
else {
|
|
683
|
+
throw new Error(`Unsupported Redis command: ${command}. Use common read commands like GET, KEYS, HGETALL, LRANGE, etc.`);
|
|
684
|
+
}
|
|
685
|
+
console.log(`Command executed successfully, returned ${rows.length} results`);
|
|
686
|
+
return {
|
|
687
|
+
success: true,
|
|
688
|
+
message: `Command executed successfully, returned ${rows.length} results`,
|
|
689
|
+
rows,
|
|
690
|
+
rowCount: rows.length,
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
catch (error) {
|
|
694
|
+
const errorMessage = `Command execution failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
695
|
+
console.error(`[Error] ${errorMessage}`);
|
|
696
|
+
return {
|
|
697
|
+
success: false,
|
|
698
|
+
message: errorMessage,
|
|
699
|
+
error: errorMessage,
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
async disconnect() {
|
|
704
|
+
if (this.pgPool) {
|
|
705
|
+
console.log('Closing PostgreSQL connection pool...');
|
|
706
|
+
try {
|
|
707
|
+
await this.pgPool.end();
|
|
708
|
+
console.log('PostgreSQL connection pool closed successfully');
|
|
709
|
+
}
|
|
710
|
+
catch (error) {
|
|
711
|
+
console.warn(`Error closing PostgreSQL connection pool: ${error instanceof Error ? error.message : String(error)}`);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
if (this.mysqlPool) {
|
|
715
|
+
console.log('Closing MySQL connection pool...');
|
|
716
|
+
try {
|
|
717
|
+
await this.mysqlPool.end();
|
|
718
|
+
console.log('MySQL connection pool closed successfully');
|
|
719
|
+
}
|
|
720
|
+
catch (error) {
|
|
721
|
+
console.warn(`Error closing MySQL connection pool: ${error instanceof Error ? error.message : String(error)}`);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
if (this.mongoClient) {
|
|
725
|
+
console.log('Closing MongoDB connection...');
|
|
726
|
+
try {
|
|
727
|
+
await this.mongoClient.close();
|
|
728
|
+
console.log('MongoDB connection closed successfully');
|
|
729
|
+
}
|
|
730
|
+
catch (error) {
|
|
731
|
+
console.warn(`Error closing MongoDB connection: ${error instanceof Error ? error.message : String(error)}`);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
if (this.redisClient) {
|
|
735
|
+
console.log('Closing Redis connection...');
|
|
736
|
+
try {
|
|
737
|
+
await this.redisClient.quit();
|
|
738
|
+
console.log('Redis connection closed successfully');
|
|
739
|
+
}
|
|
740
|
+
catch (error) {
|
|
741
|
+
console.warn(`Error closing Redis connection: ${error instanceof Error ? error.message : String(error)}`);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
this.pgPool = undefined;
|
|
745
|
+
this.mysqlPool = undefined;
|
|
746
|
+
this.mongoClient = undefined;
|
|
747
|
+
this.mongoDb = undefined;
|
|
748
|
+
this.redisClient = undefined;
|
|
749
|
+
this.connectionString = undefined;
|
|
750
|
+
this.dbType = undefined;
|
|
751
|
+
}
|
|
752
|
+
isConnected() {
|
|
753
|
+
return !!(this.pgPool || this.mysqlPool || this.mongoClient || this.redisClient);
|
|
754
|
+
}
|
|
755
|
+
getDatabaseType() {
|
|
756
|
+
return this.dbType;
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
class SqlDbConnectToolInvocation extends BaseToolInvocation {
|
|
760
|
+
constructor(params) {
|
|
761
|
+
super(params);
|
|
762
|
+
}
|
|
763
|
+
getDescription() {
|
|
764
|
+
return `Connecting to ${this.params.type.toUpperCase()} database`;
|
|
765
|
+
}
|
|
766
|
+
async execute() {
|
|
767
|
+
const session = SqlSessionManager.getInstance();
|
|
768
|
+
const connectionString = this.params.connectionString || process.env['DB_CONNECTION_URI'];
|
|
769
|
+
if (!connectionString) {
|
|
770
|
+
return {
|
|
771
|
+
llmContent: 'DB_CONNECTION_URI is not configured. Run `/db configure` to set it, then retry this request.',
|
|
772
|
+
returnDisplay: 'DB_CONNECTION_URI is not configured. Run /db configure and try again.',
|
|
773
|
+
};
|
|
774
|
+
}
|
|
775
|
+
if (this.params.type === 'postgres') {
|
|
776
|
+
try {
|
|
777
|
+
const url = new URL(connectionString);
|
|
778
|
+
if (!url.pathname || url.pathname === '/' || url.pathname === '') {
|
|
779
|
+
return {
|
|
780
|
+
llmContent: 'PostgreSQL connection URI is missing a database name (path). Ask the user the name of DB and try again. ',
|
|
781
|
+
returnDisplay: 'PostgreSQL connection URI missing database name. Ask the user the name of DB and try again',
|
|
782
|
+
};
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
catch {
|
|
786
|
+
// If parsing fails, let the underlying connection attempt surface details.
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
const result = await session.connect(connectionString, this.params.type, this.params.includeSchema ?? true, this.params.mongoDbName ?? undefined);
|
|
790
|
+
const maxSchemaTokens = 50000 * 3;
|
|
791
|
+
console.log('[SqlDbConnectTool] Connection completed', {
|
|
792
|
+
success: result.success,
|
|
793
|
+
dbType: this.params.type,
|
|
794
|
+
tableCount: result.schema?.tables.length || 0,
|
|
795
|
+
});
|
|
796
|
+
if (!result.success) {
|
|
797
|
+
return {
|
|
798
|
+
llmContent: result.message,
|
|
799
|
+
returnDisplay: result.message,
|
|
800
|
+
error: {
|
|
801
|
+
message: result.error || result.message,
|
|
802
|
+
type: ToolErrorType.SQL_CONNECTION_ERROR,
|
|
803
|
+
},
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
// Format schema information for LLM
|
|
807
|
+
let schemaInfo = `${result.message}\n\n`;
|
|
808
|
+
if (result.schema) {
|
|
809
|
+
schemaInfo += `Found ${result.schema.tables.length} tables:\n\n`;
|
|
810
|
+
for (const table of result.schema.tables) {
|
|
811
|
+
const key = `${table.table_schema}.${table.table_name}`;
|
|
812
|
+
const columns = result.schema.columns?.[key] || [];
|
|
813
|
+
schemaInfo += `**${key}**\n`;
|
|
814
|
+
schemaInfo += `Columns (${columns.length}):\n`;
|
|
815
|
+
for (const col of columns) {
|
|
816
|
+
schemaInfo += ` - ${col.column_name}: ${col.data_type}${col.is_nullable === 'NO' ? ' NOT NULL' : ''}\n`;
|
|
817
|
+
}
|
|
818
|
+
schemaInfo += `\n`;
|
|
819
|
+
}
|
|
820
|
+
if (result.schema.tables.length > 0) {
|
|
821
|
+
schemaInfo += `\n\n ${CAUTION_PROMPT}`;
|
|
822
|
+
}
|
|
823
|
+
if (result.schema.collections) {
|
|
824
|
+
schemaInfo += `Collections (${result.schema.collections.length}):\n\n`;
|
|
825
|
+
for (const col of result.schema.collections) {
|
|
826
|
+
schemaInfo += ` - ${col.collection_name} in ${col.database_name}\n`;
|
|
827
|
+
}
|
|
828
|
+
schemaInfo += '\n';
|
|
829
|
+
}
|
|
830
|
+
if (result.schema.sampleDocuments) {
|
|
831
|
+
schemaInfo += `Sample documents:\n\n`;
|
|
832
|
+
for (const key in result.schema.sampleDocuments) {
|
|
833
|
+
const doc = result.schema.sampleDocuments[key];
|
|
834
|
+
schemaInfo += ` - Collection: ${key}\n`;
|
|
835
|
+
schemaInfo += ` - Sample Document: ${JSON.stringify(doc)}\n`;
|
|
836
|
+
}
|
|
837
|
+
schemaInfo += `\n\n ${CAUTION_PROMPT}`;
|
|
838
|
+
}
|
|
839
|
+
if (result.schema.redisKeys) {
|
|
840
|
+
schemaInfo += `Redis keys:\n\n`;
|
|
841
|
+
for (const key of result.schema.redisKeys) {
|
|
842
|
+
schemaInfo += ` - Key: ${key.key}\n`;
|
|
843
|
+
schemaInfo += ` - Type: ${key.type}\n`;
|
|
844
|
+
schemaInfo += ` - Values: ${JSON.stringify(key.values)}\n`;
|
|
845
|
+
}
|
|
846
|
+
schemaInfo += `\n\n ${CAUTION_PROMPT}`;
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
// truncate the schema info to 50000 tokens
|
|
850
|
+
if (schemaInfo.length > maxSchemaTokens) {
|
|
851
|
+
schemaInfo = schemaInfo.slice(0, maxSchemaTokens) + '\n\n... [truncated]';
|
|
852
|
+
schemaInfo += '\n\n[Schema truncated to prevent context overflow. Use sql_db_query to get more data as needed.]';
|
|
853
|
+
}
|
|
854
|
+
return {
|
|
855
|
+
llmContent: schemaInfo,
|
|
856
|
+
returnDisplay: result.message,
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
export class SqlDbConnectTool extends BaseDeclarativeTool {
|
|
861
|
+
config;
|
|
862
|
+
static Name = ToolNames.SQL_DB_CONNECT;
|
|
863
|
+
// @ts-expect-error - Required by base class pattern
|
|
864
|
+
constructor(config) {
|
|
865
|
+
super(SqlDbConnectTool.Name, 'SqlDbConnect', 'Connects to a database (PostgreSQL, MySQL, MongoDB, or Redis) and returns the schema details. The connection persists for subsequent queries. Connection string formats: postgresql://user:password@host:port/database, mysql://user:password@host:port/database, mongodb://user:password@host:port/database, redis://user:password@host:port/database', Kind.Execute, {
|
|
866
|
+
type: 'object',
|
|
867
|
+
properties: {
|
|
868
|
+
connectionString: {
|
|
869
|
+
type: 'string',
|
|
870
|
+
description: 'Database connection string. If omitted, DB_CONNECTION_URI environment variable will be used.',
|
|
871
|
+
},
|
|
872
|
+
type: {
|
|
873
|
+
type: 'string',
|
|
874
|
+
enum: ['postgres', 'mysql', 'mongodb', 'redis'],
|
|
875
|
+
description: 'Database type: "postgres", "mysql", "mongodb", or "redis"',
|
|
876
|
+
},
|
|
877
|
+
mongoDbName: {
|
|
878
|
+
type: 'string',
|
|
879
|
+
description: 'MongoDB database name to connect to (required for MongoDB type). If not provided, assume the current database.',
|
|
880
|
+
},
|
|
881
|
+
includeSchema: {
|
|
882
|
+
type: 'boolean',
|
|
883
|
+
description: 'Whether to fetch and return database schema information (default: true)',
|
|
884
|
+
},
|
|
885
|
+
},
|
|
886
|
+
required: ['type'],
|
|
887
|
+
}, true);
|
|
888
|
+
this.config = config;
|
|
889
|
+
}
|
|
890
|
+
validateToolParamValues(params) {
|
|
891
|
+
const connectionString = params.connectionString?.trim() || process.env['DB_CONNECTION_URI'];
|
|
892
|
+
if (!connectionString) {
|
|
893
|
+
return 'DB_CONNECTION_URI is not set. Run /db configure or provide connectionString.';
|
|
894
|
+
}
|
|
895
|
+
if (!params.type) {
|
|
896
|
+
return 'Database type parameter is required (postgres, mysql, mongodb, or redis)';
|
|
897
|
+
}
|
|
898
|
+
const validTypes = ['postgres', 'mysql', 'mongodb', 'redis'];
|
|
899
|
+
if (!validTypes.includes(params.type)) {
|
|
900
|
+
return `Database type must be one of: ${validTypes.join(', ')}`;
|
|
901
|
+
}
|
|
902
|
+
// Basic validation based on type
|
|
903
|
+
if (params.type === 'postgres') {
|
|
904
|
+
if (!connectionString.startsWith('postgres://') &&
|
|
905
|
+
!connectionString.startsWith('postgresql://')) {
|
|
906
|
+
return 'PostgreSQL connection string must start with postgresql:// or postgres://';
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
else if (params.type === 'mysql') {
|
|
910
|
+
if (!connectionString.startsWith('mysql://')) {
|
|
911
|
+
return 'MySQL connection string must start with mysql://';
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
else if (params.type === 'mongodb') {
|
|
915
|
+
if (!connectionString.startsWith('mongodb://') &&
|
|
916
|
+
!connectionString.startsWith('mongodb+srv://')) {
|
|
917
|
+
return 'MongoDB connection string must start with mongodb:// or mongodb+srv://';
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
else if (params.type === 'redis') {
|
|
921
|
+
if (!connectionString.startsWith('redis://') &&
|
|
922
|
+
!connectionString.startsWith('rediss://')) {
|
|
923
|
+
return 'Redis connection string must start with redis:// or rediss://';
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
return null;
|
|
927
|
+
}
|
|
928
|
+
createInvocation(params) {
|
|
929
|
+
return new SqlDbConnectToolInvocation(params);
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
class SqlDbQueryToolInvocation extends BaseToolInvocation {
|
|
933
|
+
constructor(params) {
|
|
934
|
+
super(params);
|
|
935
|
+
}
|
|
936
|
+
getDescription() {
|
|
937
|
+
const preview = this.params.sql.length > 100
|
|
938
|
+
? this.params.sql.substring(0, 100) + '...'
|
|
939
|
+
: this.params.sql;
|
|
940
|
+
return `Executing query: ${preview}`;
|
|
941
|
+
}
|
|
942
|
+
async execute() {
|
|
943
|
+
const session = SqlSessionManager.getInstance();
|
|
944
|
+
if (!session.isConnected()) {
|
|
945
|
+
return {
|
|
946
|
+
llmContent: 'Not connected to database. Please use sql_db_connect first.',
|
|
947
|
+
returnDisplay: 'Not connected to database',
|
|
948
|
+
error: {
|
|
949
|
+
message: 'No active database connection',
|
|
950
|
+
type: ToolErrorType.SQL_CONNECTION_ERROR,
|
|
951
|
+
},
|
|
952
|
+
};
|
|
953
|
+
}
|
|
954
|
+
const maxResultTokens = 50000 * 3;
|
|
955
|
+
const result = await session.query(this.params.sql);
|
|
956
|
+
const dbType = session.getDatabaseType();
|
|
957
|
+
console.log('[SqlDbQueryTool] Query completed', {
|
|
958
|
+
success: result.success,
|
|
959
|
+
dbType,
|
|
960
|
+
rowCount: result.rowCount || 0,
|
|
961
|
+
});
|
|
962
|
+
if (!result.success) {
|
|
963
|
+
return {
|
|
964
|
+
llmContent: result.message,
|
|
965
|
+
returnDisplay: result.message,
|
|
966
|
+
error: {
|
|
967
|
+
message: result.error || result.message,
|
|
968
|
+
type: ToolErrorType.SQL_QUERY_ERROR,
|
|
969
|
+
},
|
|
970
|
+
};
|
|
971
|
+
}
|
|
972
|
+
// Format results for LLM
|
|
973
|
+
let output = `${result.message}\n\n`;
|
|
974
|
+
if (result.rows && result.rows.length > 0) {
|
|
975
|
+
// Show results as formatted JSON
|
|
976
|
+
output += '```json\n';
|
|
977
|
+
output += JSON.stringify(result.rows, null, 2);
|
|
978
|
+
output += '\n```\n';
|
|
979
|
+
// truncate the json to 50000 tokens
|
|
980
|
+
if (output.length > maxResultTokens) {
|
|
981
|
+
output = output.slice(0, maxResultTokens) + '\n\n... [truncated]';
|
|
982
|
+
output += '\n\n[Result truncated to prevent context overflow. To see more results, use a more specific query.]';
|
|
983
|
+
}
|
|
984
|
+
else {
|
|
985
|
+
output += `\n(Showing all ${result.rowCount} rows)\n`;
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
else {
|
|
989
|
+
output += 'No rows returned.\n';
|
|
990
|
+
}
|
|
991
|
+
output += `\n\n Note: Now analyze and think through the results to decide the next steps to respond to the user's request accurately. If necessary, use sql_db_query to get more data as needed for better analysis.`;
|
|
992
|
+
return {
|
|
993
|
+
llmContent: output.trim(),
|
|
994
|
+
returnDisplay: result.message,
|
|
995
|
+
};
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
export class SqlDbQueryTool extends BaseDeclarativeTool {
|
|
999
|
+
config;
|
|
1000
|
+
static Name = ToolNames.SQL_DB_QUERY;
|
|
1001
|
+
// @ts-expect-error - Required by base class pattern
|
|
1002
|
+
constructor(config) {
|
|
1003
|
+
super(SqlDbQueryTool.Name, 'SqlDbQuery', 'Executes a query on the connected database. For SQL databases (PostgreSQL/MySQL): use SQL SELECT statements. For MongoDB: use JSON format {"collection":"name","operation":"find","query":{...}}. For Redis: use JSON format {"command":"GET","args":["key"]}. SQL queries run in READ ONLY mode.', Kind.Execute, {
|
|
1004
|
+
type: 'object',
|
|
1005
|
+
properties: {
|
|
1006
|
+
sql: {
|
|
1007
|
+
type: 'string',
|
|
1008
|
+
description: 'The query to execute. For SQL: SELECT statements. For MongoDB: JSON with collection, operation (find/aggregate/count/distinct), query, options. For Redis: JSON with command (GET/KEYS/HGETALL/LRANGE/etc) and args array.',
|
|
1009
|
+
},
|
|
1010
|
+
},
|
|
1011
|
+
required: ['sql'],
|
|
1012
|
+
}, true);
|
|
1013
|
+
this.config = config;
|
|
1014
|
+
}
|
|
1015
|
+
validateToolParamValues(params) {
|
|
1016
|
+
if (!params.sql || params.sql.trim() === '') {
|
|
1017
|
+
return 'SQL parameter must be non-empty';
|
|
1018
|
+
}
|
|
1019
|
+
// Warn about potentially dangerous queries (but allow them since we use READ ONLY transaction)
|
|
1020
|
+
const sqlLower = params.sql.trim().toLowerCase();
|
|
1021
|
+
const dangerousKeywords = ['insert', 'update', 'delete', 'drop', 'create', 'alter', 'truncate'];
|
|
1022
|
+
for (const keyword of dangerousKeywords) {
|
|
1023
|
+
if (sqlLower.includes(keyword)) {
|
|
1024
|
+
console.warn(`[Warning] Query contains '${keyword}' but will run in READ ONLY transaction`);
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
return null;
|
|
1028
|
+
}
|
|
1029
|
+
createInvocation(params) {
|
|
1030
|
+
return new SqlDbQueryToolInvocation(params);
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
//# sourceMappingURL=sql_db.js.map
|