@yejiming/dsh-data-agent 0.0.3 → 0.0.6
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.en.md +16 -16
- package/README.md +15 -15
- package/lib/client.js +89 -72
- package/lib/client.js.map +1 -1
- package/lib/{defaults-Dgu2B2Yq.js → defaults-Bac6QvNt.js} +469 -7
- package/lib/index.js +5 -5
- package/lib/{query-vK9dr7Z6.js → query-CmhTFklw.js} +2 -2
- package/lib/routes.js +3 -2
- package/lib/tool.js +335 -18
- package/lib/types/clients.d.ts +22 -2
- package/lib/types/defaults.d.ts +3 -3
- package/lib/types/index.d.ts +6 -6
- package/lib/types/query.d.ts +6 -2
- package/lib/types/sql.d.ts +26 -0
- package/lib/types/structured.d.ts +29 -0
- package/lib/types/tool.d.ts +10 -4
- package/package.json +1 -1
- package/preset/data-agent/agent.cordis.yml +11 -7
- package/preset/data-agent/preset.yml +2 -2
package/lib/tool.js
CHANGED
|
@@ -1,7 +1,169 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { t as runClientQuery } from "./query-
|
|
1
|
+
import { d as clientsSchema, f as enforceReadRowLimit, i as DEFAULT_MAX_RESULT_CHARS, o as DEFAULT_QUERY_TIMEOUT_MS, u as classifyStatement, y as assertSingleStatement } from "./defaults-Bac6QvNt.js";
|
|
2
|
+
import { t as runClientQuery } from "./query-CmhTFklw.js";
|
|
3
3
|
import z from "schemastery";
|
|
4
4
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
5
|
+
//#region src/structured.ts
|
|
6
|
+
function normalizeNewlines(text) {
|
|
7
|
+
return text.replace(/\r\n?/g, "\n");
|
|
8
|
+
}
|
|
9
|
+
function splitLine(line, delimiter) {
|
|
10
|
+
return line.split(delimiter);
|
|
11
|
+
}
|
|
12
|
+
/** Make column names valid unique JSON object keys. */
|
|
13
|
+
function uniqueColumns(columns) {
|
|
14
|
+
const used = /* @__PURE__ */ new Set();
|
|
15
|
+
return columns.map((raw, index) => {
|
|
16
|
+
let name = raw.trim();
|
|
17
|
+
if (name.length === 0) name = `column_${index + 1}`;
|
|
18
|
+
if (used.has(name)) {
|
|
19
|
+
let suffix = 2;
|
|
20
|
+
while (used.has(`${name}_${suffix}`)) suffix += 1;
|
|
21
|
+
name = `${name}_${suffix}`;
|
|
22
|
+
}
|
|
23
|
+
used.add(name);
|
|
24
|
+
return name;
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
function rowObject(columns, fields) {
|
|
28
|
+
const row = {};
|
|
29
|
+
for (let index = 0; index < columns.length; index += 1) row[columns[index]] = fields[index] ?? null;
|
|
30
|
+
return row;
|
|
31
|
+
}
|
|
32
|
+
function emptyOutput() {
|
|
33
|
+
return {
|
|
34
|
+
columns: [],
|
|
35
|
+
rows: [],
|
|
36
|
+
rowLimitExceeded: false
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function skipLeadingBlank(lines) {
|
|
40
|
+
let index = 0;
|
|
41
|
+
while (index < lines.length && lines[index].trim().length === 0) index += 1;
|
|
42
|
+
return index;
|
|
43
|
+
}
|
|
44
|
+
/** PostgreSQL `-A` appends a `(N rows)` / `(N row)` footer after SELECT output. */
|
|
45
|
+
function isPostgresFooter(line) {
|
|
46
|
+
return /^\(\d+ rows?\)$/.test(line.trim());
|
|
47
|
+
}
|
|
48
|
+
function parseDelimited(stdout, delimiter, maxRows, skipFooter = false) {
|
|
49
|
+
const lines = normalizeNewlines(stdout).split("\n");
|
|
50
|
+
if (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
|
|
51
|
+
const headerIndex = skipLeadingBlank(lines);
|
|
52
|
+
if (headerIndex >= lines.length) return emptyOutput();
|
|
53
|
+
const columns = uniqueColumns(splitLine(lines[headerIndex], delimiter));
|
|
54
|
+
const rows = [];
|
|
55
|
+
let rowLimitExceeded = false;
|
|
56
|
+
for (let index = headerIndex + 1; index < lines.length; index += 1) {
|
|
57
|
+
const line = lines[index];
|
|
58
|
+
if (skipFooter && isPostgresFooter(line)) continue;
|
|
59
|
+
if (rows.length >= maxRows) {
|
|
60
|
+
rowLimitExceeded = true;
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
rows.push(rowObject(columns, splitLine(line, delimiter)));
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
columns,
|
|
67
|
+
rows,
|
|
68
|
+
rowLimitExceeded
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/** Minimal RFC-4180-style parser for sqlite3 `-csv` output. */
|
|
72
|
+
function parseCsv(text) {
|
|
73
|
+
const records = [];
|
|
74
|
+
let record = [];
|
|
75
|
+
let field = "";
|
|
76
|
+
let quoted = false;
|
|
77
|
+
let index = 0;
|
|
78
|
+
const pushField = () => {
|
|
79
|
+
record.push(field);
|
|
80
|
+
field = "";
|
|
81
|
+
};
|
|
82
|
+
const pushRecord = () => {
|
|
83
|
+
pushField();
|
|
84
|
+
records.push(record);
|
|
85
|
+
record = [];
|
|
86
|
+
};
|
|
87
|
+
while (index < text.length) {
|
|
88
|
+
const char = text[index];
|
|
89
|
+
if (quoted) {
|
|
90
|
+
if (char === "\"") {
|
|
91
|
+
if (text[index + 1] === "\"") {
|
|
92
|
+
field += "\"";
|
|
93
|
+
index += 2;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
quoted = false;
|
|
97
|
+
index += 1;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
field += char;
|
|
101
|
+
index += 1;
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (char === "\"" && field.length === 0) {
|
|
105
|
+
quoted = true;
|
|
106
|
+
index += 1;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (char === ",") {
|
|
110
|
+
pushField();
|
|
111
|
+
index += 1;
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (char === "\n") {
|
|
115
|
+
pushRecord();
|
|
116
|
+
index += 1;
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (char === "\r") {
|
|
120
|
+
if (text[index + 1] === "\n") index += 1;
|
|
121
|
+
pushRecord();
|
|
122
|
+
index += 1;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
field += char;
|
|
126
|
+
index += 1;
|
|
127
|
+
}
|
|
128
|
+
if (field.length > 0 || record.length > 0) pushRecord();
|
|
129
|
+
return records;
|
|
130
|
+
}
|
|
131
|
+
function parseCsvOutput(stdout, maxRows) {
|
|
132
|
+
const records = parseCsv(normalizeNewlines(stdout)).filter((record) => !(record.length === 1 && record[0] === ""));
|
|
133
|
+
if (records.length === 0) return emptyOutput();
|
|
134
|
+
const columns = uniqueColumns(records[0]);
|
|
135
|
+
const rows = [];
|
|
136
|
+
let rowLimitExceeded = false;
|
|
137
|
+
for (let index = 1; index < records.length; index += 1) {
|
|
138
|
+
if (rows.length >= maxRows) {
|
|
139
|
+
rowLimitExceeded = true;
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
rows.push(rowObject(columns, records[index]));
|
|
143
|
+
}
|
|
144
|
+
return {
|
|
145
|
+
columns,
|
|
146
|
+
rows,
|
|
147
|
+
rowLimitExceeded
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Parse one database type's structured-query stdout. The matching template is
|
|
152
|
+
* `buildStructuredQueryTemplate`: mysql tab-separated with a header, postgres
|
|
153
|
+
* pipe-separated with a header and row-count footer, sqlite CSV with a header,
|
|
154
|
+
* oracle pipe-separated with heading on, hive/impala tsv with a header.
|
|
155
|
+
*/
|
|
156
|
+
function parseStructuredQueryOutput(type, stdout, maxRows) {
|
|
157
|
+
switch (type) {
|
|
158
|
+
case "mysql": return parseDelimited(stdout, " ", maxRows);
|
|
159
|
+
case "postgres": return parseDelimited(stdout, "|", maxRows, true);
|
|
160
|
+
case "sqlite": return parseCsvOutput(stdout, maxRows);
|
|
161
|
+
case "oracle": return parseDelimited(stdout, "|", maxRows);
|
|
162
|
+
case "hive":
|
|
163
|
+
case "impala": return parseDelimited(stdout, " ", maxRows);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
//#endregion
|
|
5
167
|
//#region src/tool.ts
|
|
6
168
|
/** Cordis plugin name (diagnostics only). */
|
|
7
169
|
const name = "data-agent-tool";
|
|
@@ -19,12 +181,12 @@ const Config = z.object({
|
|
|
19
181
|
readonly: z.boolean().default(false),
|
|
20
182
|
clients: clientsSchema
|
|
21
183
|
});
|
|
22
|
-
/** One-line
|
|
184
|
+
/** One-line tool-call label (newlines collapsed). */
|
|
23
185
|
function oneLine(sql) {
|
|
24
186
|
const line = sql.replace(/\s+/g, " ").trim();
|
|
25
187
|
return line.length > 80 ? `${line.slice(0, 77)}...` : line;
|
|
26
188
|
}
|
|
27
|
-
/** Format the
|
|
189
|
+
/** Format the raw terminal result. */
|
|
28
190
|
function formatResult(value) {
|
|
29
191
|
const parts = [];
|
|
30
192
|
if (value.stdout.length > 0) parts.push(value.stdout);
|
|
@@ -33,8 +195,35 @@ function formatResult(value) {
|
|
|
33
195
|
if (value.exitCode !== 0) parts.push(`[exit code: ${value.exitCode ?? "signal"}]`);
|
|
34
196
|
return parts.join("\n");
|
|
35
197
|
}
|
|
198
|
+
/** Format the structured result as JSON text (the canonical value stays JSON). */
|
|
199
|
+
function formatStructuredResult(value) {
|
|
200
|
+
return "```json\n" + JSON.stringify(value, null, 2) + "\n```";
|
|
201
|
+
}
|
|
202
|
+
/** Look up the session connection, failing with the same message for every tool. */
|
|
203
|
+
function requireToolConnection(ctx, exec, toolName) {
|
|
204
|
+
const sessionId = exec.agent?.id;
|
|
205
|
+
if (sessionId === void 0) throw new Error(`${toolName}: 缺少会话上下文(agent loop 未注入)`);
|
|
206
|
+
const connection = ctx.dataAgentConnections.getWithSecret(sessionId);
|
|
207
|
+
if (connection === void 0) throw new Error(`请先在「数据库」标签页连接数据库,再使用 ${toolName}(未找到当前会话的连接)`);
|
|
208
|
+
return connection;
|
|
209
|
+
}
|
|
210
|
+
/** Empty and multi-statement checks shared by all three tools. */
|
|
211
|
+
function validateSingleSql(sql, toolName) {
|
|
212
|
+
if (sql.trim().length === 0) throw new Error(`${toolName}: sql 不能为空`);
|
|
213
|
+
assertSingleStatement(sql, toolName);
|
|
214
|
+
}
|
|
215
|
+
/** Query runner options with the deployment overrides applied. */
|
|
216
|
+
function runnerOptions(resolved, mode) {
|
|
217
|
+
return {
|
|
218
|
+
clients: resolved.clients,
|
|
219
|
+
timeoutMs: resolved.queryTimeoutMs,
|
|
220
|
+
maxResultChars: resolved.maxResultChars,
|
|
221
|
+
...mode !== void 0 ? { mode } : {}
|
|
222
|
+
};
|
|
223
|
+
}
|
|
36
224
|
/**
|
|
37
|
-
* Mount the
|
|
225
|
+
* Mount the data-agent database tools: `sql-query` (structured read-only),
|
|
226
|
+
* `sql-write` (explicit write semantics), and `sqlcmd` (raw compatibility).
|
|
38
227
|
* @param ctx - the preset-scoped agent context.
|
|
39
228
|
* @param config - validated loader configuration.
|
|
40
229
|
*/
|
|
@@ -46,13 +235,146 @@ function apply(ctx, config) {
|
|
|
46
235
|
readonly: config.readonly,
|
|
47
236
|
clients: config.clients
|
|
48
237
|
};
|
|
238
|
+
ctx.tools.register(defineTool({
|
|
239
|
+
name: "sql-query",
|
|
240
|
+
description: `在已连接数据库上执行一条只读 SQL(SELECT/SHOW/DESCRIBE/EXPLAIN,SQLite 还含查询型 PRAGMA),返回结构化 JSON:{ columns, rows, affectedRows, elapsedMs, truncated }。SELECT 未写 LIMIT 时会自动限制为最多 ${resolved.maxRows} 行;所有结果最多返回 ${resolved.maxRows} 行。只执行单条语句;写操作请使用 sql-write,原始客户端输出请使用 sqlcmd。`,
|
|
241
|
+
parameters: { sql: {
|
|
242
|
+
type: "string",
|
|
243
|
+
required: true,
|
|
244
|
+
description: "一条只读 SQL,如 \"SELECT * FROM orders LIMIT 5;\"、\"SHOW TABLES;\"、\"DESCRIBE users;\""
|
|
245
|
+
} },
|
|
246
|
+
output: {
|
|
247
|
+
schema: {
|
|
248
|
+
type: "object",
|
|
249
|
+
properties: {
|
|
250
|
+
columns: {
|
|
251
|
+
type: "array",
|
|
252
|
+
items: { type: "string" },
|
|
253
|
+
required: true
|
|
254
|
+
},
|
|
255
|
+
rows: {
|
|
256
|
+
type: "array",
|
|
257
|
+
items: {
|
|
258
|
+
type: "object",
|
|
259
|
+
properties: {},
|
|
260
|
+
additionalProperties: true
|
|
261
|
+
},
|
|
262
|
+
required: true
|
|
263
|
+
},
|
|
264
|
+
affectedRows: {
|
|
265
|
+
type: "integer",
|
|
266
|
+
required: true
|
|
267
|
+
},
|
|
268
|
+
elapsedMs: {
|
|
269
|
+
type: "integer",
|
|
270
|
+
required: true
|
|
271
|
+
},
|
|
272
|
+
truncated: {
|
|
273
|
+
type: "boolean",
|
|
274
|
+
required: true
|
|
275
|
+
}
|
|
276
|
+
},
|
|
277
|
+
additionalProperties: false
|
|
278
|
+
},
|
|
279
|
+
render: (_args, value) => [{
|
|
280
|
+
type: "text",
|
|
281
|
+
text: formatStructuredResult(value)
|
|
282
|
+
}]
|
|
283
|
+
},
|
|
284
|
+
presentCall: (args) => ({
|
|
285
|
+
card: "generic",
|
|
286
|
+
kind: "read",
|
|
287
|
+
title: `sql-query ${oneLine(args.sql)}`,
|
|
288
|
+
rawInput: args.sql
|
|
289
|
+
}),
|
|
290
|
+
presentResult: (args, result) => ({
|
|
291
|
+
card: "generic",
|
|
292
|
+
title: `sql-query ${oneLine(args.sql)}`,
|
|
293
|
+
content: result.content
|
|
294
|
+
}),
|
|
295
|
+
async execute(args, exec) {
|
|
296
|
+
const connection = requireToolConnection(ctx, exec, "sql-query");
|
|
297
|
+
validateSingleSql(args.sql, "sql-query");
|
|
298
|
+
if (classifyStatement(args.sql, connection.type) !== "read") throw new Error("sql-query 只执行读语句(SELECT/SHOW/DESCRIBE/EXPLAIN,SQLite 还含查询型 PRAGMA);写语句请使用 sql-write");
|
|
299
|
+
const limitedSql = enforceReadRowLimit(args.sql, connection.type, resolved.maxRows);
|
|
300
|
+
const startedAt = Date.now();
|
|
301
|
+
const result = await runClientQuery(ctx, connection, limitedSql, runnerOptions(resolved, "structured"), exec.signal);
|
|
302
|
+
const elapsedMs = Date.now() - startedAt;
|
|
303
|
+
if (result.exitCode !== 0) {
|
|
304
|
+
const detail = result.stderr.trim() !== "" ? result.stderr.trim() : result.stdout.trim();
|
|
305
|
+
throw new Error(`sql-query 执行失败(exit ${result.exitCode}):${detail}`);
|
|
306
|
+
}
|
|
307
|
+
const parsed = parseStructuredQueryOutput(connection.type, result.stdout, resolved.maxRows);
|
|
308
|
+
return {
|
|
309
|
+
columns: parsed.columns,
|
|
310
|
+
rows: parsed.rows,
|
|
311
|
+
affectedRows: 0,
|
|
312
|
+
elapsedMs,
|
|
313
|
+
truncated: result.truncated || parsed.rowLimitExceeded
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
}));
|
|
317
|
+
ctx.tools.register(defineTool({
|
|
318
|
+
name: "sql-write",
|
|
319
|
+
description: "在已连接数据库上执行一条写/管理语句(INSERT/UPDATE/DELETE/DDL 等)。每次调用都是独立客户端进程并自动提交,只接受单条语句,不支持跨调用的多语句事务;如需原子性,请改用单条 SQL(如 INSERT ... SELECT)或数据库端脚本/存储过程。只读查询请使用 sql-query。",
|
|
320
|
+
parameters: { sql: {
|
|
321
|
+
type: "string",
|
|
322
|
+
required: true,
|
|
323
|
+
description: "一条写/管理 SQL,如 \"INSERT INTO t VALUES (1);\"、\"UPDATE t SET x=1;\"、\"CREATE INDEX idx_t_x ON t(x);\""
|
|
324
|
+
} },
|
|
325
|
+
output: {
|
|
326
|
+
schema: {
|
|
327
|
+
type: "object",
|
|
328
|
+
properties: {
|
|
329
|
+
exitCode: {
|
|
330
|
+
oneOf: [{ type: "integer" }, { type: "null" }],
|
|
331
|
+
required: true
|
|
332
|
+
},
|
|
333
|
+
stdout: {
|
|
334
|
+
type: "string",
|
|
335
|
+
required: true
|
|
336
|
+
},
|
|
337
|
+
stderr: {
|
|
338
|
+
type: "string",
|
|
339
|
+
required: true
|
|
340
|
+
},
|
|
341
|
+
truncated: {
|
|
342
|
+
type: "boolean",
|
|
343
|
+
required: true
|
|
344
|
+
}
|
|
345
|
+
},
|
|
346
|
+
additionalProperties: false
|
|
347
|
+
},
|
|
348
|
+
render: (_args, value) => [{
|
|
349
|
+
type: "text",
|
|
350
|
+
text: formatResult(value)
|
|
351
|
+
}]
|
|
352
|
+
},
|
|
353
|
+
presentCall: (args) => ({
|
|
354
|
+
card: "terminal",
|
|
355
|
+
title: `sql-write ${oneLine(args.sql)}`,
|
|
356
|
+
description: "执行一条写/管理 SQL(自动提交)"
|
|
357
|
+
}),
|
|
358
|
+
presentResult: (args, result) => ({
|
|
359
|
+
card: "terminal",
|
|
360
|
+
title: `sql-write ${oneLine(args.sql)}`,
|
|
361
|
+
content: result.content
|
|
362
|
+
}),
|
|
363
|
+
async execute(args, exec) {
|
|
364
|
+
const connection = requireToolConnection(ctx, exec, "sql-write");
|
|
365
|
+
validateSingleSql(args.sql, "sql-write");
|
|
366
|
+
if (classifyStatement(args.sql, connection.type) === "read") throw new Error("sql-write 只执行写/管理语句;只读查询请使用 sql-query");
|
|
367
|
+
if (connection.readonly ?? resolved.readonly) throw new Error("当前连接为只读模式,sql-write 拒绝执行写/管理语句(仅放行 SELECT/SHOW/DESCRIBE/EXPLAIN/查询型 PRAGMA 等)");
|
|
368
|
+
return runClientQuery(ctx, connection, args.sql, runnerOptions(resolved), exec.signal);
|
|
369
|
+
}
|
|
370
|
+
}));
|
|
49
371
|
ctx.tools.register(defineTool({
|
|
50
372
|
name: "sqlcmd",
|
|
51
|
-
description:
|
|
373
|
+
description: `在已连接数据库上执行一条 SQL 或客户端命令(如 SHOW TABLES、DESCRIBE users),返回原始 exitCode/stdout/stderr 文本。新调用优先使用 sql-query(结构化只读结果)和 sql-write(明确写语义)。一次只执行一条语句;读 SELECT 会自动限制最多 ${resolved.maxRows} 行;每次调用为独立客户端进程并自动提交。`,
|
|
52
374
|
parameters: { sql: {
|
|
53
375
|
type: "string",
|
|
54
376
|
required: true,
|
|
55
|
-
description: "
|
|
377
|
+
description: "一条 SQL 文本(或客户端命令),如 \"SHOW TABLES;\"、\"DESCRIBE users;\"、\"SELECT * FROM orders LIMIT 5;\""
|
|
56
378
|
} },
|
|
57
379
|
output: {
|
|
58
380
|
schema: {
|
|
@@ -85,7 +407,7 @@ function apply(ctx, config) {
|
|
|
85
407
|
presentCall: (args) => ({
|
|
86
408
|
card: "terminal",
|
|
87
409
|
title: `sqlcmd ${oneLine(args.sql)}`,
|
|
88
|
-
description: "
|
|
410
|
+
description: "在数据库客户端执行一条 SQL"
|
|
89
411
|
}),
|
|
90
412
|
presentResult: (args, result) => ({
|
|
91
413
|
card: "terminal",
|
|
@@ -93,16 +415,11 @@ function apply(ctx, config) {
|
|
|
93
415
|
content: result.content
|
|
94
416
|
}),
|
|
95
417
|
async execute(args, exec) {
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
return runClientQuery(ctx, connection, args.sql, {
|
|
102
|
-
clients: resolved.clients,
|
|
103
|
-
timeoutMs: resolved.queryTimeoutMs,
|
|
104
|
-
maxResultChars: resolved.maxResultChars
|
|
105
|
-
}, exec.signal);
|
|
418
|
+
const connection = requireToolConnection(ctx, exec, "sqlcmd");
|
|
419
|
+
validateSingleSql(args.sql, "sqlcmd");
|
|
420
|
+
if ((connection.readonly ?? resolved.readonly) && classifyStatement(args.sql, connection.type) === "write") throw new Error("当前连接为只读模式,sqlcmd 拒绝执行非读语句(仅放行 SELECT/SHOW/DESCRIBE/EXPLAIN/查询型 PRAGMA 等)");
|
|
421
|
+
const sql = classifyStatement(args.sql, connection.type) === "read" ? enforceReadRowLimit(args.sql, connection.type, resolved.maxRows) : args.sql;
|
|
422
|
+
return runClientQuery(ctx, connection, sql, runnerOptions(resolved), exec.signal);
|
|
106
423
|
}
|
|
107
424
|
}));
|
|
108
425
|
}
|
package/lib/types/clients.d.ts
CHANGED
|
@@ -12,12 +12,26 @@
|
|
|
12
12
|
* @module @yejiming/dsh-data-agent/clients
|
|
13
13
|
*/
|
|
14
14
|
import type { DatabaseConnection, DatabaseType } from './connections.ts';
|
|
15
|
+
import { assertSingleStatement, hasTopLevelKeyword, stripTrailingTerminator } from './sql.ts';
|
|
16
|
+
export { assertSingleStatement, hasTopLevelKeyword, stripTrailingTerminator };
|
|
15
17
|
/**
|
|
16
18
|
* Classify a SQL text as a read or write statement by its FIRST effective
|
|
17
19
|
* token (a conservative read whitelist, not a parser). `with` is read only
|
|
18
|
-
* when its body's first token is `select`. `pragma` is read
|
|
20
|
+
* when its body's first token is `select`. SQLite `pragma` is read in its
|
|
21
|
+
* query form and write when a value is assigned.
|
|
19
22
|
*/
|
|
20
23
|
export declare function classifyStatement(sql: string, type: DatabaseType): 'read' | 'write';
|
|
24
|
+
/**
|
|
25
|
+
* Enforce the configured `maxRows` on a read query instead of relying on the
|
|
26
|
+
* prompt. SELECT/CTE-read statements get a real top-level LIMIT (Oracle uses
|
|
27
|
+
* a ROWNUM wrapper because it has no LIMIT); SHOW/DESCRIBE/EXPLAIN/PRAGMA are
|
|
28
|
+
* left untouched here and are capped while parsing structured output.
|
|
29
|
+
*
|
|
30
|
+
* An existing numeric top-level LIMIT is rewritten when it is larger than
|
|
31
|
+
* `maxRows`; a smaller existing LIMIT is preserved, and a non-numeric or
|
|
32
|
+
* unparseable LIMIT is left for the client (structured tools still truncate).
|
|
33
|
+
*/
|
|
34
|
+
export declare function enforceReadRowLimit(sql: string, type: DatabaseType, maxRows: number): string;
|
|
21
35
|
/**
|
|
22
36
|
* Validate and quote one schema/table identifier for a safe metadata query.
|
|
23
37
|
* Identifiers are restricted to `[A-Za-z0-9_$]+` and then wrapped per type:
|
|
@@ -73,6 +87,12 @@ export interface ClientTemplate {
|
|
|
73
87
|
export declare function buildClientTemplate(type: DatabaseType, connection: DatabaseConnection, override?: ClientConfig): ClientTemplate;
|
|
74
88
|
/** Build one client invocation for metadata runs (machine-readable flags). */
|
|
75
89
|
export declare function buildIntrospectTemplate(type: DatabaseType, connection: DatabaseConnection, override?: ClientConfig): ClientTemplate;
|
|
90
|
+
/**
|
|
91
|
+
* Build one client invocation for the structured `sql-query` tool: every
|
|
92
|
+
* supported client prints a header row followed by one row per line (mysql
|
|
93
|
+
* tab, postgres pipe, sqlite csv, oracle pipe, hive/impala tsv).
|
|
94
|
+
*/
|
|
95
|
+
export declare function buildStructuredQueryTemplate(type: DatabaseType, connection: DatabaseConnection, override?: ClientConfig): ClientTemplate;
|
|
76
96
|
/**
|
|
77
97
|
* The table-listing SQL per type, run at /connect time to verify
|
|
78
98
|
* connectivity: the connected database's own tables (mysql uses the
|
|
@@ -82,7 +102,7 @@ export declare function buildIntrospectTemplate(type: DatabaseType, connection:
|
|
|
82
102
|
export declare function tableListingSql(type: DatabaseType, connection?: DatabaseConnection): string;
|
|
83
103
|
/**
|
|
84
104
|
* Metadata query per kind × type. `schema`/`table` are identifier whitelist
|
|
85
|
-
* validated by the caller (`[A-Za-z0-9_
|
|
105
|
+
* validated by the caller (`[A-Za-z0-9_$]`) before they reach here.
|
|
86
106
|
*/
|
|
87
107
|
export declare function metadataQuery(kind: 'schemas' | 'tables' | 'describe', type: DatabaseType, schema?: string, table?: string): string;
|
|
88
108
|
/**
|
package/lib/types/defaults.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Package-wide defaults shared by the server half (`src/index.ts`) and the
|
|
3
|
-
*
|
|
3
|
+
* database tool half (`src/tool.ts`). Loader schemas carry these as their
|
|
4
4
|
* defaults so a deployment may override every one of them in cordis.yml.
|
|
5
5
|
* @module @yejiming/dsh-data-agent/defaults
|
|
6
6
|
*/
|
|
@@ -10,9 +10,9 @@ export declare const DEFAULT_PRESET_ID = "data-agent";
|
|
|
10
10
|
export declare const DEFAULT_CONNECT_TIMEOUT_MS = 10000;
|
|
11
11
|
/** Cap on the table list returned by `/connect` and `/status`. */
|
|
12
12
|
export declare const DEFAULT_INTROSPECT_MAX_TABLES = 500;
|
|
13
|
-
/** End-to-end deadline for one
|
|
13
|
+
/** End-to-end deadline for one database-tool query, milliseconds. */
|
|
14
14
|
export declare const DEFAULT_QUERY_TIMEOUT_MS = 30000;
|
|
15
|
-
/** In-memory cap on
|
|
15
|
+
/** In-memory cap on database-tool captured output (stdout and stderr each). */
|
|
16
16
|
export declare const DEFAULT_MAX_RESULT_CHARS = 20000;
|
|
17
17
|
/** Cap on one /query SQL text length (abuse guard; the wire body stays small). */
|
|
18
18
|
export declare const DEFAULT_MAX_QUERY_CHARS = 65536;
|
package/lib/types/index.d.ts
CHANGED
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
*
|
|
9
9
|
* The HTTP routes live in the separate `./routes` entry
|
|
10
10
|
* (`@yejiming/dsh-data-agent/routes`, cordis row `data-agent-routes`) so
|
|
11
|
-
* this row keeps working in headless profiles without a webserver; the
|
|
12
|
-
*
|
|
13
|
-
* data-agent preset.
|
|
11
|
+
* this row keeps working in headless profiles without a webserver; the
|
|
12
|
+
* database tools themselves live in the `./tool` entry and are mounted only
|
|
13
|
+
* by the data-agent preset.
|
|
14
14
|
* @module @yejiming/dsh-data-agent
|
|
15
15
|
*/
|
|
16
16
|
import type { Context } from '@deepseek-ai/cordis';
|
|
@@ -57,11 +57,11 @@ export interface Config {
|
|
|
57
57
|
connectTimeoutMs: number;
|
|
58
58
|
/** Cap on the table list returned by /connect and /status. */
|
|
59
59
|
introspectMaxTables: number;
|
|
60
|
-
/** Deadline for one
|
|
60
|
+
/** Deadline for one database-tool query, milliseconds. */
|
|
61
61
|
queryTimeoutMs: number;
|
|
62
|
-
/** In-memory cap on
|
|
62
|
+
/** In-memory cap on database-tool captured output. */
|
|
63
63
|
maxResultChars: number;
|
|
64
|
-
/** Default read-only guard: true rejects write statements in
|
|
64
|
+
/** Default read-only guard: true rejects write statements in database tools and /query. */
|
|
65
65
|
readonly: boolean;
|
|
66
66
|
/** CLI client overrides keyed by database type. */
|
|
67
67
|
clients: ClientsConfig;
|
package/lib/types/query.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The shared client-process runner used by both halves: the /connect
|
|
3
|
-
* connectivity check (server half) and the
|
|
3
|
+
* connectivity check (server half) and the database tools (tool half). All
|
|
4
4
|
* execution goes through `ctx.subprocess` — no shell layer, argv arrays only,
|
|
5
5
|
* SQL on stdin, credentials in env entries — with a caller-owned timeout
|
|
6
6
|
* (AbortController → process-tree terminate escalation) and bounded captured
|
|
@@ -15,7 +15,7 @@ export interface CapturedOutput {
|
|
|
15
15
|
text: string;
|
|
16
16
|
truncated: boolean;
|
|
17
17
|
}
|
|
18
|
-
/** The canonical
|
|
18
|
+
/** The canonical database-tool / connectivity-check result. */
|
|
19
19
|
export interface QueryResult {
|
|
20
20
|
/** Process exit code; null when the process died from a signal. */
|
|
21
21
|
exitCode: number | null;
|
|
@@ -26,6 +26,8 @@ export interface QueryResult {
|
|
|
26
26
|
/** True when either stream hit the maxResultChars cap. */
|
|
27
27
|
truncated: boolean;
|
|
28
28
|
}
|
|
29
|
+
/** Which CLI flag set to use for one run. */
|
|
30
|
+
export type QueryTemplateMode = 'query' | 'introspect' | 'structured';
|
|
29
31
|
/** Runner options: client overrides, deadlines, output caps. */
|
|
30
32
|
export interface QueryOptions {
|
|
31
33
|
/** Deployment client overrides keyed by database type. */
|
|
@@ -36,6 +38,8 @@ export interface QueryOptions {
|
|
|
36
38
|
maxResultChars: number;
|
|
37
39
|
/** Grace period for the terminate escalation; defaults to 5s. */
|
|
38
40
|
graceMs?: number;
|
|
41
|
+
/** CLI flag set; overrides the legacy `introspect` parameter when set. */
|
|
42
|
+
mode?: QueryTemplateMode;
|
|
39
43
|
}
|
|
40
44
|
/**
|
|
41
45
|
* Run one SQL text through the type's CLI client. The SQL is written to the
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight SQL-text scanning helpers shared by the sqlcmd tool half and
|
|
3
|
+
* the /query route. This is intentionally NOT a SQL parser: the scanner only
|
|
4
|
+
* understands lexical boundaries (strings, quoted identifiers, comments and
|
|
5
|
+
* parenthesis depth) well enough to make the two agent-loop guarantees from
|
|
6
|
+
* docs/optimization-opportunities.md:
|
|
7
|
+
*
|
|
8
|
+
* - a single tool call carries at most ONE SQL statement;
|
|
9
|
+
* - `maxRows` can be enforced with a real top-level LIMIT, not just a prompt.
|
|
10
|
+
*
|
|
11
|
+
* @module @yejiming/dsh-data-agent/sql
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Throw unless `sql` contains at most one statement. A single trailing
|
|
15
|
+
* semicolon (and any number of repeated trailing semicolons / comments) is
|
|
16
|
+
* accepted; a semicolon followed by real content is rejected.
|
|
17
|
+
*/
|
|
18
|
+
export declare function assertSingleStatement(sql: string, label?: string): void;
|
|
19
|
+
/** Whether `keyword` appears at top level as a whole word in `sql`. */
|
|
20
|
+
export declare function hasTopLevelKeyword(sql: string, keyword: string): boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Strip trailing whitespace, statement terminators and trailing comments so a
|
|
23
|
+
* limit clause can be appended to the actual statement text. Only comments
|
|
24
|
+
* that occupy the whole tail are removed; the preceding statement is kept.
|
|
25
|
+
*/
|
|
26
|
+
export declare function stripTrailingTerminator(sql: string): string;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured result parsing for the `sql-query` tool half. Each supported
|
|
3
|
+
* database type has a machine-readable query template (see
|
|
4
|
+
* {@link buildStructuredQueryTemplate} in `src/clients.ts`); this module turns
|
|
5
|
+
* that captured stdout into the canonical `{ columns, rows }` shape and
|
|
6
|
+
* enforces the row cap a second time (first line of defense is the SQL-level
|
|
7
|
+
* LIMIT injection, second is truncation while parsing).
|
|
8
|
+
*
|
|
9
|
+
* The parsers are deliberately output-shape based, not grammar based. Values
|
|
10
|
+
* stay as strings because every client renders SQL values as text (NULL
|
|
11
|
+
* rendering differs per client), and duplicate column names are made unique
|
|
12
|
+
* so the row objects are valid lossless JSON maps.
|
|
13
|
+
* @module @yejiming/dsh-data-agent/structured
|
|
14
|
+
*/
|
|
15
|
+
import type { DatabaseType } from './connections.ts';
|
|
16
|
+
/** Canonical parsed query output before elapsed/affected metadata is added. */
|
|
17
|
+
export interface ParsedQueryOutput {
|
|
18
|
+
columns: string[];
|
|
19
|
+
rows: Record<string, string | null>[];
|
|
20
|
+
/** True when the output contained more rows than `maxRows` and was capped. */
|
|
21
|
+
rowLimitExceeded: boolean;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Parse one database type's structured-query stdout. The matching template is
|
|
25
|
+
* `buildStructuredQueryTemplate`: mysql tab-separated with a header, postgres
|
|
26
|
+
* pipe-separated with a header and row-count footer, sqlite CSV with a header,
|
|
27
|
+
* oracle pipe-separated with heading on, hive/impala tsv with a header.
|
|
28
|
+
*/
|
|
29
|
+
export declare function parseStructuredQueryOutput(type: DatabaseType, stdout: string, maxRows: number): ParsedQueryOutput;
|
package/lib/types/tool.d.ts
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The
|
|
2
|
+
* The data-agent tool half (`@yejiming/dsh-data-agent/tool`): mounted ONLY by
|
|
3
3
|
* the data-agent agent preset (`preset/data-agent/agent.cordis.yml`), never
|
|
4
4
|
* by the host composition. It consumes the host's `subprocess` service and
|
|
5
5
|
* the host-provided `dataAgentConnections` connection store, so it needs no
|
|
6
6
|
* realm and satisfies the preset guard (a preset row that only consumes).
|
|
7
7
|
*
|
|
8
|
+
* Tool surface:
|
|
9
|
+
* - `sql-query`: read-only statements, structured `{ columns, rows, ... }`;
|
|
10
|
+
* - `sql-write`: one write/management statement per call, explicit autocommit;
|
|
11
|
+
* - `sqlcmd`: the original raw-terminal tool (kept for compatibility).
|
|
12
|
+
*
|
|
8
13
|
* Execution model (see `src/query.ts`): the SQL text travels on the client's
|
|
9
14
|
* stdin, argv carries flags only, credentials go through environment entries
|
|
10
15
|
* (`MYSQL_PWD` / `PGPASSWORD`), and the caller's signal plus an internal
|
|
@@ -20,11 +25,11 @@ export declare const name = "data-agent-tool";
|
|
|
20
25
|
export declare const inject: string[];
|
|
21
26
|
/** Tool-half configuration (loader schema with the same defaults as the host). */
|
|
22
27
|
export interface Config {
|
|
23
|
-
/** Deadline for one sqlcmd query, milliseconds. */
|
|
28
|
+
/** Deadline for one sqlcmd / sql-query / sql-write query, milliseconds. */
|
|
24
29
|
queryTimeoutMs: number;
|
|
25
30
|
/** In-memory cap on captured output. */
|
|
26
31
|
maxResultChars: number;
|
|
27
|
-
/**
|
|
32
|
+
/** Enforced read-query row cap (LIMIT injection + structured truncation). */
|
|
28
33
|
maxRows: number;
|
|
29
34
|
/** Read-only guard: true rejects write statements. */
|
|
30
35
|
readonly: boolean;
|
|
@@ -58,7 +63,8 @@ export declare const Config: import("@deepseek-ai/schemastery").default<Schemast
|
|
|
58
63
|
}>, string>>;
|
|
59
64
|
}>>;
|
|
60
65
|
/**
|
|
61
|
-
* Mount the
|
|
66
|
+
* Mount the data-agent database tools: `sql-query` (structured read-only),
|
|
67
|
+
* `sql-write` (explicit write semantics), and `sqlcmd` (raw compatibility).
|
|
62
68
|
* @param ctx - the preset-scoped agent context.
|
|
63
69
|
* @param config - validated loader configuration.
|
|
64
70
|
*/
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yejiming/dsh-data-agent",
|
|
3
3
|
"description": "Data Agent for the dsh web GUI: session-scoped database connections (MySQL/PostgreSQL/SQLite), the sqlcmd tool, the data-agent agent preset, and the database conversation-view tab",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.6",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|