@tea-agent/loop-agent 0.44.0-next.1 → 0.44.0-next.3
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/CHANGELOG.md +178 -175
- package/dist/build-stamp.json +3 -3
- package/dist/executors/pi-sdk-executor.js +29 -1
- package/dist/shared/wizard-local-telemetry.js +50 -0
- package/dist/worker/console/chat/chat-event-store.js +3 -0
- package/dist/worker/console/chat/db-connections.js +790 -0
- package/dist/worker/console/chat/pi-runtime/custom-tools/operator-tools.js +1 -1
- package/dist/worker/console/chat/pi-runtime/custom-tools/sql-tools.js +627 -0
- package/dist/worker/console/chat/pi-runtime.js +66 -6
- package/dist/worker/console/chat/routes.js +154 -3
- package/dist/worker/console/chat/sdd-data-alignment.js +8 -31
- package/dist/worker/console/chat/timeline-message-order.js +22 -0
- package/dist/worker/console/chat/tools.js +34 -0
- package/dist/worker/console/chat/turn-process.js +43 -39
- package/dist/worker/console/dag-confirmation.js +3 -0
- package/dist/worker/console/dag-execution-receipt.js +3 -0
- package/dist/worker/console/operator-actions.js +32 -2
- package/dist/worker/console/server.js +5 -1
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-8-j6y-sd.js → abnfDiagram-N423BO3Z-COQlLTI7.js} +1 -1
- package/dist/worker/console/static/assets/{arc-eoQiMvuk.js → arc-zN3QkNaV.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-C4A3uMcI.js → architectureDiagram-T3A2C74G-Pyoyj5f2.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-D4zD2F-Q.js → blockDiagram-VBNYF7ZC-BNppA5WW.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-j1RkJziL.js → c4Diagram-5PPSVZJV-lyxL8nTx.js} +1 -1
- package/dist/worker/console/static/assets/channel-CgLPkTw2.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-YBHmhik1.js → chunk-2GRJ4B5K-0hgjcdK4.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-COfWyo9P.js → chunk-2Q5K7J3B-D63N9d4i.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-I99OUkHY.js → chunk-5RXB4S5H-CUw2AyB5.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-XKxoJNJ4.js → chunk-5VM5RSS4-Q_kwZT30.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DLT_cYx1.js → chunk-6Q2QTUOP-C7vMiKcw.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CxcKZ9mV.js → chunk-GF5L2VYU-T9GbV5Hz.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-V1EqXdjY.js → chunk-JWPE2WC7-D8nUSUJI.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-DebTtdFp.js → chunk-KBJHAD2P-BCH2ewdp.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-B-ivNRDf.js → chunk-RYQCIY6F-BTdDuDBN.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-DC_Ds11b.js → chunk-XXDRQBXY-Cqm2RcKF.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-CuSD1Mhb.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-CuSD1Mhb.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-sWEqKwIb.js → cose-bilkent-JH36ORCC-pjwfpVu4.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BXa_dcu4.js → cynefin-VYW2F7L2-BvalptKA.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-C-Mle84F.js → cynefinDiagram-MW4NZA55-BU4qJ6d7.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-CXPDBITe.js → dagre-VZM6K2ZE-DVTpq8J9.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-CC-WJQfa.js → diagram-7IWD3JNH-B8s1W6Fc.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-CDAV5Vs4.js → diagram-B4RE2ZJO-DPrFk9yH.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-CmFiAcNz.js → diagram-LBJQPF4R-CnQIQsHC.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-DPBZHuyn.js → diagram-Q27KOJAE-op0DEpCv.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-jUlm_Ds3.js → diagram-UB23O5K3-DXQamm4T.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-DWhQ3mfY.js → ebnfDiagram-BXEA7PRR-b1ad2C0r.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-Tr2gMqet.js → erDiagram-JOGREHBK-B4cD-ZFJ.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-DJjVQHPA.js → flowDiagram-UKHOOZJN-CrgGjlQ1.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-D74dQ4u0.js → ganttDiagram-PKOTCBZU-DovmXQNV.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-DwW0tZ0X.js → gitGraphDiagram-DS77QQ5N-DpFyu2Jn.js} +1 -1
- package/dist/worker/console/static/assets/index-DHBWEwz_.css +1 -0
- package/dist/worker/console/static/assets/index-JRfTUgP7.js +478 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-ILCbxJyb.js → infoDiagram-6WML65LV-D3MoQZIl.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-DeuWBQ89.js → ishikawaDiagram-WSZJBQD7-Ci6C1yjS.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-CF5ih8Fk.js → journeyDiagram-NVQOT4AX-BWMo2lIP.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-C7yOSuRO.js → kanban-definition-27J2QSJJ-BdthIAg3.js} +1 -1
- package/dist/worker/console/static/assets/{linear-BDZ9riWi.js → linear-DuFAlSOj.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-7pKqYtpZ.js → mermaid.core-Dqz3Slj9.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-LeJDybSU.js → mindmap-definition-FAOFIHXS-DNVTrD09.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-BJvT3pMD.js → pegDiagram-VL7TDLO6-CgfZPfBh.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-_rGqpMan.js → pieDiagram-7S7Q4E2Y-ClM9P7yo.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-DPcSv5aA.js → quadrantDiagram-CIZ2JOQS-DwnEak2J.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-Drxx4hkJ.js → railroadDiagram-AXF67PYL-rK4hxt9P.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-BCTUdU4z.js → requirementDiagram-LRYGKXZP-Cjqa6vo3.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-B6wzbZmB.js → sankeyDiagram-W5VNT64P-D3Fa250Z.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-BGt8d3QQ.js → sequenceDiagram-SI44F4Z6-2PdrNajj.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-7nlhJKo7.js → sizeCapture-X5ZJPWSS-BBcuGeZn.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-Cv2stqMA.js → stateDiagram-OKZ733FA-CSJX9X6s.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-Cqskj0zt.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-qDAo4Yc1.js → swimlanes-SLNWSIFB-pOUU1bic.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-CM0ZIhNP.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CKbDNKTr.js → timeline-definition-Z64GVDOM-CAeqfQn0.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-DI-9EHic.js → vennDiagram-T6HMQDX7-DYPLOMVO.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-BrzzRDpX.js → wardleyDiagram-T6FBY63Y-Dy2V0Cha.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-BclH5hGh.js → xychartDiagram-ELKLHX3M-Dj9o7jrn.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +33 -1
- package/dist/worker/console/static-src/operator-chat/chat-stream-continuity.js +101 -0
- package/dist/worker/console/static-src/operator-chat/compaction-message.js +18 -3
- package/dist/worker/console/static-src/operator-chat/pending-user-message.js +17 -3
- package/dist/worker/console/static-src/operator-chat/tools-catalog.js +16 -7
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +15 -7
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +47 -5
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +24 -2
- package/dist/worker/console/workspace-context.js +2 -0
- package/dist/worker/observe/static/console-theme.css +174 -0
- package/dist/worker/observe/static/console-theme.js +36 -0
- package/dist/worker/observe/static/operator-chrome.css +85 -0
- package/dist/worker/observe/static/operator-chrome.js +51 -0
- package/dist/worker/scheduler/scheduled-goal-evidence.js +60 -2
- package/dist/worker/scheduler/scheduled-goal-store.js +10 -0
- package/package.json +3 -1
- package/dist/worker/console/static/assets/channel-ChE7y-cx.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C01TCf2X.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C01TCf2X.js +0 -1
- package/dist/worker/console/static/assets/index-BWkIfcrK.css +0 -1
- package/dist/worker/console/static/assets/index-Cdkvw_H6.js +0 -469
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DC7V5vcq.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Bwy4QUTO.js +0 -8
|
@@ -80,7 +80,7 @@ export async function buildCustomOperatorTools(options) {
|
|
|
80
80
|
}
|
|
81
81
|
// Defer require to avoid circular import at module load.
|
|
82
82
|
const { dispatchChatToolCall } = await import("../../session-store.js");
|
|
83
|
-
const result = await dispatchChatToolCall(ctx, decision.toolId, params, toolCallId, options.sessionMode);
|
|
83
|
+
const result = await dispatchChatToolCall({ ...ctx, chatSessionRef: options.sessionId }, decision.toolId, params, toolCallId, options.sessionMode);
|
|
84
84
|
const payload = result.ok
|
|
85
85
|
? result.result
|
|
86
86
|
: { error: result.message, code: result.errorCode };
|
|
@@ -0,0 +1,627 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Operator Chat — governed SQL tools (2026-09-12).
|
|
3
|
+
*
|
|
4
|
+
* Six model-callable custom tools over DbConnectionRegistry:
|
|
5
|
+
* sql_query read-only (SELECT/PRAGMA/EXPLAIN/SHOW/DESCRIBE/WITH),
|
|
6
|
+
* lexically enforced, multi-statement rejected
|
|
7
|
+
* sql_exec permanently disabled: Console database access is read-only
|
|
8
|
+
* regardless of confirmation
|
|
9
|
+
* sql_schema list tables / describe one table's columns
|
|
10
|
+
* sql_stats table count, per-table row counts, db size
|
|
11
|
+
* sql_health per-connection SELECT 1 + governance summary
|
|
12
|
+
* sql_connections list/add/remove/test connection configs (sanitized)
|
|
13
|
+
*
|
|
14
|
+
* Envelope: success { ok: true, ... } / failure { ok: false, error: { code,
|
|
15
|
+
* message } } with stable DB_* error codes. Credentials (DSN values) are
|
|
16
|
+
* never echoed — postgres/mysql configs carry only the env var NAME.
|
|
17
|
+
*/
|
|
18
|
+
import { stat } from "node:fs/promises";
|
|
19
|
+
import path from "node:path";
|
|
20
|
+
import { isSqliteHandle, } from "../../db-connections.js";
|
|
21
|
+
export const SQL_QUERY_TIMEOUT_MS = 30_000;
|
|
22
|
+
export const SQL_EXEC_TIMEOUT_MS = 60_000;
|
|
23
|
+
export const SQL_MAX_ROWS_DEFAULT = 200;
|
|
24
|
+
export const SQL_MAX_ROWS_LIMIT = 1_000;
|
|
25
|
+
const READ_ONLY_LEADING_TOKENS = new Set([
|
|
26
|
+
"SELECT",
|
|
27
|
+
"PRAGMA",
|
|
28
|
+
"EXPLAIN",
|
|
29
|
+
"SHOW",
|
|
30
|
+
"DESCRIBE",
|
|
31
|
+
"WITH",
|
|
32
|
+
]);
|
|
33
|
+
const WRITE_LEADING_TOKENS = new Set([
|
|
34
|
+
"INSERT",
|
|
35
|
+
"UPDATE",
|
|
36
|
+
"DELETE",
|
|
37
|
+
"REPLACE",
|
|
38
|
+
"CREATE",
|
|
39
|
+
"DROP",
|
|
40
|
+
"ALTER",
|
|
41
|
+
"TRUNCATE",
|
|
42
|
+
"VACUUM",
|
|
43
|
+
"REINDEX",
|
|
44
|
+
"ATTACH",
|
|
45
|
+
"DETACH",
|
|
46
|
+
"BEGIN",
|
|
47
|
+
"COMMIT",
|
|
48
|
+
"ROLLBACK",
|
|
49
|
+
"SAVEPOINT",
|
|
50
|
+
"RELEASE",
|
|
51
|
+
"ANALYZE",
|
|
52
|
+
"GRANT",
|
|
53
|
+
"REVOKE",
|
|
54
|
+
"MERGE",
|
|
55
|
+
"CALL",
|
|
56
|
+
"EXECUTE",
|
|
57
|
+
"SET",
|
|
58
|
+
"USE",
|
|
59
|
+
"LOAD",
|
|
60
|
+
"IMPORT",
|
|
61
|
+
"COPY",
|
|
62
|
+
"OPTIMIZE",
|
|
63
|
+
"RENAME",
|
|
64
|
+
]);
|
|
65
|
+
const SQL_TOOLS_DESCRIPTION = "Governed SQL access over named database connections (sqlite via node:sqlite; PostgreSQL via pg; MySQL via mysql2). " +
|
|
66
|
+
"sql_query is READ-ONLY (SELECT/PRAGMA/EXPLAIN/SHOW/DESCRIBE/WITH; single statement). " +
|
|
67
|
+
"Before writing any SQL, call sql_schema to inspect real table/column names — never guess identifiers. " +
|
|
68
|
+
"sql_exec is permanently disabled: DELETE, DROP, INSERT, UPDATE, DDL, permission changes, and every other mutation are rejected even after confirmation. " +
|
|
69
|
+
"sql_stats counts rows per table (can be expensive on large tables). sql_health probes connectivity. " +
|
|
70
|
+
"sql_connections manages configs: adding a connection is low-risk, but run action=\"test\" before first real use.";
|
|
71
|
+
class SqlToolError extends Error {
|
|
72
|
+
code;
|
|
73
|
+
constructor(code, message, options) {
|
|
74
|
+
super(message, options);
|
|
75
|
+
this.code = code;
|
|
76
|
+
this.name = "SqlToolError";
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Strip SQL comments (`-- line`, `/* block *\/`) outside string literals so
|
|
81
|
+
* lexical checks see the executable tokens only.
|
|
82
|
+
*/
|
|
83
|
+
function stripSqlComments(sql) {
|
|
84
|
+
let out = "";
|
|
85
|
+
let i = 0;
|
|
86
|
+
let inSingle = false;
|
|
87
|
+
let inDouble = false;
|
|
88
|
+
while (i < sql.length) {
|
|
89
|
+
const ch = sql[i];
|
|
90
|
+
const next = sql[i + 1];
|
|
91
|
+
if (!inSingle && !inDouble) {
|
|
92
|
+
if (ch === "-" && next === "-") {
|
|
93
|
+
const end = sql.indexOf("\n", i);
|
|
94
|
+
i = end === -1 ? sql.length : end;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (ch === "/" && next === "*") {
|
|
98
|
+
const end = sql.indexOf("*/", i + 2);
|
|
99
|
+
i = end === -1 ? sql.length : end + 2;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (ch === "'")
|
|
103
|
+
inSingle = true;
|
|
104
|
+
else if (ch === '"')
|
|
105
|
+
inDouble = true;
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
if (inSingle) {
|
|
109
|
+
if (ch === "'") {
|
|
110
|
+
if (next === "'") {
|
|
111
|
+
out += "''";
|
|
112
|
+
i += 2;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
inSingle = false;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
else if (inDouble && ch === '"')
|
|
119
|
+
inDouble = false;
|
|
120
|
+
}
|
|
121
|
+
out += ch;
|
|
122
|
+
i += 1;
|
|
123
|
+
}
|
|
124
|
+
return out;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Split a SQL script into top-level statements on `;`, respecting string
|
|
128
|
+
* literals ('...', "...") and comments. Empty/whitespace-only parts are
|
|
129
|
+
* dropped. Returns [] for a blank script.
|
|
130
|
+
*/
|
|
131
|
+
function splitStatements(sql) {
|
|
132
|
+
const parts = [];
|
|
133
|
+
let current = "";
|
|
134
|
+
let inSingle = false;
|
|
135
|
+
let inDouble = false;
|
|
136
|
+
let i = 0;
|
|
137
|
+
while (i < sql.length) {
|
|
138
|
+
const ch = sql[i];
|
|
139
|
+
const next = sql[i + 1];
|
|
140
|
+
if (!inSingle && !inDouble) {
|
|
141
|
+
if (ch === "-" && next === "-") {
|
|
142
|
+
const end = sql.indexOf("\n", i);
|
|
143
|
+
const stop = end === -1 ? sql.length : end;
|
|
144
|
+
current += sql.slice(i, stop);
|
|
145
|
+
i = stop;
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (ch === "/" && next === "*") {
|
|
149
|
+
const end = sql.indexOf("*/", i + 2);
|
|
150
|
+
const stop = end === -1 ? sql.length : end + 2;
|
|
151
|
+
current += sql.slice(i, stop);
|
|
152
|
+
i = stop;
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (ch === "'") {
|
|
156
|
+
inSingle = true;
|
|
157
|
+
current += ch;
|
|
158
|
+
i += 1;
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
if (ch === '"') {
|
|
162
|
+
inDouble = true;
|
|
163
|
+
current += ch;
|
|
164
|
+
i += 1;
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
if (ch === ";") {
|
|
168
|
+
parts.push(current);
|
|
169
|
+
current = "";
|
|
170
|
+
i += 1;
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
current += ch;
|
|
174
|
+
i += 1;
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
// inside a literal
|
|
178
|
+
if (inSingle) {
|
|
179
|
+
if (ch === "'") {
|
|
180
|
+
if (next === "'") {
|
|
181
|
+
current += "''";
|
|
182
|
+
i += 2;
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
inSingle = false;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
else if (inDouble && ch === '"') {
|
|
189
|
+
inDouble = false;
|
|
190
|
+
}
|
|
191
|
+
current += ch;
|
|
192
|
+
i += 1;
|
|
193
|
+
}
|
|
194
|
+
parts.push(current);
|
|
195
|
+
return parts.map((p) => p.trim()).filter(Boolean);
|
|
196
|
+
}
|
|
197
|
+
function leadingToken(statement) {
|
|
198
|
+
const match = /^([A-Za-z_][A-Za-z0-9_]*)/.exec(statement);
|
|
199
|
+
return match?.[1]?.toUpperCase() ?? "";
|
|
200
|
+
}
|
|
201
|
+
/** Throw DB_MULTI_STATEMENT_REJECTED when >1 top-level statement is present. */
|
|
202
|
+
function assertSingleStatement(statements) {
|
|
203
|
+
if (statements.length > 1) {
|
|
204
|
+
throw new SqlToolError("DB_MULTI_STATEMENT_REJECTED", `sql_query accepts exactly one statement (got ${statements.length}); use sql_exec with a confirmation token for multi-statement writes`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
/** Lexical read-only gate for sql_query (statement must already be single). */
|
|
208
|
+
function assertReadOnlyStatement(statement) {
|
|
209
|
+
const bare = stripSqlComments(statement);
|
|
210
|
+
const token = leadingToken(bare);
|
|
211
|
+
if (READ_ONLY_LEADING_TOKENS.has(token)) {
|
|
212
|
+
// allowed root — now reject write-capable constructs inside it
|
|
213
|
+
}
|
|
214
|
+
else if (WRITE_LEADING_TOKENS.has(token)) {
|
|
215
|
+
throw new SqlToolError("DB_READ_ONLY_VIOLATION", `sql_query rejected: leading token ${token} is not read-only`);
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
throw new SqlToolError("DB_READ_ONLY_VIOLATION", `sql_query rejected: statement must start with SELECT/PRAGMA/EXPLAIN/SHOW/DESCRIBE/WITH (got ${token || "nothing"})`);
|
|
219
|
+
}
|
|
220
|
+
const folded = bare.toUpperCase();
|
|
221
|
+
if (/\bFOR\s+UPDATE\b/.test(folded) || /\bFOR\s+SHARE\b/.test(folded)) {
|
|
222
|
+
throw new SqlToolError("DB_READ_ONLY_VIOLATION", "sql_query rejected: FOR UPDATE / FOR SHARE locking clauses are not read-only");
|
|
223
|
+
}
|
|
224
|
+
if (/\bSELECT\b[^;]*\bINTO\b/.test(folded)) {
|
|
225
|
+
throw new SqlToolError("DB_READ_ONLY_VIOLATION", "sql_query rejected: SELECT ... INTO creates a table");
|
|
226
|
+
}
|
|
227
|
+
if (token === "WITH") {
|
|
228
|
+
// data-modifying CTE: WITH ... (INSERT|UPDATE|DELETE|MERGE|REPLACE)
|
|
229
|
+
if (/\)\s*(INSERT|UPDATE|DELETE|MERGE|REPLACE)\b/.test(folded) ||
|
|
230
|
+
/\bAS\s*\(\s*(INSERT|UPDATE|DELETE|MERGE|REPLACE)\b/.test(folded)) {
|
|
231
|
+
throw new SqlToolError("DB_READ_ONLY_VIOLATION", "sql_query rejected: data-modifying CTE (WITH ... INSERT/UPDATE/DELETE)");
|
|
232
|
+
}
|
|
233
|
+
if (/\b(INSERT|UPDATE|DELETE|MERGE|REPLACE)\b/.test(folded)) {
|
|
234
|
+
throw new SqlToolError("DB_READ_ONLY_VIOLATION", "sql_query rejected: data-modifying statement inside WITH");
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
if (token === "PRAGMA") {
|
|
238
|
+
// PRAGMA assignment forms: `PRAGMA x = y` or `PRAGMA x(y)`
|
|
239
|
+
if (/^PRAGMA\s+[A-Za-z0-9_.]+\s*=/.test(bare.trim().toUpperCase())) {
|
|
240
|
+
throw new SqlToolError("DB_READ_ONLY_VIOLATION", "sql_query rejected: PRAGMA assignment writes configuration");
|
|
241
|
+
}
|
|
242
|
+
if (/^PRAGMA\s+[A-Za-z0-9_.]+\s*\(/.test(bare.trim())) {
|
|
243
|
+
throw new SqlToolError("DB_READ_ONLY_VIOLATION", "sql_query rejected: PRAGMA call-form assignment writes configuration");
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
/** Identifier guard for interpolated table names (schema/stats). */
|
|
248
|
+
function assertSafeIdentifier(name) {
|
|
249
|
+
const trimmed = name.trim();
|
|
250
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(trimmed)) {
|
|
251
|
+
throw new SqlToolError("DB_INVALID_CONFIG", `invalid SQL identifier: ${JSON.stringify(trimmed)}`);
|
|
252
|
+
}
|
|
253
|
+
return trimmed;
|
|
254
|
+
}
|
|
255
|
+
function formatTable(columns, rows) {
|
|
256
|
+
const cell = (v) => (v === null || v === undefined ? "" : String(v));
|
|
257
|
+
const header = columns.join(" | ");
|
|
258
|
+
const divider = columns.map(() => "---").join(" | ");
|
|
259
|
+
const body = rows.map((r) => columns.map((c) => cell(r[c])).join(" | ")).join("\n");
|
|
260
|
+
return [header, divider, body].join("\n");
|
|
261
|
+
}
|
|
262
|
+
function toCsv(columns, rows) {
|
|
263
|
+
const esc = (v) => {
|
|
264
|
+
const s = v === null || v === undefined ? "" : String(v);
|
|
265
|
+
return /[",\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;
|
|
266
|
+
};
|
|
267
|
+
return [columns.join(","), ...rows.map((r) => columns.map((c) => esc(r[c])).join(","))].join("\n");
|
|
268
|
+
}
|
|
269
|
+
export async function buildSqlTools(options) {
|
|
270
|
+
const [{ Type }, { defineTool }] = await Promise.all([
|
|
271
|
+
import("typebox"),
|
|
272
|
+
import("@earendil-works/pi-coding-agent"),
|
|
273
|
+
]);
|
|
274
|
+
const registry = options.registry;
|
|
275
|
+
/** resolved confirmations: questionId → { sessionId, rejected } (one-shot). */
|
|
276
|
+
const resolvedConfirmations = new Map();
|
|
277
|
+
let unsubscribe;
|
|
278
|
+
if (options.questionRegistry) {
|
|
279
|
+
unsubscribe = options.questionRegistry.subscribe((change) => {
|
|
280
|
+
if (change.type !== "resolved")
|
|
281
|
+
return;
|
|
282
|
+
const question = change.question;
|
|
283
|
+
if (question.sessionId !== options.sessionId)
|
|
284
|
+
return;
|
|
285
|
+
const rejected = question.answer?.answers.some((a) => a.selected.some((label) => /reject|cancel|deny|拒绝|取消|否/i.test(label)));
|
|
286
|
+
for (const q of question.questions) {
|
|
287
|
+
resolvedConfirmations.set(q.id, {
|
|
288
|
+
sessionId: question.sessionId,
|
|
289
|
+
rejected: Boolean(rejected),
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
/** Validate and consume a one-shot confirmation token. */
|
|
295
|
+
function consumeConfirmationToken(token) {
|
|
296
|
+
if (!options.questionRegistry) {
|
|
297
|
+
throw new SqlToolError("DB_CONFIRMATION_REQUIRED", "sql_exec requires the ask_user_question confirmation gate, which is unavailable in this session");
|
|
298
|
+
}
|
|
299
|
+
const id = typeof token === "string" ? token.trim() : "";
|
|
300
|
+
if (!id) {
|
|
301
|
+
throw new SqlToolError("DB_CONFIRMATION_REQUIRED", "sql_exec requires confirmationToken: first call ask_user_question with the full SQL and Approve/Reject options, then pass its question id");
|
|
302
|
+
}
|
|
303
|
+
const entry = resolvedConfirmations.get(id);
|
|
304
|
+
if (!entry || entry.sessionId !== options.sessionId) {
|
|
305
|
+
throw new SqlToolError("DB_CONFIRMATION_REQUIRED", `confirmation token "${id}" was not approved in this session; ask the user via ask_user_question first`);
|
|
306
|
+
}
|
|
307
|
+
resolvedConfirmations.delete(id); // one-shot: burn even when rejected below
|
|
308
|
+
if (entry.rejected) {
|
|
309
|
+
throw new SqlToolError("DB_CONFIRMATION_REQUIRED", `the user rejected the confirmation for "${id}"`);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
async function envelope(operation, timeoutMs) {
|
|
313
|
+
let result;
|
|
314
|
+
try {
|
|
315
|
+
result = await withTimeout(operation(), timeoutMs);
|
|
316
|
+
}
|
|
317
|
+
catch (error) {
|
|
318
|
+
const code = error instanceof SqlToolError
|
|
319
|
+
? error.code
|
|
320
|
+
: error instanceof Error && "code" in error
|
|
321
|
+
? mapRegistryCode(String(error.code))
|
|
322
|
+
: "DB_ENGINE_ERROR";
|
|
323
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
324
|
+
result = { ok: false, error: { code, message } };
|
|
325
|
+
}
|
|
326
|
+
return {
|
|
327
|
+
content: [{ type: "text", text: JSON.stringify(result) }],
|
|
328
|
+
details: result,
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
function mapRegistryCode(code) {
|
|
332
|
+
switch (code) {
|
|
333
|
+
case "DB_CONNECTION_NOT_FOUND":
|
|
334
|
+
case "DB_ENV_MISSING":
|
|
335
|
+
case "DB_ENGINE_UNAVAILABLE":
|
|
336
|
+
case "DB_ENGINE_ERROR":
|
|
337
|
+
case "DB_INVALID_CONFIG":
|
|
338
|
+
return code;
|
|
339
|
+
default:
|
|
340
|
+
return "DB_ENGINE_ERROR";
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
async function withTimeout(promise, timeoutMs) {
|
|
344
|
+
let timer;
|
|
345
|
+
try {
|
|
346
|
+
return await Promise.race([
|
|
347
|
+
promise,
|
|
348
|
+
new Promise((_, reject) => {
|
|
349
|
+
timer = setTimeout(() => reject(new SqlToolError("DB_TIMEOUT", `operation exceeded ${timeoutMs}ms`)), timeoutMs);
|
|
350
|
+
}),
|
|
351
|
+
]);
|
|
352
|
+
}
|
|
353
|
+
finally {
|
|
354
|
+
if (timer)
|
|
355
|
+
clearTimeout(timer);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
const connectionParameter = Type.String({
|
|
359
|
+
description: "Named connection from sql_connections list.",
|
|
360
|
+
});
|
|
361
|
+
return [
|
|
362
|
+
defineTool({
|
|
363
|
+
name: "sql_query",
|
|
364
|
+
label: "SQL query (read-only)",
|
|
365
|
+
description: SQL_TOOLS_DESCRIPTION,
|
|
366
|
+
promptSnippet: SQL_TOOLS_DESCRIPTION,
|
|
367
|
+
parameters: Type.Object({
|
|
368
|
+
connection: connectionParameter,
|
|
369
|
+
sql: Type.String({ description: "Single read-only statement (SELECT/PRAGMA/EXPLAIN/SHOW/DESCRIBE/WITH)." }),
|
|
370
|
+
format: Type.Optional(Type.Union([Type.Literal("table"), Type.Literal("csv"), Type.Literal("json")])),
|
|
371
|
+
maxRows: Type.Optional(Type.Integer({ minimum: 1, maximum: SQL_MAX_ROWS_LIMIT })),
|
|
372
|
+
}, { additionalProperties: false }),
|
|
373
|
+
async execute(_toolCallId, params) {
|
|
374
|
+
return envelope(async () => {
|
|
375
|
+
const statements = splitStatements(params.sql ?? "");
|
|
376
|
+
if (statements.length === 0) {
|
|
377
|
+
throw new SqlToolError("DB_INVALID_CONFIG", "sql is empty");
|
|
378
|
+
}
|
|
379
|
+
assertSingleStatement(statements);
|
|
380
|
+
assertReadOnlyStatement(statements[0]);
|
|
381
|
+
const handle = await registry.connect(params.connection);
|
|
382
|
+
const limit = Math.min(Math.max(params.maxRows ?? SQL_MAX_ROWS_DEFAULT, 1), SQL_MAX_ROWS_LIMIT);
|
|
383
|
+
// sqlite exposes column metadata via prepare(); pg/mysql derive it
|
|
384
|
+
// from the first result row.
|
|
385
|
+
let columns;
|
|
386
|
+
let all;
|
|
387
|
+
if (isSqliteHandle(handle)) {
|
|
388
|
+
const stmt = handle.prepare(params.sql.trim());
|
|
389
|
+
columns = stmt.columns().map((c) => c.name);
|
|
390
|
+
all = stmt.all();
|
|
391
|
+
}
|
|
392
|
+
else {
|
|
393
|
+
all = await handle.query(params.sql.trim());
|
|
394
|
+
columns = all.length > 0 ? Object.keys(all[0]) : [];
|
|
395
|
+
}
|
|
396
|
+
const truncated = all.length > limit;
|
|
397
|
+
const rows = truncated ? all.slice(0, limit) : all;
|
|
398
|
+
const format = params.format ?? "json";
|
|
399
|
+
let text;
|
|
400
|
+
if (format === "table")
|
|
401
|
+
text = formatTable(columns, rows);
|
|
402
|
+
else if (format === "csv")
|
|
403
|
+
text = toCsv(columns, rows);
|
|
404
|
+
else
|
|
405
|
+
text = JSON.stringify(rows);
|
|
406
|
+
return {
|
|
407
|
+
ok: true,
|
|
408
|
+
connection: params.connection,
|
|
409
|
+
columns,
|
|
410
|
+
rows,
|
|
411
|
+
rowCount: rows.length,
|
|
412
|
+
truncated,
|
|
413
|
+
...(text !== undefined ? { text } : {}),
|
|
414
|
+
};
|
|
415
|
+
}, SQL_QUERY_TIMEOUT_MS);
|
|
416
|
+
},
|
|
417
|
+
}),
|
|
418
|
+
defineTool({
|
|
419
|
+
name: "sql_exec",
|
|
420
|
+
label: "SQL exec (disabled: read-only policy)",
|
|
421
|
+
description: SQL_TOOLS_DESCRIPTION,
|
|
422
|
+
promptSnippet: SQL_TOOLS_DESCRIPTION,
|
|
423
|
+
parameters: Type.Object({
|
|
424
|
+
connection: connectionParameter,
|
|
425
|
+
sql: Type.String({ description: "Write/DDL script; multiple statements separated by ';'." }),
|
|
426
|
+
confirmationToken: Type.String({
|
|
427
|
+
description: "Question id of the APPROVED ask_user_question confirmation (single use).",
|
|
428
|
+
}),
|
|
429
|
+
}, { additionalProperties: false }),
|
|
430
|
+
async execute(_toolCallId, params) {
|
|
431
|
+
return envelope(async () => {
|
|
432
|
+
void params;
|
|
433
|
+
throw new SqlToolError("DB_READ_ONLY_VIOLATION", "sql_exec is disabled: database connections are permanently read-only; DELETE, DROP, and all other mutations are forbidden");
|
|
434
|
+
}, SQL_EXEC_TIMEOUT_MS);
|
|
435
|
+
},
|
|
436
|
+
}),
|
|
437
|
+
defineTool({
|
|
438
|
+
name: "sql_schema",
|
|
439
|
+
label: "SQL schema",
|
|
440
|
+
description: SQL_TOOLS_DESCRIPTION,
|
|
441
|
+
promptSnippet: SQL_TOOLS_DESCRIPTION,
|
|
442
|
+
parameters: Type.Object({
|
|
443
|
+
connection: connectionParameter,
|
|
444
|
+
table: Type.Optional(Type.String({ description: "Table name to describe; omit to list all tables/views." })),
|
|
445
|
+
}, { additionalProperties: false }),
|
|
446
|
+
async execute(_toolCallId, params) {
|
|
447
|
+
return envelope(async () => {
|
|
448
|
+
const handle = await registry.connect(params.connection);
|
|
449
|
+
if (params.table === undefined || params.table.trim() === "") {
|
|
450
|
+
const rows = (await handle.query("SELECT name, type FROM sqlite_master WHERE type IN ('table','view') ORDER BY name"));
|
|
451
|
+
return { ok: true, connection: params.connection, tables: rows };
|
|
452
|
+
}
|
|
453
|
+
const table = assertSafeIdentifier(params.table);
|
|
454
|
+
const rows = await handle.query(`PRAGMA table_info(${table})`);
|
|
455
|
+
return {
|
|
456
|
+
ok: true,
|
|
457
|
+
connection: params.connection,
|
|
458
|
+
table,
|
|
459
|
+
columns: rows.map((r) => ({
|
|
460
|
+
name: String(r.name),
|
|
461
|
+
type: r.type === null || r.type === undefined ? "" : String(r.type),
|
|
462
|
+
notNull: Number(r.notnull ?? r.notNull ?? 0) === 1,
|
|
463
|
+
pk: Number(r.pk ?? 0) > 0,
|
|
464
|
+
})),
|
|
465
|
+
};
|
|
466
|
+
}, SQL_QUERY_TIMEOUT_MS);
|
|
467
|
+
},
|
|
468
|
+
}),
|
|
469
|
+
defineTool({
|
|
470
|
+
name: "sql_stats",
|
|
471
|
+
label: "SQL stats",
|
|
472
|
+
description: SQL_TOOLS_DESCRIPTION,
|
|
473
|
+
promptSnippet: SQL_TOOLS_DESCRIPTION,
|
|
474
|
+
parameters: Type.Object({ connection: connectionParameter }, { additionalProperties: false }),
|
|
475
|
+
async execute(_toolCallId, params) {
|
|
476
|
+
return envelope(async () => {
|
|
477
|
+
const handle = await registry.connect(params.connection);
|
|
478
|
+
const tables = (await handle.query("SELECT name FROM sqlite_master WHERE type IN ('table','view') ORDER BY name"));
|
|
479
|
+
const rowsByTable = {};
|
|
480
|
+
for (const t of tables) {
|
|
481
|
+
const name = assertSafeIdentifier(t.name);
|
|
482
|
+
const countRows = await handle.query(`SELECT COUNT(*) AS n FROM ${name}`);
|
|
483
|
+
rowsByTable[name] = Number(countRows[0]?.n ?? 0);
|
|
484
|
+
}
|
|
485
|
+
const config = registry.get(params.connection);
|
|
486
|
+
let sizeBytes;
|
|
487
|
+
if (config?.path) {
|
|
488
|
+
try {
|
|
489
|
+
sizeBytes = (await stat(path.resolve(config.path))).size;
|
|
490
|
+
}
|
|
491
|
+
catch {
|
|
492
|
+
sizeBytes = undefined;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
return {
|
|
496
|
+
ok: true,
|
|
497
|
+
connection: params.connection,
|
|
498
|
+
tableCount: tables.length,
|
|
499
|
+
tables: rowsByTable,
|
|
500
|
+
...(sizeBytes !== undefined ? { sizeBytes } : {}),
|
|
501
|
+
};
|
|
502
|
+
}, SQL_EXEC_TIMEOUT_MS);
|
|
503
|
+
},
|
|
504
|
+
}),
|
|
505
|
+
defineTool({
|
|
506
|
+
name: "sql_health",
|
|
507
|
+
label: "SQL health",
|
|
508
|
+
description: SQL_TOOLS_DESCRIPTION,
|
|
509
|
+
promptSnippet: SQL_TOOLS_DESCRIPTION,
|
|
510
|
+
parameters: Type.Object({}, { additionalProperties: false }),
|
|
511
|
+
async execute() {
|
|
512
|
+
return envelope(async () => {
|
|
513
|
+
const results = [];
|
|
514
|
+
for (const summary of registry.list()) {
|
|
515
|
+
try {
|
|
516
|
+
const probe = await registry.test(summary.name);
|
|
517
|
+
results.push({
|
|
518
|
+
name: summary.name,
|
|
519
|
+
engine: summary.engine,
|
|
520
|
+
ok: true,
|
|
521
|
+
elapsedMs: probe.elapsedMs,
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
catch (error) {
|
|
525
|
+
results.push({
|
|
526
|
+
name: summary.name,
|
|
527
|
+
engine: summary.engine,
|
|
528
|
+
ok: false,
|
|
529
|
+
code: mapRegistryCode(error instanceof Error && "code" in error
|
|
530
|
+
? String(error.code)
|
|
531
|
+
: ""),
|
|
532
|
+
message: error instanceof Error ? error.message : String(error),
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
return {
|
|
537
|
+
ok: true,
|
|
538
|
+
connections: results,
|
|
539
|
+
readOnlyMode: true,
|
|
540
|
+
writeGate: "ask_user_question",
|
|
541
|
+
maxRowsDefault: SQL_MAX_ROWS_DEFAULT,
|
|
542
|
+
maxRowsLimit: SQL_MAX_ROWS_LIMIT,
|
|
543
|
+
queryTimeoutMs: SQL_QUERY_TIMEOUT_MS,
|
|
544
|
+
execTimeoutMs: SQL_EXEC_TIMEOUT_MS,
|
|
545
|
+
};
|
|
546
|
+
}, SQL_EXEC_TIMEOUT_MS);
|
|
547
|
+
},
|
|
548
|
+
}),
|
|
549
|
+
defineTool({
|
|
550
|
+
name: "sql_connections",
|
|
551
|
+
label: "SQL connections",
|
|
552
|
+
description: SQL_TOOLS_DESCRIPTION,
|
|
553
|
+
promptSnippet: SQL_TOOLS_DESCRIPTION,
|
|
554
|
+
parameters: Type.Object({
|
|
555
|
+
action: Type.Union([
|
|
556
|
+
Type.Literal("list"),
|
|
557
|
+
Type.Literal("add"),
|
|
558
|
+
Type.Literal("remove"),
|
|
559
|
+
Type.Literal("test"),
|
|
560
|
+
]),
|
|
561
|
+
name: Type.Optional(Type.String()),
|
|
562
|
+
engine: Type.Optional(Type.Union([
|
|
563
|
+
Type.Literal("sqlite"),
|
|
564
|
+
Type.Literal("postgres"),
|
|
565
|
+
Type.Literal("mysql"),
|
|
566
|
+
])),
|
|
567
|
+
path: Type.Optional(Type.String({ description: "sqlite only: ABSOLUTE database file path." })),
|
|
568
|
+
mode: Type.Optional(Type.Union([Type.Literal("parameters"), Type.Literal("url-env")])),
|
|
569
|
+
urlEnv: Type.Optional(Type.String({ description: "advanced postgres/mysql mode: env var NAME holding the full DSN." })),
|
|
570
|
+
host: Type.Optional(Type.String({ description: "postgres/mysql host name or IP address." })),
|
|
571
|
+
port: Type.Optional(Type.Number({ description: "optional port; defaults to 5432 for PostgreSQL or 3306 for MySQL." })),
|
|
572
|
+
database: Type.Optional(Type.String({ description: "postgres/mysql database name." })),
|
|
573
|
+
username: Type.Optional(Type.String({ description: "postgres/mysql user name." })),
|
|
574
|
+
passwordEnv: Type.Optional(Type.String({ description: "env var NAME holding the password; never ask for a password in chat." })),
|
|
575
|
+
ssl: Type.Optional(Type.Boolean({ description: "enable a TLS/SSL connection." })),
|
|
576
|
+
}, { additionalProperties: false }),
|
|
577
|
+
async execute(_toolCallId, params) {
|
|
578
|
+
return envelope(async () => {
|
|
579
|
+
if (params.action === "list") {
|
|
580
|
+
return { ok: true, connections: registry.list() };
|
|
581
|
+
}
|
|
582
|
+
if (params.action === "add") {
|
|
583
|
+
if (!params.name || !params.engine) {
|
|
584
|
+
throw new SqlToolError("DB_INVALID_CONFIG", "action=add requires name and engine");
|
|
585
|
+
}
|
|
586
|
+
const summary = await registry.add({
|
|
587
|
+
name: params.name,
|
|
588
|
+
engine: params.engine,
|
|
589
|
+
...(params.path ? { path: params.path } : {}),
|
|
590
|
+
...(params.mode ? { mode: params.mode } : {}),
|
|
591
|
+
...(params.urlEnv ? { urlEnv: params.urlEnv } : {}),
|
|
592
|
+
...(params.host ? { host: params.host } : {}),
|
|
593
|
+
...(params.port !== undefined ? { port: params.port } : {}),
|
|
594
|
+
...(params.database ? { database: params.database } : {}),
|
|
595
|
+
...(params.username ? { username: params.username } : {}),
|
|
596
|
+
...(params.passwordEnv ? { passwordMode: "environment", passwordEnv: params.passwordEnv } : {}),
|
|
597
|
+
...(params.ssl !== undefined ? { ssl: params.ssl } : {}),
|
|
598
|
+
});
|
|
599
|
+
return {
|
|
600
|
+
ok: true,
|
|
601
|
+
added: { name: summary.name, engine: summary.engine },
|
|
602
|
+
note: "connection registered; run action=\"test\" before first real use",
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
if (params.action === "remove") {
|
|
606
|
+
if (!params.name) {
|
|
607
|
+
throw new SqlToolError("DB_INVALID_CONFIG", "action=remove requires name");
|
|
608
|
+
}
|
|
609
|
+
await registry.remove(params.name);
|
|
610
|
+
return { ok: true, removed: params.name };
|
|
611
|
+
}
|
|
612
|
+
// action === "test"
|
|
613
|
+
if (!params.name) {
|
|
614
|
+
throw new SqlToolError("DB_INVALID_CONFIG", "action=test requires name");
|
|
615
|
+
}
|
|
616
|
+
const probe = await registry.test(params.name);
|
|
617
|
+
return {
|
|
618
|
+
ok: true,
|
|
619
|
+
name: probe.name,
|
|
620
|
+
engine: probe.engine,
|
|
621
|
+
elapsedMs: probe.elapsedMs,
|
|
622
|
+
};
|
|
623
|
+
}, SQL_QUERY_TIMEOUT_MS);
|
|
624
|
+
},
|
|
625
|
+
}),
|
|
626
|
+
];
|
|
627
|
+
}
|