@yeyuan98/opencode-bioresearcher-plugin 1.4.0 → 1.5.0-alpha.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.
- package/README.md +35 -20
- package/dist/db-tools/backends/index.d.ts +11 -0
- package/dist/db-tools/backends/index.js +48 -0
- package/dist/db-tools/backends/mongodb/backend.d.ts +15 -0
- package/dist/db-tools/backends/mongodb/backend.js +76 -0
- package/dist/db-tools/backends/mongodb/connection.d.ts +27 -0
- package/dist/db-tools/backends/mongodb/connection.js +107 -0
- package/dist/db-tools/backends/mongodb/index.d.ts +4 -0
- package/dist/db-tools/backends/mongodb/index.js +3 -0
- package/dist/db-tools/backends/mongodb/translator.d.ts +30 -0
- package/dist/db-tools/backends/mongodb/translator.js +407 -0
- package/dist/db-tools/backends/mysql/backend.d.ts +15 -0
- package/dist/db-tools/backends/mysql/backend.js +57 -0
- package/dist/db-tools/backends/mysql/connection.d.ts +25 -0
- package/dist/db-tools/backends/mysql/connection.js +83 -0
- package/dist/db-tools/backends/mysql/index.d.ts +3 -0
- package/dist/db-tools/backends/mysql/index.js +2 -0
- package/dist/db-tools/backends/mysql/translator.d.ts +7 -0
- package/dist/db-tools/backends/mysql/translator.js +67 -0
- package/dist/db-tools/core/base.d.ts +17 -0
- package/dist/db-tools/core/base.js +51 -0
- package/dist/db-tools/core/config-loader.d.ts +3 -0
- package/dist/db-tools/core/config-loader.js +46 -0
- package/dist/db-tools/core/index.d.ts +2 -0
- package/dist/db-tools/core/index.js +2 -0
- package/dist/db-tools/core/jsonc-parser.d.ts +2 -0
- package/dist/db-tools/core/jsonc-parser.js +77 -0
- package/dist/db-tools/core/validator.d.ts +16 -0
- package/dist/db-tools/core/validator.js +118 -0
- package/dist/db-tools/executor.d.ts +13 -0
- package/dist/db-tools/executor.js +54 -0
- package/dist/db-tools/index.d.ts +51 -0
- package/dist/db-tools/index.js +27 -0
- package/dist/db-tools/interface/backend.d.ts +24 -0
- package/dist/db-tools/interface/backend.js +1 -0
- package/dist/db-tools/interface/connection.d.ts +21 -0
- package/dist/db-tools/interface/connection.js +11 -0
- package/dist/db-tools/interface/index.d.ts +4 -0
- package/dist/db-tools/interface/index.js +4 -0
- package/dist/db-tools/interface/query.d.ts +60 -0
- package/dist/db-tools/interface/query.js +1 -0
- package/dist/db-tools/interface/schema.d.ts +22 -0
- package/dist/db-tools/interface/schema.js +1 -0
- package/dist/db-tools/pool.d.ts +8 -0
- package/dist/db-tools/pool.js +49 -0
- package/dist/db-tools/tools/index.d.ts +27 -0
- package/dist/db-tools/tools/index.js +191 -0
- package/dist/db-tools/tools.d.ts +27 -0
- package/dist/db-tools/tools.js +111 -0
- package/dist/db-tools/types.d.ts +94 -0
- package/dist/db-tools/types.js +40 -0
- package/dist/db-tools/utils.d.ts +33 -0
- package/dist/db-tools/utils.js +94 -0
- package/dist/index.js +2 -0
- package/dist/skills/bioresearcher-core/README.md +210 -210
- package/dist/skills/bioresearcher-core/SKILL.md +128 -128
- package/dist/skills/bioresearcher-core/examples/contexts.json +29 -29
- package/dist/skills/bioresearcher-core/examples/data-exchange-example.md +303 -303
- package/dist/skills/bioresearcher-core/examples/template.md +49 -49
- package/dist/skills/bioresearcher-core/patterns/calculator.md +215 -215
- package/dist/skills/bioresearcher-core/patterns/data-exchange.md +406 -406
- package/dist/skills/bioresearcher-core/patterns/json-tools.md +263 -263
- package/dist/skills/bioresearcher-core/patterns/progress.md +127 -127
- package/dist/skills/bioresearcher-core/patterns/retry.md +110 -110
- package/dist/skills/bioresearcher-core/patterns/shell-commands.md +79 -79
- package/dist/skills/bioresearcher-core/patterns/subagent-waves.md +186 -186
- package/dist/skills/bioresearcher-core/patterns/table-tools.md +260 -260
- package/dist/skills/bioresearcher-core/patterns/user-confirmation.md +187 -187
- package/dist/skills/bioresearcher-core/python/template.md +273 -273
- package/dist/skills/bioresearcher-core/python/template.py +323 -323
- package/dist/skills/env-jsonc-setup/SKILL.md +206 -0
- package/package.json +3 -1
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { getErrorHints } from './validator';
|
|
2
|
+
export class BaseBackend {
|
|
3
|
+
_connected = false;
|
|
4
|
+
isConnected() {
|
|
5
|
+
return this._connected;
|
|
6
|
+
}
|
|
7
|
+
createSuccessResult(rows, fields, executionTimeMs) {
|
|
8
|
+
return {
|
|
9
|
+
success: true,
|
|
10
|
+
data: {
|
|
11
|
+
rows,
|
|
12
|
+
rowCount: rows.length,
|
|
13
|
+
fields,
|
|
14
|
+
},
|
|
15
|
+
metadata: {
|
|
16
|
+
executionTimeMs,
|
|
17
|
+
backend: this.type,
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
createErrorResult(error, executionTimeMs, additionalHints) {
|
|
22
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
23
|
+
const errorCode = err.code;
|
|
24
|
+
const baseHints = getErrorHints(errorCode);
|
|
25
|
+
const hints = additionalHints ? [...baseHints, ...additionalHints] : [...baseHints];
|
|
26
|
+
const structuredError = {
|
|
27
|
+
code: errorCode || 'UNKNOWN_ERROR',
|
|
28
|
+
message: err.message || 'Unknown error occurred',
|
|
29
|
+
hints,
|
|
30
|
+
};
|
|
31
|
+
return {
|
|
32
|
+
success: false,
|
|
33
|
+
error: structuredError,
|
|
34
|
+
metadata: {
|
|
35
|
+
executionTimeMs,
|
|
36
|
+
backend: this.type,
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
createFieldInfo(name, type = 'unknown', nullable = true) {
|
|
41
|
+
return {
|
|
42
|
+
name,
|
|
43
|
+
type,
|
|
44
|
+
nullable,
|
|
45
|
+
key: null,
|
|
46
|
+
default: null,
|
|
47
|
+
extra: null,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
export { loadDbConfig as getConfigFromEnv } from './config-loader';
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { readJsoncFile } from './jsonc-parser';
|
|
3
|
+
import { validateBaseConfig } from '../interface/connection';
|
|
4
|
+
export function loadDbConfig() {
|
|
5
|
+
const configPath = path.join(process.cwd(), 'env.jsonc');
|
|
6
|
+
let rawConfig;
|
|
7
|
+
try {
|
|
8
|
+
rawConfig = readJsoncFile(configPath);
|
|
9
|
+
}
|
|
10
|
+
catch (error) {
|
|
11
|
+
throw new Error(`Failed to load env.jsonc from ${configPath}.\n` +
|
|
12
|
+
`Reason: ${error instanceof Error ? error.message : String(error)}\n` +
|
|
13
|
+
`Create env.jsonc in your working directory by using the "env-jsonc-setup" skill.\n` +
|
|
14
|
+
`Load the skill and follow the guided setup to create your database configuration.`);
|
|
15
|
+
}
|
|
16
|
+
const envJsonc = rawConfig;
|
|
17
|
+
if (!envJsonc['db-tools']) {
|
|
18
|
+
throw new Error(`Missing "db-tools" section in env.jsonc.\n` +
|
|
19
|
+
`Use the "env-jsonc-setup" skill to recreate the configuration file, or\n` +
|
|
20
|
+
`manually add a "db-tools" object with your database configuration.`);
|
|
21
|
+
}
|
|
22
|
+
const dbConfig = envJsonc['db-tools'];
|
|
23
|
+
const rawType = dbConfig.type || 'mysql';
|
|
24
|
+
const validTypes = ['mysql', 'mongodb'];
|
|
25
|
+
if (!validTypes.includes(rawType)) {
|
|
26
|
+
throw new Error(`Invalid type "${rawType}" in db-tools config. Supported types: ${validTypes.join(', ')}`);
|
|
27
|
+
}
|
|
28
|
+
const type = rawType;
|
|
29
|
+
const baseConfig = {
|
|
30
|
+
type,
|
|
31
|
+
host: dbConfig.host || 'localhost',
|
|
32
|
+
port: dbConfig.port || (type === 'mongodb' ? 27017 : 3306),
|
|
33
|
+
database: dbConfig.database || '',
|
|
34
|
+
username: dbConfig.username,
|
|
35
|
+
password: dbConfig.password,
|
|
36
|
+
connectionTimeout: dbConfig.connectionTimeout || 10000,
|
|
37
|
+
};
|
|
38
|
+
if (type === 'mongodb' && dbConfig.uri) {
|
|
39
|
+
baseConfig.options = { uri: dbConfig.uri };
|
|
40
|
+
}
|
|
41
|
+
validateBaseConfig(baseConfig);
|
|
42
|
+
return baseConfig;
|
|
43
|
+
}
|
|
44
|
+
export function getConfigFromEnv() {
|
|
45
|
+
return loadDbConfig();
|
|
46
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
export function parseJsonc(text) {
|
|
4
|
+
let result = '';
|
|
5
|
+
let i = 0;
|
|
6
|
+
const len = text.length;
|
|
7
|
+
let inString = false;
|
|
8
|
+
let stringQuote = null;
|
|
9
|
+
let inLineComment = false;
|
|
10
|
+
let inBlockComment = false;
|
|
11
|
+
let escaped = false;
|
|
12
|
+
while (i < len) {
|
|
13
|
+
const char = text[i];
|
|
14
|
+
const nextChar = i + 1 < len ? text[i + 1] : null;
|
|
15
|
+
if (inLineComment) {
|
|
16
|
+
if (char === '\n') {
|
|
17
|
+
inLineComment = false;
|
|
18
|
+
result += char;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
else if (inBlockComment) {
|
|
22
|
+
if (char === '*' && nextChar === '/') {
|
|
23
|
+
inBlockComment = false;
|
|
24
|
+
i += 2;
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
else if (inString) {
|
|
29
|
+
if (escaped) {
|
|
30
|
+
result += char;
|
|
31
|
+
escaped = false;
|
|
32
|
+
}
|
|
33
|
+
else if (char === '\\') {
|
|
34
|
+
result += char;
|
|
35
|
+
escaped = true;
|
|
36
|
+
}
|
|
37
|
+
else if (char === stringQuote) {
|
|
38
|
+
result += char;
|
|
39
|
+
inString = false;
|
|
40
|
+
stringQuote = null;
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
result += char;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
if (char === '"' || char === "'") {
|
|
48
|
+
inString = true;
|
|
49
|
+
stringQuote = char;
|
|
50
|
+
result += char;
|
|
51
|
+
}
|
|
52
|
+
else if (char === '/' && nextChar === '/') {
|
|
53
|
+
inLineComment = true;
|
|
54
|
+
i += 2;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
else if (char === '/' && nextChar === '*') {
|
|
58
|
+
inBlockComment = true;
|
|
59
|
+
i += 2;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
result += char;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
i++;
|
|
67
|
+
}
|
|
68
|
+
return JSON.parse(result);
|
|
69
|
+
}
|
|
70
|
+
export function readJsoncFile(filePath) {
|
|
71
|
+
const absolutePath = path.resolve(filePath);
|
|
72
|
+
if (!fs.existsSync(absolutePath)) {
|
|
73
|
+
throw new Error(`Configuration file not found: ${absolutePath}`);
|
|
74
|
+
}
|
|
75
|
+
const content = fs.readFileSync(absolutePath, 'utf-8');
|
|
76
|
+
return parseJsonc(content);
|
|
77
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { IQueryResult } from '../interface';
|
|
2
|
+
export interface ValidationResult {
|
|
3
|
+
valid: boolean;
|
|
4
|
+
error?: IQueryResult;
|
|
5
|
+
}
|
|
6
|
+
export declare function validateReadOnlyQuery(sql: string, backendType: string): ValidationResult;
|
|
7
|
+
export declare function validateCollectionName(name: string, backendType: string): ValidationResult;
|
|
8
|
+
export declare const ERROR_HINTS: {
|
|
9
|
+
readonly CONNECTION: readonly ["Check that the database server is running", "Verify DB_HOST and DB_PORT environment variables", "Ensure network connectivity to the database"];
|
|
10
|
+
readonly AUTH: readonly ["Verify DB_USER and DB_PASSWORD environment variables", "Check that the user has permissions for the database"];
|
|
11
|
+
readonly UNKNOWN_TABLE: readonly ["Use dbListTables to see available collections/tables", "Check the collection/table name for typos"];
|
|
12
|
+
readonly UNKNOWN_COLUMN: readonly ["Use dbDescribeTable to see available columns/fields", "Check the column/field name for typos"];
|
|
13
|
+
readonly SYNTAX: readonly ["Check your SQL syntax", "Ensure all identifiers are properly quoted if needed"];
|
|
14
|
+
readonly GENERIC: readonly ["Check your query syntax", "Use dbListTables to see available collections/tables", "Use dbDescribeTable to check column/field names"];
|
|
15
|
+
};
|
|
16
|
+
export declare function getErrorHints(errorCode: string | undefined): readonly string[];
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
const FORBIDDEN_KEYWORDS = [
|
|
2
|
+
'INSERT', 'UPDATE', 'DELETE', 'DROP', 'CREATE',
|
|
3
|
+
'ALTER', 'TRUNCATE', 'GRANT', 'REVOKE', 'EXEC', 'EXECUTE'
|
|
4
|
+
];
|
|
5
|
+
export function validateReadOnlyQuery(sql, backendType) {
|
|
6
|
+
const trimmed = sql.trim();
|
|
7
|
+
if (!trimmed) {
|
|
8
|
+
return {
|
|
9
|
+
valid: false,
|
|
10
|
+
error: createValidationError('EMPTY_QUERY', 'SQL query cannot be empty', ['Provide a valid SELECT query'], backendType),
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
if (trimmed.includes(';')) {
|
|
14
|
+
return {
|
|
15
|
+
valid: false,
|
|
16
|
+
error: createValidationError('MULTIPLE_STATEMENTS', 'Multiple SQL statements are not allowed (semicolon detected)', ['Remove the semicolon and use only one SELECT statement'], backendType),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
const upper = trimmed.toUpperCase();
|
|
20
|
+
if (!upper.startsWith('SELECT') && !upper.startsWith('SHOW') && !upper.startsWith('DESCRIBE') && !upper.startsWith('EXPLAIN')) {
|
|
21
|
+
return {
|
|
22
|
+
valid: false,
|
|
23
|
+
error: createValidationError('NOT_READ_ONLY', 'Only SELECT, SHOW, DESCRIBE, and EXPLAIN queries are allowed. This toolkit is read-only.', ['Use a SELECT statement to query data'], backendType),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
for (const keyword of FORBIDDEN_KEYWORDS) {
|
|
27
|
+
const regex = new RegExp(`\\b${keyword}\\b`, 'i');
|
|
28
|
+
if (regex.test(trimmed)) {
|
|
29
|
+
return {
|
|
30
|
+
valid: false,
|
|
31
|
+
error: createValidationError('FORBIDDEN_KEYWORD', `Forbidden keyword detected: ${keyword}. This toolkit is read-only.`, ['Remove the forbidden keyword', 'Only read-only queries are permitted'], backendType),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return { valid: true };
|
|
36
|
+
}
|
|
37
|
+
export function validateCollectionName(name, backendType) {
|
|
38
|
+
const TABLE_NAME_REGEX = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
39
|
+
if (!name || !name.trim()) {
|
|
40
|
+
return {
|
|
41
|
+
valid: false,
|
|
42
|
+
error: createValidationError('EMPTY_COLLECTION_NAME', 'Collection/table name cannot be empty', ['Provide a valid collection/table name', 'Use dbListTables to see available collections'], backendType),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
if (!TABLE_NAME_REGEX.test(name)) {
|
|
46
|
+
return {
|
|
47
|
+
valid: false,
|
|
48
|
+
error: createValidationError('INVALID_COLLECTION_NAME', `Invalid collection/table name format: '${name}'`, [
|
|
49
|
+
'Names must start with a letter or underscore',
|
|
50
|
+
'Names can only contain letters, numbers, and underscores',
|
|
51
|
+
'Use dbListTables to see valid names',
|
|
52
|
+
], backendType),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
return { valid: true };
|
|
56
|
+
}
|
|
57
|
+
function createValidationError(code, message, hints, backendType) {
|
|
58
|
+
const error = {
|
|
59
|
+
code,
|
|
60
|
+
message,
|
|
61
|
+
hints,
|
|
62
|
+
};
|
|
63
|
+
const metadata = {
|
|
64
|
+
executionTimeMs: 0,
|
|
65
|
+
backend: backendType,
|
|
66
|
+
};
|
|
67
|
+
return {
|
|
68
|
+
success: false,
|
|
69
|
+
error,
|
|
70
|
+
metadata,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
export const ERROR_HINTS = {
|
|
74
|
+
CONNECTION: [
|
|
75
|
+
'Check that the database server is running',
|
|
76
|
+
'Verify DB_HOST and DB_PORT environment variables',
|
|
77
|
+
'Ensure network connectivity to the database',
|
|
78
|
+
],
|
|
79
|
+
AUTH: [
|
|
80
|
+
'Verify DB_USER and DB_PASSWORD environment variables',
|
|
81
|
+
'Check that the user has permissions for the database',
|
|
82
|
+
],
|
|
83
|
+
UNKNOWN_TABLE: [
|
|
84
|
+
'Use dbListTables to see available collections/tables',
|
|
85
|
+
'Check the collection/table name for typos',
|
|
86
|
+
],
|
|
87
|
+
UNKNOWN_COLUMN: [
|
|
88
|
+
'Use dbDescribeTable to see available columns/fields',
|
|
89
|
+
'Check the column/field name for typos',
|
|
90
|
+
],
|
|
91
|
+
SYNTAX: [
|
|
92
|
+
'Check your SQL syntax',
|
|
93
|
+
'Ensure all identifiers are properly quoted if needed',
|
|
94
|
+
],
|
|
95
|
+
GENERIC: [
|
|
96
|
+
'Check your query syntax',
|
|
97
|
+
'Use dbListTables to see available collections/tables',
|
|
98
|
+
'Use dbDescribeTable to check column/field names',
|
|
99
|
+
],
|
|
100
|
+
};
|
|
101
|
+
export function getErrorHints(errorCode) {
|
|
102
|
+
const hintMap = {
|
|
103
|
+
'ECONNREFUSED': ERROR_HINTS.CONNECTION,
|
|
104
|
+
'ETIMEDOUT': ERROR_HINTS.CONNECTION,
|
|
105
|
+
'ENOTFOUND': ERROR_HINTS.CONNECTION,
|
|
106
|
+
'ER_ACCESS_DENIED_ERROR': ERROR_HINTS.AUTH,
|
|
107
|
+
'ER_BAD_DB_ERROR': ERROR_HINTS.AUTH,
|
|
108
|
+
'ER_NO_SUCH_TABLE': ERROR_HINTS.UNKNOWN_TABLE,
|
|
109
|
+
'ER_BAD_TABLE_ERROR': ERROR_HINTS.UNKNOWN_TABLE,
|
|
110
|
+
'ER_BAD_FIELD_ERROR': ERROR_HINTS.UNKNOWN_COLUMN,
|
|
111
|
+
'ER_UNKNOWN_COLUMN': ERROR_HINTS.UNKNOWN_COLUMN,
|
|
112
|
+
'ER_PARSE_ERROR': ERROR_HINTS.SYNTAX,
|
|
113
|
+
'ER_SYNTAX_ERROR': ERROR_HINTS.SYNTAX,
|
|
114
|
+
'MongoServerError': ERROR_HINTS.AUTH,
|
|
115
|
+
'MongoNetworkError': ERROR_HINTS.CONNECTION,
|
|
116
|
+
};
|
|
117
|
+
return hintMap[errorCode ?? ''] ?? ERROR_HINTS.GENERIC;
|
|
118
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import * as mysql from 'mysql2/promise';
|
|
2
|
+
import type { DbConfig, QueryResponse } from './types';
|
|
3
|
+
type QueryParams = Record<string, unknown> | unknown[];
|
|
4
|
+
interface ExecuteQueryOptions {
|
|
5
|
+
sql: string;
|
|
6
|
+
params?: QueryParams;
|
|
7
|
+
}
|
|
8
|
+
export declare function getConfigFromEnv(): DbConfig;
|
|
9
|
+
export declare function validateConfig(config: DbConfig): void;
|
|
10
|
+
export declare function executeQuery(options: ExecuteQueryOptions): Promise<QueryResponse>;
|
|
11
|
+
export declare function executeRawQuery<T extends mysql.RowDataPacket[]>(sql: string, params?: QueryParams): Promise<[T, mysql.FieldPacket[]]>;
|
|
12
|
+
export { getPool } from './pool';
|
|
13
|
+
export { closePool } from './pool';
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { DEFAULT_CONFIG as defaultConfig } from './types';
|
|
2
|
+
import { formatQueryResult, formatQueryError } from './utils';
|
|
3
|
+
import { getPool } from './pool';
|
|
4
|
+
export function getConfigFromEnv() {
|
|
5
|
+
const envConfig = {
|
|
6
|
+
host: process.env.DB_HOST || defaultConfig.host,
|
|
7
|
+
port: parseInt(process.env.DB_PORT || '', 10) || defaultConfig.port,
|
|
8
|
+
user: process.env.DB_USER || '',
|
|
9
|
+
password: process.env.DB_PASSWORD || '',
|
|
10
|
+
database: process.env.DB_DATABASE || '',
|
|
11
|
+
charset: process.env.DB_CHARSET || defaultConfig.charset,
|
|
12
|
+
connectionTimeout: parseInt(process.env.DB_CONNECTION_TIMEOUT || '', 10) || defaultConfig.connectionTimeout,
|
|
13
|
+
};
|
|
14
|
+
return envConfig;
|
|
15
|
+
}
|
|
16
|
+
export function validateConfig(config) {
|
|
17
|
+
const missing = [];
|
|
18
|
+
if (!config.user)
|
|
19
|
+
missing.push('DB_USER');
|
|
20
|
+
if (!config.database)
|
|
21
|
+
missing.push('DB_DATABASE');
|
|
22
|
+
if (missing.length > 0) {
|
|
23
|
+
throw new Error(`Missing required environment variables: ${missing.join(', ')}`);
|
|
24
|
+
}
|
|
25
|
+
if (config.port < 1 || config.port > 65535) {
|
|
26
|
+
throw new Error(`Invalid port number: ${config.port}. Must be between 1 and 65535.`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export async function executeQuery(options) {
|
|
30
|
+
const startTime = Date.now();
|
|
31
|
+
try {
|
|
32
|
+
const config = getConfigFromEnv();
|
|
33
|
+
validateConfig(config);
|
|
34
|
+
const pool = getPool(config);
|
|
35
|
+
const params = options.params ?? [];
|
|
36
|
+
const [rows, fields] = await pool.query(options.sql, params);
|
|
37
|
+
const executionTimeMs = Date.now() - startTime;
|
|
38
|
+
return formatQueryResult(rows, fields, executionTimeMs);
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
const executionTimeMs = Date.now() - startTime;
|
|
42
|
+
return formatQueryError(error, executionTimeMs);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export async function executeRawQuery(sql, params) {
|
|
46
|
+
const config = getConfigFromEnv();
|
|
47
|
+
validateConfig(config);
|
|
48
|
+
const pool = getPool(config);
|
|
49
|
+
const queryParams = params ?? [];
|
|
50
|
+
const result = await pool.query(sql, queryParams);
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
export { getPool } from './pool';
|
|
54
|
+
export { closePool } from './pool';
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export declare const dbToolsMetadata: {
|
|
2
|
+
category: string;
|
|
3
|
+
description: string;
|
|
4
|
+
capabilities: string[];
|
|
5
|
+
supportedBackends: string[];
|
|
6
|
+
typicalWorkflow: string[];
|
|
7
|
+
configMethod: string;
|
|
8
|
+
configPath: string;
|
|
9
|
+
};
|
|
10
|
+
export declare const dbTools: {
|
|
11
|
+
dbQuery: {
|
|
12
|
+
description: string;
|
|
13
|
+
args: {
|
|
14
|
+
sql: import("zod").ZodString;
|
|
15
|
+
params: import("zod").ZodOptional<import("zod").ZodRecord<import("zod").ZodString, import("zod").ZodAny>>;
|
|
16
|
+
};
|
|
17
|
+
execute(args: {
|
|
18
|
+
sql: string;
|
|
19
|
+
params?: Record<string, any> | undefined;
|
|
20
|
+
}, context: import("@opencode-ai/plugin").ToolContext): Promise<string>;
|
|
21
|
+
};
|
|
22
|
+
dbListTables: {
|
|
23
|
+
description: string;
|
|
24
|
+
args: {};
|
|
25
|
+
execute(args: Record<string, never>, context: import("@opencode-ai/plugin").ToolContext): Promise<string>;
|
|
26
|
+
};
|
|
27
|
+
dbDescribeTable: {
|
|
28
|
+
description: string;
|
|
29
|
+
args: {
|
|
30
|
+
table_name: import("zod").ZodString;
|
|
31
|
+
};
|
|
32
|
+
execute(args: {
|
|
33
|
+
table_name: string;
|
|
34
|
+
}, context: import("@opencode-ai/plugin").ToolContext): Promise<string>;
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
export { dbQuery, dbListTables, dbDescribeTable } from './tools';
|
|
38
|
+
export { createBackend, initializeBackend, closeBackend, getBackend, getDefaultBackend, getSupportedTypes, isBackendSupported, registerBackend, } from './backends';
|
|
39
|
+
export { MySQLBackend, createMySQLBackend, } from './backends/mysql';
|
|
40
|
+
export { MongoDBBackend, createMongoDBBackend, parseSelectQuery, SQLParseError, } from './backends/mongodb';
|
|
41
|
+
export { validateReadOnlyQuery, validateCollectionName, ERROR_HINTS, } from './core/validator';
|
|
42
|
+
export { BaseBackend, getConfigFromEnv, } from './core/base';
|
|
43
|
+
export { loadDbConfig, } from './core/config-loader';
|
|
44
|
+
export { parseJsonc, readJsoncFile, } from './core/jsonc-parser';
|
|
45
|
+
export type { IDatabaseBackend, IWriteableBackend, BackendType, IBackendFactory, } from './interface/backend';
|
|
46
|
+
export type { IConnectionConfig, IConnectionPool, IConnection, DatabaseType, } from './interface/connection';
|
|
47
|
+
export type { IQuery, IQueryOptions, IFieldInfo, IResultMetadata, IStructuredError, IQueryResult, IRow, IInsertResult, IUpdateResult, IDeleteResult, } from './interface/query';
|
|
48
|
+
export type { ICollectionInfo, IColumnSchema, ISchemaResult, } from './interface/schema';
|
|
49
|
+
export type { ValidationResult, } from './core/validator';
|
|
50
|
+
export type { MySQLConnectionConfig } from './backends/mysql/connection';
|
|
51
|
+
export type { MongoDBConnectionConfig } from './backends/mongodb/connection';
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { dbQuery, dbListTables, dbDescribeTable } from './tools';
|
|
2
|
+
export const dbToolsMetadata = {
|
|
3
|
+
category: 'database',
|
|
4
|
+
description: 'Read-only database query tools with multi-backend support',
|
|
5
|
+
capabilities: ['query', 'list_tables', 'describe_schema'],
|
|
6
|
+
supportedBackends: ['mysql', 'mongodb'],
|
|
7
|
+
typicalWorkflow: [
|
|
8
|
+
'1. Use dbListTables to discover available tables/collections',
|
|
9
|
+
'2. Use dbDescribeTable to understand column/field schema',
|
|
10
|
+
'3. Use dbQuery to execute SELECT queries with named parameters',
|
|
11
|
+
],
|
|
12
|
+
configMethod: 'env.jsonc file in working directory',
|
|
13
|
+
configPath: '<cwd>/env.jsonc with "db-tools" section',
|
|
14
|
+
};
|
|
15
|
+
export const dbTools = {
|
|
16
|
+
dbQuery,
|
|
17
|
+
dbListTables,
|
|
18
|
+
dbDescribeTable,
|
|
19
|
+
};
|
|
20
|
+
export { dbQuery, dbListTables, dbDescribeTable } from './tools';
|
|
21
|
+
export { createBackend, initializeBackend, closeBackend, getBackend, getDefaultBackend, getSupportedTypes, isBackendSupported, registerBackend, } from './backends';
|
|
22
|
+
export { MySQLBackend, createMySQLBackend, } from './backends/mysql';
|
|
23
|
+
export { MongoDBBackend, createMongoDBBackend, parseSelectQuery, SQLParseError, } from './backends/mongodb';
|
|
24
|
+
export { validateReadOnlyQuery, validateCollectionName, ERROR_HINTS, } from './core/validator';
|
|
25
|
+
export { BaseBackend, getConfigFromEnv, } from './core/base';
|
|
26
|
+
export { loadDbConfig, } from './core/config-loader';
|
|
27
|
+
export { parseJsonc, readJsoncFile, } from './core/jsonc-parser';
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { IConnectionConfig, IConnectionPool } from './connection';
|
|
2
|
+
import type { IQuery, IQueryResult, IInsertResult, IUpdateResult, IDeleteResult } from './query';
|
|
3
|
+
import type { ICollectionInfo, IColumnSchema } from './schema';
|
|
4
|
+
export interface IDatabaseBackend {
|
|
5
|
+
readonly type: string;
|
|
6
|
+
readonly config: IConnectionConfig;
|
|
7
|
+
connect(): Promise<void>;
|
|
8
|
+
disconnect(): Promise<void>;
|
|
9
|
+
isConnected(): boolean;
|
|
10
|
+
getPool(): IConnectionPool | null;
|
|
11
|
+
executeQuery(query: IQuery): Promise<IQueryResult>;
|
|
12
|
+
listCollections(): Promise<ICollectionInfo[]>;
|
|
13
|
+
describeCollection(name: string): Promise<IColumnSchema[]>;
|
|
14
|
+
}
|
|
15
|
+
export interface IWriteableBackend extends IDatabaseBackend {
|
|
16
|
+
insert(collection: string, data: Record<string, unknown> | Record<string, unknown>[]): Promise<IInsertResult>;
|
|
17
|
+
update(collection: string, filter: Record<string, unknown>, data: Record<string, unknown>): Promise<IUpdateResult>;
|
|
18
|
+
delete(collection: string, filter: Record<string, unknown>): Promise<IDeleteResult>;
|
|
19
|
+
}
|
|
20
|
+
export type BackendType = 'mysql' | 'mongodb';
|
|
21
|
+
export interface IBackendFactory {
|
|
22
|
+
create(config: IConnectionConfig): IDatabaseBackend;
|
|
23
|
+
getSupportedTypes(): BackendType[];
|
|
24
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export type DatabaseType = 'mysql' | 'mongodb';
|
|
2
|
+
export interface IConnectionConfig {
|
|
3
|
+
type: DatabaseType;
|
|
4
|
+
host: string;
|
|
5
|
+
port: number;
|
|
6
|
+
database: string;
|
|
7
|
+
username?: string;
|
|
8
|
+
password?: string;
|
|
9
|
+
connectionTimeout?: number;
|
|
10
|
+
options?: Record<string, unknown>;
|
|
11
|
+
}
|
|
12
|
+
export interface IConnectionPool {
|
|
13
|
+
getConnection(): Promise<IConnection>;
|
|
14
|
+
end(): Promise<void>;
|
|
15
|
+
isHealthy(): boolean;
|
|
16
|
+
}
|
|
17
|
+
export interface IConnection {
|
|
18
|
+
isConnected(): boolean;
|
|
19
|
+
close(): Promise<void>;
|
|
20
|
+
}
|
|
21
|
+
export declare function validateBaseConfig(config: IConnectionConfig): void;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export function validateBaseConfig(config) {
|
|
2
|
+
const missing = [];
|
|
3
|
+
if (!config.database)
|
|
4
|
+
missing.push('DB_DATABASE');
|
|
5
|
+
if (missing.length > 0) {
|
|
6
|
+
throw new Error(`Missing required configuration: ${missing.join(', ')}`);
|
|
7
|
+
}
|
|
8
|
+
if (config.port < 1 || config.port > 65535) {
|
|
9
|
+
throw new Error(`Invalid port number: ${config.port}. Must be between 1 and 65535.`);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
export interface IQuery {
|
|
2
|
+
sql: string;
|
|
3
|
+
params?: Record<string, unknown> | unknown[];
|
|
4
|
+
options?: IQueryOptions;
|
|
5
|
+
}
|
|
6
|
+
export interface IQueryOptions {
|
|
7
|
+
timeout?: number;
|
|
8
|
+
limit?: number;
|
|
9
|
+
offset?: number;
|
|
10
|
+
}
|
|
11
|
+
export interface IFieldInfo {
|
|
12
|
+
name: string;
|
|
13
|
+
type: string;
|
|
14
|
+
nullable: boolean;
|
|
15
|
+
key: string | null;
|
|
16
|
+
default: unknown;
|
|
17
|
+
extra: string | null;
|
|
18
|
+
}
|
|
19
|
+
export interface IResultMetadata {
|
|
20
|
+
executionTimeMs: number;
|
|
21
|
+
backend: string;
|
|
22
|
+
[key: string]: unknown;
|
|
23
|
+
}
|
|
24
|
+
export interface IStructuredError {
|
|
25
|
+
code: string;
|
|
26
|
+
message: string;
|
|
27
|
+
details?: string;
|
|
28
|
+
hints?: string[];
|
|
29
|
+
}
|
|
30
|
+
export interface IQueryResult {
|
|
31
|
+
success: boolean;
|
|
32
|
+
data?: {
|
|
33
|
+
rows: IRow[];
|
|
34
|
+
rowCount: number;
|
|
35
|
+
fields: IFieldInfo[];
|
|
36
|
+
};
|
|
37
|
+
error?: IStructuredError;
|
|
38
|
+
metadata: IResultMetadata;
|
|
39
|
+
}
|
|
40
|
+
export type IRow = Record<string, unknown>;
|
|
41
|
+
export interface IInsertResult {
|
|
42
|
+
success: boolean;
|
|
43
|
+
insertedId?: unknown;
|
|
44
|
+
insertedCount?: number;
|
|
45
|
+
error?: IStructuredError;
|
|
46
|
+
metadata: IResultMetadata;
|
|
47
|
+
}
|
|
48
|
+
export interface IUpdateResult {
|
|
49
|
+
success: boolean;
|
|
50
|
+
matchedCount?: number;
|
|
51
|
+
modifiedCount?: number;
|
|
52
|
+
error?: IStructuredError;
|
|
53
|
+
metadata: IResultMetadata;
|
|
54
|
+
}
|
|
55
|
+
export interface IDeleteResult {
|
|
56
|
+
success: boolean;
|
|
57
|
+
deletedCount?: number;
|
|
58
|
+
error?: IStructuredError;
|
|
59
|
+
metadata: IResultMetadata;
|
|
60
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface ICollectionInfo {
|
|
2
|
+
name: string;
|
|
3
|
+
type: 'table' | 'view' | 'collection';
|
|
4
|
+
engine?: string | null;
|
|
5
|
+
rowCount?: number | null;
|
|
6
|
+
createdAt?: string | null;
|
|
7
|
+
comment?: string | null;
|
|
8
|
+
}
|
|
9
|
+
export interface IColumnSchema {
|
|
10
|
+
field: string;
|
|
11
|
+
type: string;
|
|
12
|
+
nullable: boolean;
|
|
13
|
+
key: string | null;
|
|
14
|
+
defaultValue: unknown;
|
|
15
|
+
extra: string | null;
|
|
16
|
+
comment?: string | null;
|
|
17
|
+
}
|
|
18
|
+
export interface ISchemaResult {
|
|
19
|
+
collection: string;
|
|
20
|
+
columns: IColumnSchema[];
|
|
21
|
+
columnCount: number;
|
|
22
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import * as mysql from 'mysql2/promise';
|
|
2
|
+
import type { DbConfig } from './types';
|
|
3
|
+
export declare function getPool(config: DbConfig): mysql.Pool;
|
|
4
|
+
export declare function closePool(): Promise<void>;
|
|
5
|
+
export declare function getPoolStatus(): {
|
|
6
|
+
initialized: boolean;
|
|
7
|
+
hasConfig: boolean;
|
|
8
|
+
};
|