@niuhuoshan/dsh-connect 0.1.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.
Files changed (51) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +380 -0
  3. package/cordis.patch.yml +3 -0
  4. package/lib/client.js +1489 -0
  5. package/lib/index.js +2303 -0
  6. package/lib/index.js.map +7 -0
  7. package/lib/types/client/ConnectSettings.d.ts +9 -0
  8. package/lib/types/client/ConnectSettings.d.ts.map +1 -0
  9. package/lib/types/client/index.d.ts +5 -0
  10. package/lib/types/client/index.d.ts.map +1 -0
  11. package/lib/types/client/rpc.d.ts +7 -0
  12. package/lib/types/client/rpc.d.ts.map +1 -0
  13. package/lib/types/client/styles.d.ts +3 -0
  14. package/lib/types/client/styles.d.ts.map +1 -0
  15. package/lib/types/domain.d.ts +156 -0
  16. package/lib/types/domain.d.ts.map +1 -0
  17. package/lib/types/host/datasource/clickhouse.d.ts +8 -0
  18. package/lib/types/host/datasource/clickhouse.d.ts.map +1 -0
  19. package/lib/types/host/datasource/index.d.ts +4 -0
  20. package/lib/types/host/datasource/index.d.ts.map +1 -0
  21. package/lib/types/host/datasource/mysql.d.ts +8 -0
  22. package/lib/types/host/datasource/mysql.d.ts.map +1 -0
  23. package/lib/types/host/datasource/postgresql.d.ts +8 -0
  24. package/lib/types/host/datasource/postgresql.d.ts.map +1 -0
  25. package/lib/types/host/datasource/provider.d.ts +24 -0
  26. package/lib/types/host/datasource/provider.d.ts.map +1 -0
  27. package/lib/types/host/http/conversation-api.d.ts +57 -0
  28. package/lib/types/host/http/conversation-api.d.ts.map +1 -0
  29. package/lib/types/host/http/executor.d.ts +10 -0
  30. package/lib/types/host/http/executor.d.ts.map +1 -0
  31. package/lib/types/host/http/security.d.ts +5 -0
  32. package/lib/types/host/http/security.d.ts.map +1 -0
  33. package/lib/types/host/http/tools.d.ts +13 -0
  34. package/lib/types/host/http/tools.d.ts.map +1 -0
  35. package/lib/types/host/metadata/enricher.d.ts +35 -0
  36. package/lib/types/host/metadata/enricher.d.ts.map +1 -0
  37. package/lib/types/host/metadata/governance.d.ts +4 -0
  38. package/lib/types/host/metadata/governance.d.ts.map +1 -0
  39. package/lib/types/host/metadata/profiler.d.ts +22 -0
  40. package/lib/types/host/metadata/profiler.d.ts.map +1 -0
  41. package/lib/types/host/metadata/tools.d.ts +4 -0
  42. package/lib/types/host/metadata/tools.d.ts.map +1 -0
  43. package/lib/types/host/rpc.d.ts +29 -0
  44. package/lib/types/host/rpc.d.ts.map +1 -0
  45. package/lib/types/host/store.d.ts +47 -0
  46. package/lib/types/host/store.d.ts.map +1 -0
  47. package/lib/types/index.d.ts +6 -0
  48. package/lib/types/index.d.ts.map +1 -0
  49. package/lib/types/types.d.ts +395 -0
  50. package/lib/types/types.d.ts.map +1 -0
  51. package/package.json +121 -0
package/lib/index.js ADDED
@@ -0,0 +1,2303 @@
1
+ // src/index.ts
2
+ import { settingsNamespace } from "@deepseek-ai/dsh-settings";
3
+ import z5 from "@deepseek-ai/schemastery";
4
+
5
+ // src/domain.ts
6
+ import { defineDomain, domainTable } from "@deepseek-ai/dsh-storage-domain";
7
+
8
+ // src/types.ts
9
+ import { z } from "zod";
10
+ var databaseTypeSchema = z.enum(["mysql", "postgresql", "clickhouse"]);
11
+ var apiMethodSchema = z.enum(["GET", "POST", "PUT", "PATCH", "DELETE"]);
12
+ var apiParameterSchema = z.object({
13
+ name: z.string().min(1).max(64).regex(/^[A-Za-z_][A-Za-z0-9_-]*$/),
14
+ location: z.enum(["path", "query", "header", "body"]),
15
+ type: z.enum(["string", "number", "integer", "boolean", "json"]),
16
+ description: z.string().max(500).default(""),
17
+ required: z.boolean().default(false)
18
+ });
19
+ var apiAuthSchema = z.discriminatedUnion("type", [
20
+ z.object({ type: z.literal("none") }),
21
+ z.object({ type: z.literal("bearer"), credentialRef: z.string().min(1) }),
22
+ z.object({
23
+ type: z.literal("api-key"),
24
+ credentialRef: z.string().min(1),
25
+ location: z.enum(["header", "query"]),
26
+ name: z.string().min(1).max(100)
27
+ }),
28
+ z.object({ type: z.literal("basic"), credentialRef: z.string().min(1), username: z.string().min(1).max(200) })
29
+ ]);
30
+ var timestampSchema = z.number().int().nonnegative();
31
+ var dataSourceSchema = z.object({
32
+ id: z.string().uuid(),
33
+ name: z.string().min(1).max(100),
34
+ type: databaseTypeSchema,
35
+ host: z.string().min(1).max(255),
36
+ port: z.number().int().min(1).max(65535),
37
+ database: z.string().min(1).max(255),
38
+ username: z.string().min(1).max(255),
39
+ credentialRef: z.string().min(1),
40
+ schemaInclude: z.array(z.string().min(1).max(255)).max(100).default([]),
41
+ tls: z.boolean().default(false),
42
+ sampleRows: z.number().int().min(0).max(3).default(0),
43
+ aiEnrichment: z.boolean().default(false),
44
+ enabled: z.boolean().default(true),
45
+ createdAt: timestampSchema,
46
+ updatedAt: timestampSchema
47
+ });
48
+ var apiDefinitionSchema = z.object({
49
+ id: z.string().uuid(),
50
+ name: z.string().min(1).max(100),
51
+ slug: z.string().min(2).max(48).regex(/^[a-z][a-z0-9_]*$/),
52
+ description: z.string().min(1).max(1e3),
53
+ method: apiMethodSchema,
54
+ baseUrl: z.string().url().max(2048),
55
+ pathTemplate: z.string().min(1).max(2048).default("/"),
56
+ parameters: z.array(apiParameterSchema).max(50).default([]),
57
+ auth: apiAuthSchema,
58
+ timeoutMs: z.number().int().min(1e3).max(12e4).default(3e4),
59
+ maxResponseBytes: z.number().int().min(1024).max(2 * 1024 * 1024).default(131072),
60
+ responsePointer: z.string().max(500).default(""),
61
+ allowPrivateNetwork: z.boolean().default(false),
62
+ enabled: z.boolean().default(true),
63
+ createdAt: timestampSchema,
64
+ updatedAt: timestampSchema
65
+ });
66
+ var physicalColumnSchema = z.object({
67
+ name: z.string(),
68
+ type: z.string(),
69
+ nullable: z.boolean(),
70
+ defaultValue: z.string().nullable().optional(),
71
+ comment: z.string().optional(),
72
+ primaryKey: z.boolean().default(false),
73
+ references: z.object({
74
+ schemaName: z.string(),
75
+ objectName: z.string(),
76
+ columnName: z.string()
77
+ }).optional()
78
+ });
79
+ var semanticColumnSchema = z.object({
80
+ name: z.string(),
81
+ term: z.string(),
82
+ description: z.string(),
83
+ synonyms: z.array(z.string().max(100)).max(20).default([]),
84
+ enums: z.array(z.string().max(100)).max(50).default([]),
85
+ role: z.enum(["identifier", "dimension", "measure", "time", "unknown"]).default("unknown")
86
+ });
87
+ var metadataProfileSchema = z.object({
88
+ id: z.string(),
89
+ sourceId: z.string().uuid(),
90
+ schemaName: z.string(),
91
+ objectName: z.string(),
92
+ objectType: z.enum(["table", "view"]),
93
+ engine: z.string().optional(),
94
+ comment: z.string().optional(),
95
+ ddl: z.string().max(1e5),
96
+ columns: z.array(physicalColumnSchema),
97
+ sample: z.array(z.record(z.string(), z.unknown())).max(3).default([]),
98
+ term: z.string().default(""),
99
+ description: z.string().default(""),
100
+ tags: z.array(z.string()).max(20).default([]),
101
+ synonyms: z.array(z.string().max(100)).max(30).default([]),
102
+ semanticColumns: z.array(semanticColumnSchema).default([]),
103
+ confidence: z.number().int().min(0).max(100).default(0),
104
+ confidenceReason: z.string().default(""),
105
+ temporary: z.boolean().default(false),
106
+ ignored: z.boolean().default(false),
107
+ ignoredOverride: z.boolean().optional(),
108
+ editedAt: timestampSchema.optional(),
109
+ fingerprint: z.string(),
110
+ semanticFingerprint: z.string().default(""),
111
+ modelProvider: z.string().optional(),
112
+ modelName: z.string().optional(),
113
+ modelReasoningEffort: z.string().optional(),
114
+ modelAnalyzedAt: timestampSchema.optional(),
115
+ modelStatus: z.enum(["heuristic", "ai", "failed"]).default("heuristic"),
116
+ modelPromptVersion: z.string().default("v1"),
117
+ profiledAt: timestampSchema
118
+ });
119
+ var metricAggregationSchema = z.enum(["count", "sum", "avg", "min", "max", "formula"]);
120
+ var metricDefinitionSchema = z.object({
121
+ id: z.string().uuid(),
122
+ sourceId: z.string().uuid(),
123
+ profileId: z.string().min(1),
124
+ name: z.string().min(1).max(120),
125
+ term: z.string().min(1).max(120),
126
+ description: z.string().max(500).default(""),
127
+ aggregation: metricAggregationSchema,
128
+ columnName: z.string().max(255).default(""),
129
+ expression: z.string().max(2e3),
130
+ unit: z.string().max(50).default(""),
131
+ tags: z.array(z.string().max(50)).max(20).default([]),
132
+ enabled: z.boolean().default(true),
133
+ createdAt: timestampSchema,
134
+ updatedAt: timestampSchema
135
+ });
136
+ var metadataChangeSchema = z.object({
137
+ id: z.string().uuid(),
138
+ sourceId: z.string().uuid(),
139
+ profileId: z.string().min(1),
140
+ action: z.enum(["create", "update", "scan", "model"]),
141
+ summary: z.string().max(500),
142
+ before: z.record(z.string(), z.unknown()).optional(),
143
+ after: z.record(z.string(), z.unknown()).optional(),
144
+ changedAt: timestampSchema
145
+ });
146
+ var profileJobSchema = z.object({
147
+ id: z.string().uuid(),
148
+ sourceId: z.string().uuid(),
149
+ status: z.enum(["queued", "running", "completed", "cancelled", "failed"]),
150
+ mode: z.enum(["incremental", "rebuild-ai", "full"]).default("incremental"),
151
+ total: z.number().int().nonnegative(),
152
+ processed: z.number().int().nonnegative(),
153
+ currentObject: z.string().optional(),
154
+ error: z.string().optional(),
155
+ startedAt: timestampSchema,
156
+ updatedAt: timestampSchema,
157
+ finishedAt: timestampSchema.optional(),
158
+ modelProvider: z.string().optional(),
159
+ modelName: z.string().optional()
160
+ });
161
+
162
+ // src/domain.ts
163
+ var connectDomainSpec = defineDomain({
164
+ name: "dsh_connect",
165
+ version: 0,
166
+ tables: {
167
+ data_sources: domainTable(dataSourceSchema),
168
+ api_definitions: domainTable(apiDefinitionSchema),
169
+ metadata_profiles: domainTable(metadataProfileSchema),
170
+ profile_jobs: domainTable(profileJobSchema),
171
+ metrics: domainTable(metricDefinitionSchema),
172
+ metadata_changes: domainTable(metadataChangeSchema)
173
+ }
174
+ });
175
+
176
+ // src/host/store.ts
177
+ var ConnectStore = class {
178
+ constructor(ctx, domain) {
179
+ this.ctx = ctx;
180
+ this.domain = domain;
181
+ }
182
+ dataSources() {
183
+ return [...this.domain.table("data_sources").entries()].map(([, value]) => value).sort((a, b) => a.name.localeCompare(b.name));
184
+ }
185
+ dataSource(id) {
186
+ return this.domain.table("data_sources").get(id);
187
+ }
188
+ async dataSourceViews() {
189
+ const jobs = this.jobs();
190
+ const profiles = this.profiles();
191
+ return Promise.all(this.dataSources().map(async (source) => {
192
+ const { credentialRef: credentialRef5, ...safe } = source;
193
+ const latestJob = jobs.filter((job) => job.sourceId === source.id && job.status !== "cancelled").sort((a, b) => b.startedAt - a.startedAt)[0];
194
+ return {
195
+ ...safe,
196
+ credentialConfigured: (await this.ctx.credentials.describe(credentialRef5)).configured,
197
+ ...latestJob === void 0 ? {} : { latestJob },
198
+ profileCount: profiles.filter((profile) => profile.sourceId === source.id).length,
199
+ aiProfileCount: profiles.filter((profile) => profile.sourceId === source.id && profile.modelStatus === "ai").length
200
+ };
201
+ }));
202
+ }
203
+ putDataSource(value) {
204
+ return this.domain.table("data_sources").put(value.id, value);
205
+ }
206
+ async deleteDataSource(id) {
207
+ await this.domain.table("data_sources").delete(id);
208
+ const profileTable = this.domain.table("metadata_profiles");
209
+ for (const [key, profile] of profileTable.entries()) {
210
+ if (profile.sourceId === id) await profileTable.delete(key);
211
+ }
212
+ const jobTable = this.domain.table("profile_jobs");
213
+ for (const [key, job] of jobTable.entries()) {
214
+ if (job.sourceId === id) await jobTable.delete(key);
215
+ }
216
+ for (const tableName of ["metrics", "metadata_changes"]) {
217
+ const table = this.domain.table(tableName);
218
+ for (const [key, value] of table.entries()) if (value.sourceId === id) await table.delete(key);
219
+ }
220
+ }
221
+ apiDefinitions() {
222
+ return [...this.domain.table("api_definitions").entries()].map(([, value]) => value).sort((a, b) => a.name.localeCompare(b.name));
223
+ }
224
+ apiDefinition(id) {
225
+ return this.domain.table("api_definitions").get(id);
226
+ }
227
+ async apiDefinitionViews() {
228
+ return Promise.all(this.apiDefinitions().map(async (definition) => {
229
+ const { auth, ...safe } = definition;
230
+ if (auth.type === "none") {
231
+ return { ...safe, auth: { type: "none", credentialConfigured: true }, toolName: toolName(definition) };
232
+ }
233
+ const { credentialRef: credentialRef5, ...safeAuth } = auth;
234
+ return {
235
+ ...safe,
236
+ auth: {
237
+ ...safeAuth,
238
+ credentialConfigured: (await this.ctx.credentials.describe(credentialRef5)).configured
239
+ },
240
+ toolName: toolName(definition)
241
+ };
242
+ }));
243
+ }
244
+ putApiDefinition(value) {
245
+ return this.domain.table("api_definitions").put(value.id, value);
246
+ }
247
+ deleteApiDefinition(id) {
248
+ return this.domain.table("api_definitions").delete(id);
249
+ }
250
+ profiles(sourceId) {
251
+ return [...this.domain.table("metadata_profiles").entries()].map(([, value]) => value).filter((profile) => sourceId === void 0 || profile.sourceId === sourceId).sort((a, b) => `${a.schemaName}.${a.objectName}`.localeCompare(`${b.schemaName}.${b.objectName}`));
252
+ }
253
+ profile(id) {
254
+ return this.domain.table("metadata_profiles").get(id);
255
+ }
256
+ putProfile(value) {
257
+ return this.domain.table("metadata_profiles").put(value.id, value);
258
+ }
259
+ metrics(sourceId) {
260
+ return [...this.domain.table("metrics").entries()].map(([, value]) => value).filter((metric) => sourceId === void 0 || metric.sourceId === sourceId).sort((a, b) => a.term.localeCompare(b.term));
261
+ }
262
+ metric(id) {
263
+ return this.domain.table("metrics").get(id);
264
+ }
265
+ putMetric(value) {
266
+ return this.domain.table("metrics").put(value.id, value);
267
+ }
268
+ deleteMetric(id) {
269
+ return this.domain.table("metrics").delete(id);
270
+ }
271
+ changes(sourceId, profileId2) {
272
+ return [...this.domain.table("metadata_changes").entries()].map(([, value]) => value).filter((change) => (sourceId === void 0 || change.sourceId === sourceId) && (profileId2 === void 0 || change.profileId === profileId2)).sort((a, b) => b.changedAt - a.changedAt);
273
+ }
274
+ changePage(sourceId, profileId2, offset, limit) {
275
+ const changes = this.changes(sourceId, profileId2);
276
+ return {
277
+ items: changes.slice(offset, offset + limit).map(({ id, sourceId: itemSourceId, profileId: itemProfileId, action, summary, changedAt }) => ({
278
+ id,
279
+ sourceId: itemSourceId,
280
+ profileId: itemProfileId,
281
+ action,
282
+ summary,
283
+ changedAt
284
+ })),
285
+ offset,
286
+ limit,
287
+ total: changes.length,
288
+ hasMore: offset + limit < changes.length
289
+ };
290
+ }
291
+ putChange(value) {
292
+ return this.domain.table("metadata_changes").put(value.id, value);
293
+ }
294
+ async deleteStaleProfiles(sourceId, keep) {
295
+ const table = this.domain.table("metadata_profiles");
296
+ for (const [key, value] of table.entries()) {
297
+ if (value.sourceId === sourceId && !keep.has(key)) await table.delete(key);
298
+ }
299
+ }
300
+ jobs(sourceId) {
301
+ return [...this.domain.table("profile_jobs").entries()].map(([, value]) => value).filter((job) => job.status !== "cancelled" && (sourceId === void 0 || job.sourceId === sourceId)).sort((a, b) => b.startedAt - a.startedAt);
302
+ }
303
+ putJob(value) {
304
+ return this.domain.table("profile_jobs").put(value.id, value);
305
+ }
306
+ deleteJob(id) {
307
+ return this.domain.table("profile_jobs").delete(id);
308
+ }
309
+ };
310
+ function toolName(definition) {
311
+ return `dsh_connect_api_${definition.slug}`;
312
+ }
313
+
314
+ // src/host/http/executor.ts
315
+ import { credentialRef } from "@deepseek-ai/dsh-credentials";
316
+
317
+ // src/host/http/security.ts
318
+ import { lookup } from "node:dns/promises";
319
+ import { isIP } from "node:net";
320
+ var FORBIDDEN_HEADERS = /* @__PURE__ */ new Set([
321
+ "authorization",
322
+ "proxy-authorization",
323
+ "host",
324
+ "content-length",
325
+ "connection",
326
+ "transfer-encoding",
327
+ "cookie"
328
+ ]);
329
+ function assertSafeHeaderName(name2) {
330
+ const normalized = name2.trim().toLowerCase();
331
+ if (!/^[a-z0-9!#$%&'*+.^_`|~-]+$/.test(normalized)) {
332
+ throw new Error(`Invalid HTTP header name: ${name2}`);
333
+ }
334
+ if (FORBIDDEN_HEADERS.has(normalized)) {
335
+ throw new Error(`HTTP header ${name2} is controlled by dsh-connect`);
336
+ }
337
+ }
338
+ function parseConfiguredBaseUrl(raw) {
339
+ const url = new URL(raw);
340
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
341
+ throw new Error("Only HTTP and HTTPS API targets are supported");
342
+ }
343
+ if (url.username !== "" || url.password !== "") {
344
+ throw new Error("Credentials must not be embedded in the API URL");
345
+ }
346
+ if (url.hash !== "") throw new Error("API base URL must not contain a fragment");
347
+ return url;
348
+ }
349
+ async function assertOutboundTarget(url, allowPrivateNetwork) {
350
+ if (allowPrivateNetwork) return;
351
+ const addresses = isIP(url.hostname) === 0 ? await lookup(url.hostname, { all: true, verbatim: true }) : [{ address: url.hostname, family: isIP(url.hostname) }];
352
+ if (addresses.length === 0) throw new Error("API target did not resolve to an address");
353
+ for (const { address } of addresses) {
354
+ if (isPrivateAddress(address)) {
355
+ throw new Error("API target resolves to a private, loopback, link-local, or reserved address");
356
+ }
357
+ }
358
+ }
359
+ function isPrivateAddress(raw) {
360
+ const lower = raw.toLowerCase();
361
+ const unbracketed = lower.startsWith("[") && lower.endsWith("]") ? lower.slice(1, -1) : lower;
362
+ const address = unbracketed.split("%", 1)[0] ?? unbracketed;
363
+ if (address.includes(":")) {
364
+ if (isIP(address) !== 6) return true;
365
+ if (address === "::" || address === "::1") return true;
366
+ if (address.startsWith("fc") || address.startsWith("fd") || address.startsWith("fe8") || address.startsWith("fe9") || address.startsWith("fea") || address.startsWith("feb")) return true;
367
+ if (address.startsWith("::ffff:")) return isPrivateAddress(address.slice("::ffff:".length));
368
+ return address.startsWith("ff") || address.startsWith("2001:db8:") || address.startsWith("2001:0db8:") || address.startsWith("2001:10:") || address.startsWith("2001:20:") || address.startsWith("2002:");
369
+ }
370
+ const parts = address.split(".").map(Number);
371
+ if (parts.length !== 4 || parts.some((value) => !Number.isInteger(value) || value < 0 || value > 255)) return true;
372
+ const [a, b, c] = parts;
373
+ return a === 0 || a === 10 || a === 127 || a === 169 && b === 254 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 168 || a === 192 && b === 0 && (c === 0 || c === 2) || a === 192 && b === 88 && c === 99 || a === 198 && (b === 18 || b === 19) || a === 198 && b === 51 && c === 100 || a === 203 && b === 0 && c === 113 || a === 100 && b >= 64 && b <= 127 || a >= 224;
374
+ }
375
+
376
+ // src/host/http/executor.ts
377
+ var BLOCKED_PARAMETER_HEADERS = /* @__PURE__ */ new Set(["authorization", "proxy-authorization"]);
378
+ var SecureHttpExecutor = class {
379
+ constructor(ctx) {
380
+ this.ctx = ctx;
381
+ }
382
+ async execute(definition, rawArgs, signal) {
383
+ const args = validateArguments(definition.parameters, rawArgs);
384
+ const base = parseConfiguredBaseUrl(definition.baseUrl);
385
+ const path = replacePathParameters(definition.pathTemplate, definition.parameters, args);
386
+ const target = new URL(path, base);
387
+ if (target.origin !== base.origin) throw new Error("API path must stay on the configured origin");
388
+ await assertOutboundTarget(target, definition.allowPrivateNetwork);
389
+ const headers = new Headers({ Accept: "application/json, text/plain;q=0.9, */*;q=0.5" });
390
+ const bodyValues = {};
391
+ for (const parameter of definition.parameters) {
392
+ const value = args[parameter.name];
393
+ if (value === void 0 || parameter.location === "path") continue;
394
+ if (parameter.location === "query") target.searchParams.append(parameter.name, serializeScalar(value));
395
+ if (parameter.location === "header") {
396
+ assertSafeHeaderName(parameter.name);
397
+ if (BLOCKED_PARAMETER_HEADERS.has(parameter.name.toLowerCase())) {
398
+ throw new Error(`Header ${parameter.name} cannot be supplied by the model`);
399
+ }
400
+ headers.set(parameter.name, serializeScalar(value));
401
+ }
402
+ if (parameter.location === "body") bodyValues[parameter.name] = value;
403
+ }
404
+ await this.applyAuth(definition, target, headers);
405
+ let body;
406
+ if (!["GET", "DELETE"].includes(definition.method) && Object.keys(bodyValues).length > 0) {
407
+ body = JSON.stringify(bodyValues);
408
+ headers.set("Content-Type", "application/json");
409
+ }
410
+ const timeout = AbortSignal.timeout(definition.timeoutMs);
411
+ const combined = signal === void 0 ? timeout : AbortSignal.any([signal, timeout]);
412
+ const started = Date.now();
413
+ const response = await fetch(target, {
414
+ method: definition.method,
415
+ headers,
416
+ ...body === void 0 ? {} : { body },
417
+ redirect: "error",
418
+ signal: combined
419
+ });
420
+ const payload = await readBounded(response, definition.maxResponseBytes);
421
+ const contentType = response.headers.get("content-type")?.split(";", 1)[0]?.trim().toLowerCase() ?? "";
422
+ const parsed = parseBody(payload.text, contentType);
423
+ const selected = definition.responsePointer === "" ? parsed : resolveJsonPointer(parsed, definition.responsePointer);
424
+ if (!response.ok) {
425
+ const detail = typeof selected === "string" ? selected : JSON.stringify(selected);
426
+ throw new Error(`HTTP ${response.status}: ${detail.slice(0, 500)}`);
427
+ }
428
+ return {
429
+ status: response.status,
430
+ contentType,
431
+ body: selected,
432
+ truncated: payload.truncated,
433
+ durationMs: Date.now() - started
434
+ };
435
+ }
436
+ async applyAuth(definition, target, headers) {
437
+ const auth = definition.auth;
438
+ if (auth.type === "none") return;
439
+ const resolved = await this.ctx.credentials.resolve(credentialRef(auth.credentialRef));
440
+ if (resolved === void 0) throw new Error(`Credential ${auth.credentialRef} is not configured`);
441
+ if (auth.type === "bearer") headers.set("Authorization", `Bearer ${resolved.value}`);
442
+ if (auth.type === "basic") {
443
+ headers.set("Authorization", `Basic ${Buffer.from(`${auth.username}:${resolved.value}`).toString("base64")}`);
444
+ }
445
+ if (auth.type === "api-key") {
446
+ if (auth.location === "header") {
447
+ assertSafeHeaderName(auth.name);
448
+ headers.set(auth.name, resolved.value);
449
+ } else {
450
+ target.searchParams.set(auth.name, resolved.value);
451
+ }
452
+ }
453
+ }
454
+ };
455
+ function validateArguments(parameters, raw) {
456
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) throw new Error("Tool arguments must be an object");
457
+ const source = raw;
458
+ const known = new Set(parameters.map((parameter) => parameter.name));
459
+ for (const key of Object.keys(source)) {
460
+ if (!known.has(key)) throw new Error(`Unknown API parameter: ${key}`);
461
+ }
462
+ const result = {};
463
+ for (const parameter of parameters) {
464
+ const value = source[parameter.name];
465
+ if (value === void 0) {
466
+ if (parameter.required) throw new Error(`Missing required API parameter: ${parameter.name}`);
467
+ continue;
468
+ }
469
+ if (!matchesType(parameter.type, value)) throw new Error(`API parameter ${parameter.name} must be ${parameter.type}`);
470
+ result[parameter.name] = value;
471
+ }
472
+ return result;
473
+ }
474
+ function matchesType(type, value) {
475
+ if (type === "string") return typeof value === "string";
476
+ if (type === "boolean") return typeof value === "boolean";
477
+ if (type === "number") return typeof value === "number" && Number.isFinite(value);
478
+ if (type === "integer") return typeof value === "number" && Number.isSafeInteger(value);
479
+ return value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean" || Array.isArray(value) || typeof value === "object";
480
+ }
481
+ function replacePathParameters(template, parameters, args) {
482
+ let path = template.startsWith("/") ? template : `/${template}`;
483
+ for (const parameter of parameters.filter((item) => item.location === "path")) {
484
+ const value = args[parameter.name];
485
+ if (value === void 0) throw new Error(`Path parameter ${parameter.name} is required`);
486
+ path = path.replaceAll(`{${parameter.name}}`, encodeURIComponent(serializeScalar(value)));
487
+ }
488
+ if (/\{[^}]+\}/.test(path)) throw new Error("API path contains an unresolved placeholder");
489
+ return path;
490
+ }
491
+ function serializeScalar(value) {
492
+ return typeof value === "string" ? value : typeof value === "object" ? JSON.stringify(value) : String(value);
493
+ }
494
+ async function readBounded(response, maxBytes) {
495
+ if (response.body === null) return { text: "", truncated: false };
496
+ const reader = response.body.getReader();
497
+ const chunks = [];
498
+ let size = 0;
499
+ let truncated = false;
500
+ try {
501
+ while (true) {
502
+ const next = await reader.read();
503
+ if (next.done) break;
504
+ const remaining = maxBytes - size;
505
+ if (next.value.byteLength > remaining) {
506
+ if (remaining > 0) chunks.push(next.value.slice(0, remaining));
507
+ truncated = true;
508
+ await reader.cancel("dsh-connect response limit reached");
509
+ break;
510
+ }
511
+ chunks.push(next.value);
512
+ size += next.value.byteLength;
513
+ }
514
+ } finally {
515
+ reader.releaseLock();
516
+ }
517
+ const bytes = new Uint8Array(chunks.reduce((total, chunk) => total + chunk.byteLength, 0));
518
+ let offset = 0;
519
+ for (const chunk of chunks) {
520
+ bytes.set(chunk, offset);
521
+ offset += chunk.byteLength;
522
+ }
523
+ return { text: new TextDecoder().decode(bytes), truncated };
524
+ }
525
+ function parseBody(text, contentType) {
526
+ if (text === "") return null;
527
+ if (contentType === "application/json" || contentType.endsWith("+json")) {
528
+ try {
529
+ return JSON.parse(text);
530
+ } catch {
531
+ throw new Error("API returned invalid JSON");
532
+ }
533
+ }
534
+ return text;
535
+ }
536
+ function resolveJsonPointer(value, pointer) {
537
+ if (pointer === "") return value;
538
+ if (!pointer.startsWith("/")) throw new Error("Response pointer must be empty or start with /");
539
+ let current = value;
540
+ for (const encoded of pointer.slice(1).split("/")) {
541
+ const key = encoded.replaceAll("~1", "/").replaceAll("~0", "~");
542
+ if (Array.isArray(current)) {
543
+ const index = Number(key);
544
+ if (!Number.isSafeInteger(index) || index < 0 || index >= current.length) throw new Error(`Response pointer not found: ${pointer}`);
545
+ current = current[index];
546
+ } else if (current !== null && typeof current === "object" && Object.hasOwn(current, key)) {
547
+ current = current[key];
548
+ } else {
549
+ throw new Error(`Response pointer not found: ${pointer}`);
550
+ }
551
+ }
552
+ return current;
553
+ }
554
+
555
+ // src/host/http/tools.ts
556
+ var ApiToolRegistry = class {
557
+ constructor(ctx) {
558
+ this.ctx = ctx;
559
+ this.executor = new SecureHttpExecutor(ctx);
560
+ }
561
+ disposers = [];
562
+ names = /* @__PURE__ */ new Set();
563
+ executor;
564
+ replace(definitions) {
565
+ const enabled = definitions.filter((definition) => definition.enabled);
566
+ const names = /* @__PURE__ */ new Set();
567
+ for (const definition of enabled) {
568
+ const name2 = toolName(definition);
569
+ if (names.has(name2)) throw new Error(`Duplicate API tool name: ${name2}`);
570
+ names.add(name2);
571
+ const existing = this.ctx.tools.get(name2);
572
+ if (existing !== void 0 && !this.names.has(name2)) throw new Error(`Tool name is already registered: ${name2}`);
573
+ }
574
+ this.dispose();
575
+ const next = [];
576
+ try {
577
+ for (const definition of enabled) next.push(this.ctx.tools.register(this.definition(definition)));
578
+ } catch (error) {
579
+ for (const dispose of next.reverse()) dispose();
580
+ throw error;
581
+ }
582
+ this.disposers = next;
583
+ this.names = names;
584
+ }
585
+ dispose() {
586
+ for (const dispose of this.disposers.splice(0).reverse()) dispose();
587
+ this.names.clear();
588
+ }
589
+ definition(definition) {
590
+ return {
591
+ name: toolName(definition),
592
+ description: definition.description,
593
+ parameters: parametersSchema(definition.parameters),
594
+ output: {
595
+ schema: {
596
+ type: "object",
597
+ additionalProperties: false,
598
+ properties: {
599
+ status: { type: "integer" },
600
+ contentType: { type: "string" },
601
+ body: {},
602
+ truncated: { type: "boolean" },
603
+ durationMs: { type: "integer" }
604
+ },
605
+ required: ["status", "contentType", "body", "truncated", "durationMs"]
606
+ },
607
+ render: (_args, value) => [{ type: "text", text: JSON.stringify(value, null, 2) }]
608
+ },
609
+ timeoutMs: definition.timeoutMs,
610
+ isConcurrencySafe: () => definition.method === "GET",
611
+ execute: (args, exec) => this.executor.execute(definition, args, exec.signal),
612
+ presentCall: (args) => ({
613
+ card: "generic",
614
+ title: definition.name,
615
+ kind: "fetch",
616
+ rawInput: args
617
+ })
618
+ };
619
+ }
620
+ };
621
+ function parametersSchema(parameters) {
622
+ const properties = {};
623
+ const required = [];
624
+ for (const parameter of parameters) {
625
+ properties[parameter.name] = {
626
+ ...parameter.type === "json" ? {} : { type: parameter.type },
627
+ description: parameter.description || `${parameter.location} parameter ${parameter.name}`
628
+ };
629
+ if (parameter.required || parameter.location === "path") required.push(parameter.name);
630
+ }
631
+ return {
632
+ type: "object",
633
+ properties,
634
+ required,
635
+ additionalProperties: false
636
+ };
637
+ }
638
+
639
+ // src/host/http/conversation-api.ts
640
+ import { defineTool } from "@deepseek-ai/dsh-tools";
641
+ import { z as z2 } from "zod";
642
+ var CONVERSATION_API_TOOL = "dsh_connect_create_http_api";
643
+ var conversationApiInput = z2.object({
644
+ name: z2.string().min(1).max(100),
645
+ slug: z2.string().min(2).max(48).regex(/^[a-z][a-z0-9_]*$/),
646
+ description: z2.string().min(1).max(1e3),
647
+ method: apiMethodSchema,
648
+ baseUrl: z2.string().url().max(2048),
649
+ pathTemplate: z2.string().min(1).max(2048).default("/"),
650
+ parameters: z2.array(apiParameterSchema).max(50).default([]),
651
+ authType: z2.enum(["none", "bearer", "api-key", "basic"]).default("none"),
652
+ authLocation: z2.enum(["header", "query"]).optional(),
653
+ authName: z2.string().min(1).max(100).optional(),
654
+ basicUsername: z2.string().min(1).max(200).optional(),
655
+ timeoutMs: z2.number().int().min(1e3).max(12e4).default(3e4),
656
+ maxResponseBytes: z2.number().int().min(1024).max(2 * 1024 * 1024).default(131072),
657
+ responsePointer: z2.string().max(500).default("")
658
+ }).strict().superRefine((input, refinement) => {
659
+ if (input.authType === "api-key") {
660
+ if (input.authLocation === void 0) refinement.addIssue({ code: "custom", path: ["authLocation"], message: "Required for api-key authentication" });
661
+ if (input.authName === void 0) refinement.addIssue({ code: "custom", path: ["authName"], message: "Required for api-key authentication" });
662
+ } else if (input.authLocation !== void 0 || input.authName !== void 0) {
663
+ refinement.addIssue({ code: "custom", path: ["authType"], message: "API key fields are only allowed with api-key authentication" });
664
+ }
665
+ if (input.authType === "basic") {
666
+ if (input.basicUsername === void 0) refinement.addIssue({ code: "custom", path: ["basicUsername"], message: "Required for basic authentication" });
667
+ } else if (input.basicUsername !== void 0) {
668
+ refinement.addIssue({ code: "custom", path: ["basicUsername"], message: "Only allowed with basic authentication" });
669
+ }
670
+ });
671
+ var parameterItem = {
672
+ type: "object",
673
+ additionalProperties: false,
674
+ properties: {
675
+ name: { type: "string", required: true, description: "Parameter name." },
676
+ location: { type: "string", enum: ["path", "query", "header", "body"], required: true },
677
+ type: { type: "string", enum: ["string", "number", "integer", "boolean", "json"], required: true },
678
+ description: { type: "string", description: "What the parameter means." },
679
+ required: { type: "boolean", description: "Whether the caller must provide this parameter." }
680
+ }
681
+ };
682
+ function registerConversationApiTool(ctx, validate, create) {
683
+ const disposeTool = ctx.tools.register(defineTool({
684
+ name: CONVERSATION_API_TOOL,
685
+ description: [
686
+ "Create and register a reusable HTTP API in DSH\u8FDE\u63A5\u5668 when the user asks to add, configure, or register an API.",
687
+ "Derive this non-secret configuration from the conversation, then call this tool so DSH can show an approval preview.",
688
+ "Never ask for or pass tokens, passwords, API-key values, Authorization values, or other credentials.",
689
+ "For authenticated APIs, provide only the authentication type and public metadata; the user completes credentials in Settings."
690
+ ].join(" "),
691
+ parameters: {
692
+ name: { type: "string", required: true, description: "User-facing API name." },
693
+ slug: { type: "string", required: true, description: "Lowercase tool suffix matching ^[a-z][a-z0-9_]{1,47}$." },
694
+ description: { type: "string", required: true, description: "Describe when the Agent should call this API and what it returns." },
695
+ method: { type: "string", enum: ["GET", "POST", "PUT", "PATCH", "DELETE"], required: true },
696
+ baseUrl: { type: "string", required: true, description: "Public HTTP(S) base URL without credentials or a query string." },
697
+ pathTemplate: { type: "string", description: "Request path such as /v1/weather/{city}. Default /." },
698
+ parameters: { type: "array", items: parameterItem, description: "Path, query, header, and body arguments exposed to the Agent." },
699
+ authType: { type: "string", enum: ["none", "bearer", "api-key", "basic"], description: "Authentication type. Default none." },
700
+ authLocation: { type: "string", enum: ["header", "query"], description: "Required only for api-key authentication." },
701
+ authName: { type: "string", description: "Header or query parameter name for an API key, never its value." },
702
+ basicUsername: { type: "string", description: "Public username for basic authentication, never the password." },
703
+ timeoutMs: { type: "integer", description: "Timeout from 1000 to 120000 milliseconds. Default 30000." },
704
+ maxResponseBytes: { type: "integer", description: "Response limit from 1024 to 2097152 bytes. Default 131072." },
705
+ responsePointer: { type: "string", description: "Optional RFC 6901 JSON pointer selecting the useful response value." }
706
+ },
707
+ output: {
708
+ schema: {
709
+ type: "object",
710
+ additionalProperties: false,
711
+ properties: {
712
+ created: { type: "boolean", required: true },
713
+ id: { type: "string", required: true },
714
+ name: { type: "string", required: true },
715
+ toolName: { type: "string", required: true },
716
+ enabled: { type: "boolean", required: true },
717
+ credentialRequired: { type: "boolean", required: true },
718
+ nextStep: { type: "string", required: true }
719
+ }
720
+ },
721
+ render: (_args, value) => [{ type: "text", text: JSON.stringify(value, null, 2) }]
722
+ },
723
+ isConcurrencySafe: () => false,
724
+ async execute(rawInput) {
725
+ const input = conversationApiInput.parse(rawInput);
726
+ validate(input);
727
+ const saved = await create(input);
728
+ if (saved === void 0) throw new Error("HTTP API was saved but could not be loaded");
729
+ const credentialRequired = saved.auth.type !== "none" && !saved.auth.credentialConfigured;
730
+ return {
731
+ created: true,
732
+ id: saved.id,
733
+ name: saved.name,
734
+ toolName: saved.toolName,
735
+ enabled: saved.enabled,
736
+ credentialRequired,
737
+ nextStep: credentialRequired ? "\u8BF7\u524D\u5F80\u201CDSH\u8FDE\u63A5\u5668 \u2192 HTTP API\u201D\u8865\u5145\u51ED\u636E\u5E76\u542F\u7528\u8BE5 API\u3002" : `API \u5DF2\u542F\u7528\uFF0CAgent \u73B0\u5728\u53EF\u4EE5\u8C03\u7528 ${saved.toolName}\u3002`
738
+ };
739
+ },
740
+ presentCall: (input) => ({
741
+ card: "generic",
742
+ title: `\u521B\u5EFA HTTP API\uFF1A${input.name}`,
743
+ kind: "edit",
744
+ rawInput: input
745
+ })
746
+ }));
747
+ const disposeApproval = ctx.on("tools/pre-execute", async (exec, next) => {
748
+ if (exec.name !== CONVERSATION_API_TOOL) return next();
749
+ const downstream = await next();
750
+ if (downstream.kind !== "allow") return downstream;
751
+ const parsed = conversationApiInput.safeParse(exec.arguments);
752
+ if (!parsed.success) {
753
+ return { kind: "deny", reason: "HTTP API \u914D\u7F6E\u65E0\u6548\uFF1B\u5BF9\u8BDD\u521B\u5EFA\u4E0D\u63A5\u53D7\u672A\u58F0\u660E\u5B57\u6BB5\u6216\u4EFB\u4F55\u51ED\u636E\u3002\u8BF7\u4FEE\u6B63\u975E\u654F\u611F\u914D\u7F6E\u540E\u91CD\u8BD5\u3002" };
754
+ }
755
+ try {
756
+ validate(parsed.data);
757
+ } catch (error) {
758
+ return { kind: "deny", reason: safeValidationMessage(error) };
759
+ }
760
+ return { kind: "ask", reason: approvalPreview(parsed.data) };
761
+ });
762
+ return [disposeTool, disposeApproval];
763
+ }
764
+ function approvalPreview(input) {
765
+ const auth = input.authType === "none" ? "\u65E0\u8BA4\u8BC1" : `${input.authType}\uFF08\u51ED\u636E\u7A0D\u540E\u5728\u8BBE\u7F6E\u4E2D\u586B\u5199\uFF0C\u6B64 API \u5C06\u5148\u4FDD\u6301\u7981\u7528\uFF09`;
766
+ return [
767
+ "\u786E\u8BA4\u521B\u5EFA\u4EE5\u4E0B HTTP API\uFF1A",
768
+ `\u540D\u79F0\uFF1A${input.name}`,
769
+ `\u8BF7\u6C42\uFF1A${input.method} ${previewTarget(input)}`,
770
+ `\u5DE5\u5177\uFF1Adsh_connect_api_${input.slug}`,
771
+ `\u53C2\u6570\uFF1A${input.parameters.length} \u4E2A`,
772
+ `\u8BA4\u8BC1\uFF1A${auth}`
773
+ ].join("\n");
774
+ }
775
+ function previewTarget(input) {
776
+ try {
777
+ const path = input.pathTemplate.startsWith("/") ? input.pathTemplate : `/${input.pathTemplate}`;
778
+ return new URL(path, input.baseUrl).toString();
779
+ } catch {
780
+ return `${input.baseUrl.replace(/\/$/, "")}/${input.pathTemplate.replace(/^\//, "")}`;
781
+ }
782
+ }
783
+ function safeValidationMessage(error) {
784
+ const message = error instanceof Error ? error.message : String(error);
785
+ return `HTTP API \u914D\u7F6E\u65E0\u6548\uFF1A${message.replaceAll(/[\r\n]+/g, " ").slice(0, 500)}`;
786
+ }
787
+
788
+ // src/host/metadata/profiler.ts
789
+ import { createHash, randomUUID } from "node:crypto";
790
+ import { credentialRef as credentialRef2 } from "@deepseek-ai/dsh-credentials";
791
+
792
+ // src/host/datasource/clickhouse.ts
793
+ import { createClient } from "@clickhouse/client";
794
+
795
+ // src/host/datasource/provider.ts
796
+ var SENSITIVE_COLUMN = /(?:pass(?:word)?|secret|token|api[_-]?key|authorization|cookie|email|e[-_]?mail|phone|mobile|id[_-]?card|身份证|手机号|邮箱)/i;
797
+ function sanitizeRows(rows, maxRows) {
798
+ return rows.slice(0, maxRows).map((row) => {
799
+ if (row === null || typeof row !== "object" || Array.isArray(row)) return { value: sanitizeValue(row) };
800
+ return Object.fromEntries(Object.entries(row).map(([key, value]) => [
801
+ key,
802
+ SENSITIVE_COLUMN.test(key) ? "[REDACTED]" : sanitizeValue(value)
803
+ ]));
804
+ });
805
+ }
806
+ function sanitizeValue(value) {
807
+ if (value === null || typeof value === "number" || typeof value === "boolean") return value;
808
+ if (value instanceof Date) return value.toISOString();
809
+ if (typeof value === "bigint") return value.toString();
810
+ if (value instanceof Uint8Array) return `[binary:${value.byteLength} bytes]`;
811
+ const text = typeof value === "string" ? value : JSON.stringify(value);
812
+ return text.length <= 200 ? text : `${text.slice(0, 200)}...`;
813
+ }
814
+ function throwIfAborted(signal) {
815
+ signal?.throwIfAborted();
816
+ }
817
+ function validateReadOnlySql(rawSql) {
818
+ const sql = rawSql.trim();
819
+ if (sql === "") throw new Error("SQL cannot be empty");
820
+ if (sql.length > 2e4) throw new Error("SQL is limited to 20000 characters");
821
+ if (sql.includes(";") || sql.includes("--") || sql.includes("/*") || sql.includes("*/")) {
822
+ throw new Error("Only one read-only SQL statement is allowed; comments and semicolons are not allowed");
823
+ }
824
+ if (!/^(?:select|with)\b/i.test(sql)) throw new Error("Only SELECT or WITH queries are allowed");
825
+ if (/\b(?:insert|update|delete|drop|alter|create|truncate|grant|revoke|replace|merge|call|set|use|into|outfile|dumpfile)\b/i.test(sql)) {
826
+ throw new Error("The SQL contains a write or session-changing operation");
827
+ }
828
+ return sql;
829
+ }
830
+ function queryLimit(value) {
831
+ if (value === void 0 || !Number.isFinite(value)) return 100;
832
+ return Math.min(1e3, Math.max(1, Math.trunc(value)));
833
+ }
834
+ function queryResult(columns, rows, limit, startedAt) {
835
+ const truncated = rows.length > limit;
836
+ return {
837
+ columns,
838
+ rows: sanitizeRows(rows, limit),
839
+ rowCount: Math.min(rows.length, limit),
840
+ truncated,
841
+ durationMs: Date.now() - startedAt
842
+ };
843
+ }
844
+
845
+ // src/host/datasource/clickhouse.ts
846
+ var ClickhouseProvider = class {
847
+ async test(source, password, signal) {
848
+ const client = connect(source, password);
849
+ try {
850
+ await client.query({ query: "SELECT 1", format: "JSONEachRow", ...signal === void 0 ? {} : { abort_signal: signal } });
851
+ } finally {
852
+ await client.close();
853
+ }
854
+ }
855
+ async discover(source, password, signal) {
856
+ const client = connect(source, password);
857
+ try {
858
+ const databases = source.schemaInclude.length > 0 ? source.schemaInclude : [source.database];
859
+ const dbList = databases.map(quoteLiteral).join(", ");
860
+ const tableResult = await client.query({
861
+ query: `SELECT database, name, engine, comment, create_table_query, is_temporary
862
+ FROM system.tables WHERE database IN (${dbList}) ORDER BY database, name`,
863
+ format: "JSONEachRow",
864
+ ...signal === void 0 ? {} : { abort_signal: signal }
865
+ });
866
+ const columnResult = await client.query({
867
+ query: `SELECT database, table, name, type, default_expression, comment, is_in_primary_key
868
+ FROM system.columns WHERE database IN (${dbList}) ORDER BY database, table, position`,
869
+ format: "JSONEachRow",
870
+ ...signal === void 0 ? {} : { abort_signal: signal }
871
+ });
872
+ const tables = await tableResult.json();
873
+ const columns = await columnResult.json();
874
+ const columnsByObject = groupColumns(columns);
875
+ const result = [];
876
+ for (const table of tables) {
877
+ throwIfAborted(signal);
878
+ let sample = [];
879
+ if (source.sampleRows > 0) {
880
+ const rows = await client.query({
881
+ query: `SELECT * FROM ${quote(table.database)}.${quote(table.name)} LIMIT ${source.sampleRows}`,
882
+ format: "JSONEachRow",
883
+ ...signal === void 0 ? {} : { abort_signal: signal }
884
+ });
885
+ sample = sanitizeRows(await rows.json(), source.sampleRows);
886
+ }
887
+ result.push({
888
+ schemaName: table.database,
889
+ objectName: table.name,
890
+ objectType: table.engine === "View" || table.engine.endsWith("View") ? "view" : "table",
891
+ engine: table.engine,
892
+ ...table.comment ? { comment: table.comment } : {},
893
+ ddl: table.create_table_query,
894
+ columns: columnsByObject.get(`${table.database}\0${table.name}`) ?? [],
895
+ sample
896
+ });
897
+ }
898
+ return result;
899
+ } finally {
900
+ await client.close();
901
+ }
902
+ }
903
+ async query(source, password, rawSql, limit, signal) {
904
+ const sql = validateReadOnlySql(rawSql);
905
+ const bounded = queryLimit(limit);
906
+ const startedAt = Date.now();
907
+ const client = connect(source, password);
908
+ try {
909
+ const result = await client.query({ query: `SELECT * FROM (${sql}) AS dsh_connect_query LIMIT ${bounded + 1}`, format: "JSONEachRow", clickhouse_settings: { readonly: "2", max_execution_time: 30 }, ...signal === void 0 ? {} : { abort_signal: signal } });
910
+ const rows = await result.json();
911
+ throwIfAborted(signal);
912
+ return queryResult(Object.keys(rows[0] ?? {}), rows, bounded, startedAt);
913
+ } finally {
914
+ await client.close();
915
+ }
916
+ }
917
+ };
918
+ function connect(source, password) {
919
+ return createClient({
920
+ url: `${source.tls ? "https" : "http"}://${source.host}:${source.port}`,
921
+ username: source.username,
922
+ password,
923
+ database: source.database,
924
+ request_timeout: 3e4
925
+ });
926
+ }
927
+ function groupColumns(rows) {
928
+ const result = /* @__PURE__ */ new Map();
929
+ for (const row of rows) {
930
+ const key = `${row.database}\0${row.table}`;
931
+ const list = result.get(key) ?? [];
932
+ list.push({
933
+ name: row.name,
934
+ type: row.type,
935
+ nullable: row.type.startsWith("Nullable("),
936
+ defaultValue: row.default_expression === "" ? null : row.default_expression,
937
+ ...row.comment ? { comment: row.comment } : {},
938
+ primaryKey: Boolean(row.is_in_primary_key)
939
+ });
940
+ result.set(key, list);
941
+ }
942
+ return result;
943
+ }
944
+ function quote(value) {
945
+ return `\`${value.replaceAll("`", "``")}\``;
946
+ }
947
+ function quoteLiteral(value) {
948
+ return `'${value.replaceAll("\\", "\\\\").replaceAll("'", "\\'")}'`;
949
+ }
950
+
951
+ // src/host/datasource/mysql.ts
952
+ import mysql from "mysql2/promise";
953
+ var MysqlProvider = class {
954
+ async test(source, password, signal) {
955
+ throwIfAborted(signal);
956
+ const connection = await connect2(source, password);
957
+ try {
958
+ await connection.query("SELECT 1");
959
+ throwIfAborted(signal);
960
+ } finally {
961
+ await connection.end();
962
+ }
963
+ }
964
+ async discover(source, password, signal) {
965
+ const connection = await connect2(source, password);
966
+ try {
967
+ const schemas = source.schemaInclude.length > 0 ? source.schemaInclude : [source.database];
968
+ const placeholders = schemas.map(() => "?").join(", ");
969
+ const [tables] = await connection.query(
970
+ `SELECT TABLE_SCHEMA, TABLE_NAME, TABLE_TYPE, ENGINE, TABLE_COMMENT
971
+ FROM information_schema.TABLES
972
+ WHERE TABLE_SCHEMA IN (${placeholders})
973
+ ORDER BY TABLE_SCHEMA, TABLE_NAME`,
974
+ schemas
975
+ );
976
+ const [columns] = await connection.query(
977
+ `SELECT TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME, COLUMN_TYPE, IS_NULLABLE,
978
+ COLUMN_DEFAULT, COLUMN_COMMENT, COLUMN_KEY
979
+ FROM information_schema.COLUMNS
980
+ WHERE TABLE_SCHEMA IN (${placeholders})
981
+ ORDER BY TABLE_SCHEMA, TABLE_NAME, ORDINAL_POSITION`,
982
+ schemas
983
+ );
984
+ const [foreignKeys] = await connection.query(
985
+ `SELECT TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME, REFERENCED_TABLE_SCHEMA,
986
+ REFERENCED_TABLE_NAME, REFERENCED_COLUMN_NAME
987
+ FROM information_schema.KEY_COLUMN_USAGE
988
+ WHERE TABLE_SCHEMA IN (${placeholders})
989
+ AND REFERENCED_TABLE_NAME IS NOT NULL
990
+ ORDER BY TABLE_SCHEMA, TABLE_NAME, ORDINAL_POSITION`,
991
+ schemas
992
+ );
993
+ const columnsByObject = groupColumns2(columns, foreignKeys);
994
+ const result = [];
995
+ for (const table of tables) {
996
+ throwIfAborted(signal);
997
+ const qualified = `${quote2(table.TABLE_SCHEMA)}.${quote2(table.TABLE_NAME)}`;
998
+ const [ddlRows] = await connection.query(`SHOW CREATE ${table.TABLE_TYPE === "VIEW" ? "VIEW" : "TABLE"} ${qualified}`);
999
+ const ddlRecord = ddlRows[0];
1000
+ const ddl = ddlRecord === void 0 ? "" : String(ddlRecord["Create Table"] ?? ddlRecord["Create View"] ?? "");
1001
+ let sample = [];
1002
+ if (source.sampleRows > 0) {
1003
+ const [rows] = await connection.query(`SELECT * FROM ${qualified} LIMIT ${source.sampleRows}`);
1004
+ sample = sanitizeRows(rows, source.sampleRows);
1005
+ }
1006
+ result.push({
1007
+ schemaName: table.TABLE_SCHEMA,
1008
+ objectName: table.TABLE_NAME,
1009
+ objectType: table.TABLE_TYPE === "VIEW" ? "view" : "table",
1010
+ ...table.ENGINE === null ? {} : { engine: table.ENGINE },
1011
+ ...table.TABLE_COMMENT ? { comment: table.TABLE_COMMENT } : {},
1012
+ ddl,
1013
+ columns: columnsByObject.get(`${table.TABLE_SCHEMA}\0${table.TABLE_NAME}`) ?? [],
1014
+ sample
1015
+ });
1016
+ }
1017
+ return result;
1018
+ } finally {
1019
+ await connection.end();
1020
+ }
1021
+ }
1022
+ async query(source, password, rawSql, limit, signal) {
1023
+ const sql = validateReadOnlySql(rawSql);
1024
+ const bounded = queryLimit(limit);
1025
+ const startedAt = Date.now();
1026
+ throwIfAborted(signal);
1027
+ const connection = await connect2(source, password);
1028
+ try {
1029
+ await connection.query("START TRANSACTION READ ONLY");
1030
+ const [rows, fields] = await connection.query({ sql: `SELECT * FROM (${sql}) AS dsh_connect_query LIMIT ${bounded + 1}`, timeout: 3e4 });
1031
+ throwIfAborted(signal);
1032
+ const columns = fields?.map((field) => field.name) ?? Object.keys(rows[0] ?? {});
1033
+ return queryResult(columns, rows, bounded, startedAt);
1034
+ } finally {
1035
+ await connection.rollback().catch(() => {
1036
+ });
1037
+ await connection.end();
1038
+ }
1039
+ }
1040
+ };
1041
+ function connect2(source, password) {
1042
+ return mysql.createConnection({
1043
+ host: source.host,
1044
+ port: source.port,
1045
+ user: source.username,
1046
+ password,
1047
+ database: source.database,
1048
+ connectTimeout: 1e4,
1049
+ ...source.tls ? { ssl: {} } : {}
1050
+ });
1051
+ }
1052
+ function groupColumns2(rows, foreignKeys) {
1053
+ const result = /* @__PURE__ */ new Map();
1054
+ const references = new Map(foreignKeys.map((row) => [
1055
+ `${row.TABLE_SCHEMA}\0${row.TABLE_NAME}\0${row.COLUMN_NAME}`,
1056
+ { schemaName: row.REFERENCED_TABLE_SCHEMA, objectName: row.REFERENCED_TABLE_NAME, columnName: row.REFERENCED_COLUMN_NAME }
1057
+ ]));
1058
+ for (const row of rows) {
1059
+ const key = `${row.TABLE_SCHEMA}\0${row.TABLE_NAME}`;
1060
+ const list = result.get(key) ?? [];
1061
+ list.push({
1062
+ name: row.COLUMN_NAME,
1063
+ type: row.COLUMN_TYPE,
1064
+ nullable: row.IS_NULLABLE === "YES",
1065
+ defaultValue: row.COLUMN_DEFAULT === null ? null : String(row.COLUMN_DEFAULT),
1066
+ ...row.COLUMN_COMMENT ? { comment: row.COLUMN_COMMENT } : {},
1067
+ primaryKey: row.COLUMN_KEY === "PRI",
1068
+ ...references.get(`${row.TABLE_SCHEMA}\0${row.TABLE_NAME}\0${row.COLUMN_NAME}`) === void 0 ? {} : { references: references.get(`${row.TABLE_SCHEMA}\0${row.TABLE_NAME}\0${row.COLUMN_NAME}`) }
1069
+ });
1070
+ result.set(key, list);
1071
+ }
1072
+ return result;
1073
+ }
1074
+ function quote2(value) {
1075
+ return `\`${value.replaceAll("`", "``")}\``;
1076
+ }
1077
+
1078
+ // src/host/datasource/postgresql.ts
1079
+ import { Client } from "pg";
1080
+ var PostgresqlProvider = class {
1081
+ async test(source, password, signal) {
1082
+ const client = await connect3(source, password);
1083
+ try {
1084
+ await queryRows(client, "SELECT 1", [], signal);
1085
+ } finally {
1086
+ await client.end();
1087
+ }
1088
+ }
1089
+ async discover(source, password, signal) {
1090
+ const client = await connect3(source, password);
1091
+ try {
1092
+ const schemas = source.schemaInclude.length > 0 ? source.schemaInclude : ["public"];
1093
+ const tables = await queryRows(client, `SELECT t.table_schema, t.table_name, t.table_type,
1094
+ obj_description((quote_ident(t.table_schema) || '.' || quote_ident(t.table_name))::regclass) AS table_comment
1095
+ FROM information_schema.tables t
1096
+ WHERE t.table_schema = ANY($1::text[])
1097
+ ORDER BY t.table_schema, t.table_name`, [schemas], signal);
1098
+ const columns = await queryRows(client, `SELECT c.table_schema, c.table_name, c.column_name, c.data_type, c.udt_name,
1099
+ c.is_nullable, c.column_default,
1100
+ col_description((quote_ident(c.table_schema) || '.' || quote_ident(c.table_name))::regclass, c.ordinal_position) AS column_comment,
1101
+ EXISTS (
1102
+ SELECT 1 FROM information_schema.table_constraints tc
1103
+ JOIN information_schema.key_column_usage kcu
1104
+ ON tc.constraint_name = kcu.constraint_name AND tc.constraint_schema = kcu.constraint_schema
1105
+ WHERE tc.constraint_type = 'PRIMARY KEY'
1106
+ AND tc.table_schema = c.table_schema AND tc.table_name = c.table_name
1107
+ AND kcu.column_name = c.column_name
1108
+ ) AS primary_key
1109
+ FROM information_schema.columns c
1110
+ WHERE c.table_schema = ANY($1::text[])
1111
+ ORDER BY c.table_schema, c.table_name, c.ordinal_position`, [schemas], signal);
1112
+ const foreignKeys = await queryRows(client, `SELECT fk.table_schema, fk.table_name, fk.column_name,
1113
+ pk.table_schema AS referenced_table_schema,
1114
+ pk.table_name AS referenced_table_name,
1115
+ pk.column_name AS referenced_column_name
1116
+ FROM information_schema.referential_constraints rc
1117
+ JOIN information_schema.key_column_usage fk
1118
+ ON fk.constraint_catalog = rc.constraint_catalog
1119
+ AND fk.constraint_schema = rc.constraint_schema
1120
+ AND fk.constraint_name = rc.constraint_name
1121
+ JOIN information_schema.key_column_usage pk
1122
+ ON pk.constraint_catalog = rc.unique_constraint_catalog
1123
+ AND pk.constraint_schema = rc.unique_constraint_schema
1124
+ AND pk.constraint_name = rc.unique_constraint_name
1125
+ AND pk.ordinal_position = fk.position_in_unique_constraint
1126
+ WHERE fk.table_schema = ANY($1::text[])
1127
+ ORDER BY fk.table_schema, fk.table_name, fk.ordinal_position`, [schemas], signal);
1128
+ const columnsByObject = groupColumns3(columns, foreignKeys);
1129
+ const result = [];
1130
+ for (const table of tables) {
1131
+ throwIfAborted(signal);
1132
+ const objectColumns = columnsByObject.get(`${table.table_schema}\0${table.table_name}`) ?? [];
1133
+ const qualified = `${quote3(table.table_schema)}.${quote3(table.table_name)}`;
1134
+ let sample = [];
1135
+ if (source.sampleRows > 0) {
1136
+ const rows = await queryRows(client, `SELECT * FROM ${qualified} LIMIT ${source.sampleRows}`, [], signal);
1137
+ sample = sanitizeRows(rows, source.sampleRows);
1138
+ }
1139
+ result.push({
1140
+ schemaName: table.table_schema,
1141
+ objectName: table.table_name,
1142
+ objectType: table.table_type === "VIEW" ? "view" : "table",
1143
+ ...table.table_comment ? { comment: table.table_comment } : {},
1144
+ ddl: buildDdl(table.table_schema, table.table_name, objectColumns, table.table_type === "VIEW"),
1145
+ columns: objectColumns,
1146
+ sample
1147
+ });
1148
+ }
1149
+ return result;
1150
+ } finally {
1151
+ await client.end();
1152
+ }
1153
+ }
1154
+ async query(source, password, rawSql, limit, signal) {
1155
+ const sql = validateReadOnlySql(rawSql);
1156
+ const bounded = queryLimit(limit);
1157
+ const startedAt = Date.now();
1158
+ const client = await connect3(source, password);
1159
+ try {
1160
+ const result = await client.query({ text: `SELECT * FROM (${sql}) AS dsh_connect_query LIMIT $1`, values: [bounded + 1], ...signal === void 0 ? {} : { signal } });
1161
+ throwIfAborted(signal);
1162
+ return queryResult(result.fields.map((field) => field.name), result.rows, bounded, startedAt);
1163
+ } finally {
1164
+ await client.end();
1165
+ }
1166
+ }
1167
+ };
1168
+ async function queryRows(client, text, values, signal) {
1169
+ throwIfAborted(signal);
1170
+ const config = {
1171
+ text,
1172
+ ...values.length === 0 ? {} : { values },
1173
+ ...signal === void 0 ? {} : { signal }
1174
+ };
1175
+ const result = await client.query(config);
1176
+ throwIfAborted(signal);
1177
+ return result.rows;
1178
+ }
1179
+ async function connect3(source, password) {
1180
+ const client = new Client({
1181
+ host: source.host,
1182
+ port: source.port,
1183
+ user: source.username,
1184
+ password,
1185
+ database: source.database,
1186
+ connectionTimeoutMillis: 1e4,
1187
+ statement_timeout: 3e4,
1188
+ ssl: source.tls ? { rejectUnauthorized: true } : false
1189
+ });
1190
+ await client.connect();
1191
+ await client.query("SET SESSION CHARACTERISTICS AS TRANSACTION READ ONLY");
1192
+ return client;
1193
+ }
1194
+ function groupColumns3(rows, foreignKeys) {
1195
+ const result = /* @__PURE__ */ new Map();
1196
+ const references = new Map(foreignKeys.map((row) => [
1197
+ `${row.table_schema}\0${row.table_name}\0${row.column_name}`,
1198
+ { schemaName: row.referenced_table_schema, objectName: row.referenced_table_name, columnName: row.referenced_column_name }
1199
+ ]));
1200
+ for (const row of rows) {
1201
+ const key = `${row.table_schema}\0${row.table_name}`;
1202
+ const list = result.get(key) ?? [];
1203
+ list.push({
1204
+ name: row.column_name,
1205
+ type: row.data_type === "USER-DEFINED" ? row.udt_name : row.data_type,
1206
+ nullable: row.is_nullable === "YES",
1207
+ defaultValue: row.column_default,
1208
+ ...row.column_comment ? { comment: row.column_comment } : {},
1209
+ primaryKey: row.primary_key,
1210
+ ...references.get(`${row.table_schema}\0${row.table_name}\0${row.column_name}`) === void 0 ? {} : { references: references.get(`${row.table_schema}\0${row.table_name}\0${row.column_name}`) }
1211
+ });
1212
+ result.set(key, list);
1213
+ }
1214
+ return result;
1215
+ }
1216
+ function buildDdl(schema, table, columns, view) {
1217
+ if (view) return `CREATE VIEW ${quote3(schema)}.${quote3(table)} AS /* definition not exposed by information_schema */;`;
1218
+ const definitions = columns.map((column) => {
1219
+ const suffix = [column.nullable ? "" : "NOT NULL", column.defaultValue == null ? "" : `DEFAULT ${column.defaultValue}`].filter(Boolean).join(" ");
1220
+ return ` ${quote3(column.name)} ${column.type}${suffix === "" ? "" : ` ${suffix}`}`;
1221
+ });
1222
+ const primary = columns.filter((column) => column.primaryKey).map((column) => quote3(column.name));
1223
+ if (primary.length > 0) definitions.push(` PRIMARY KEY (${primary.join(", ")})`);
1224
+ for (const column of columns) {
1225
+ if (column.references !== void 0) {
1226
+ definitions.push(` FOREIGN KEY (${quote3(column.name)}) REFERENCES ${quote3(column.references.schemaName)}.${quote3(column.references.objectName)} (${quote3(column.references.columnName)})`);
1227
+ }
1228
+ }
1229
+ return `CREATE TABLE ${quote3(schema)}.${quote3(table)} (
1230
+ ${definitions.join(",\n")}
1231
+ );`;
1232
+ }
1233
+ function quote3(value) {
1234
+ return `"${value.replaceAll('"', '""')}"`;
1235
+ }
1236
+
1237
+ // src/host/datasource/index.ts
1238
+ var providers = {
1239
+ mysql: new MysqlProvider(),
1240
+ postgresql: new PostgresqlProvider(),
1241
+ clickhouse: new ClickhouseProvider()
1242
+ };
1243
+ function providerFor(type) {
1244
+ return providers[type];
1245
+ }
1246
+
1247
+ // src/host/metadata/enricher.ts
1248
+ import { BlockAssembler, createUserMessage, deepFreeze } from "@deepseek-ai/dsh-llm";
1249
+ import { z as z3 } from "zod";
1250
+ var METADATA_MODEL_PROMPT_VERSION = "v3";
1251
+ var MetadataModelOutputError = class extends Error {
1252
+ constructor(message, options) {
1253
+ super(message, options);
1254
+ this.name = "MetadataModelOutputError";
1255
+ }
1256
+ };
1257
+ var stringList = (maxItems, maxLength) => z3.preprocess(
1258
+ (value) => normalizeStringList(value, maxItems, maxLength),
1259
+ z3.array(z3.string().max(maxLength)).max(maxItems)
1260
+ );
1261
+ var semanticProfileSchema = z3.object({
1262
+ term: z3.string().max(100),
1263
+ description: z3.string().max(500),
1264
+ tags: stringList(20, 50),
1265
+ synonyms: stringList(30, 100),
1266
+ columns: z3.array(z3.object({
1267
+ name: z3.string(),
1268
+ term: z3.string().max(100),
1269
+ description: z3.string().max(300),
1270
+ synonyms: stringList(20, 100),
1271
+ enums: stringList(50, 100),
1272
+ role: z3.enum(["identifier", "dimension", "measure", "time", "unknown"]).default("unknown")
1273
+ })).max(500),
1274
+ confidence: z3.number().refine(Number.isFinite, "confidence must be a finite number"),
1275
+ confidenceReason: z3.string().max(500),
1276
+ temporary: z3.boolean()
1277
+ });
1278
+ var MetadataEnricher = class {
1279
+ constructor(ctx) {
1280
+ this.ctx = ctx;
1281
+ }
1282
+ async enrich(object, useAi, signal, selectedRoute) {
1283
+ const fallback = heuristicProfile(object);
1284
+ if (!useAi) return fallback;
1285
+ const llm = this.ctx.get("llm");
1286
+ const defaults = this.ctx.get("agentDefaultModel");
1287
+ if (llm === void 0 || defaults === void 0) {
1288
+ return { ...fallback, modelStatus: "failed", confidenceReason: `${fallback.confidenceReason}; DSH model service unavailable` };
1289
+ }
1290
+ try {
1291
+ signal?.throwIfAborted();
1292
+ const route = selectedRoute ?? defaults.currentSelection();
1293
+ const input = JSON.stringify({
1294
+ schema: object.schemaName,
1295
+ name: object.objectName,
1296
+ type: object.objectType,
1297
+ comment: object.comment ?? "",
1298
+ ddl: object.ddl.slice(0, 16e3),
1299
+ columns: object.columns
1300
+ });
1301
+ const messages = [createUserMessage({
1302
+ content: [{ type: "text", text: input }],
1303
+ source: { kind: "plugin", plugin: "dsh-connect" }
1304
+ })];
1305
+ const assembler = new BlockAssembler();
1306
+ const options = deepFreeze({
1307
+ provider: route.provider,
1308
+ model: route.model,
1309
+ messages,
1310
+ system: SYSTEM_PROMPT,
1311
+ temperature: 0.1,
1312
+ maxTokens: 12e3,
1313
+ ...signal === void 0 ? {} : { signal }
1314
+ });
1315
+ for await (const chunk of llm.stream(options)) assembler.push(chunk);
1316
+ if (assembler.finish.kind !== "stop") throw new Error(`model stopped with ${assembler.finish.kind}`);
1317
+ const text = assembler.blocks().filter((block) => block.type === "text").map((block) => block.text).join("\n");
1318
+ let parsed;
1319
+ try {
1320
+ parsed = semanticProfileSchema.parse(parseJson(text));
1321
+ } catch (error) {
1322
+ if (error instanceof z3.ZodError) {
1323
+ throw new MetadataModelOutputError(`AI \u5143\u6570\u636E\u7ED3\u679C\u683C\u5F0F\u65E0\u6548\uFF1A${formatIssues(error)}`, { cause: error });
1324
+ }
1325
+ throw new MetadataModelOutputError("AI \u5143\u6570\u636E\u7ED3\u679C\u4E0D\u662F\u6709\u6548 JSON", { cause: error });
1326
+ }
1327
+ const knownColumns = new Set(object.columns.map((column) => column.name));
1328
+ const parsedColumns = new Map(parsed.columns.filter((column) => knownColumns.has(column.name)).map((column) => [column.name, column]));
1329
+ return {
1330
+ term: parsed.term,
1331
+ description: parsed.description,
1332
+ tags: [...new Set(parsed.tags)],
1333
+ synonyms: [...new Set(parsed.synonyms)],
1334
+ semanticColumns: object.columns.map((column) => {
1335
+ const semantic = parsedColumns.get(column.name);
1336
+ const fallbackColumn = fallback.semanticColumns.find((item) => item.name === column.name);
1337
+ return {
1338
+ name: column.name,
1339
+ term: semantic?.term ?? fallbackColumn?.term ?? column.name,
1340
+ description: semantic?.description ?? fallbackColumn?.description ?? "",
1341
+ synonyms: semantic?.synonyms ?? fallbackColumn?.synonyms ?? [],
1342
+ enums: semantic?.enums ?? fallbackColumn?.enums ?? [],
1343
+ role: normalizeRole(semantic?.role ?? fallbackColumn?.role ?? "unknown", column)
1344
+ };
1345
+ }),
1346
+ confidence: normalizeConfidence(parsed.confidence),
1347
+ confidenceReason: parsed.confidenceReason,
1348
+ temporary: parsed.temporary,
1349
+ modelProvider: route.provider,
1350
+ modelName: route.model,
1351
+ ...route.reasoningEffort === void 0 ? {} : { modelReasoningEffort: route.reasoningEffort },
1352
+ modelAnalyzedAt: Date.now(),
1353
+ modelStatus: "ai"
1354
+ };
1355
+ } catch (error) {
1356
+ if (signal?.aborted) throw error;
1357
+ return {
1358
+ ...fallback,
1359
+ modelStatus: "failed",
1360
+ ...selectedRoute === void 0 ? {} : { modelProvider: selectedRoute.provider, modelName: selectedRoute.model, modelAnalyzedAt: Date.now() },
1361
+ confidenceReason: `${fallback.confidenceReason}; AI enrichment failed: ${safeError(error)}`.slice(0, 500)
1362
+ };
1363
+ }
1364
+ }
1365
+ };
1366
+ var SYSTEM_PROMPT = `You are a database metadata governance specialist. Infer semantic metadata only from the supplied JSON physical metadata. Never invent rows, metrics, or business rules. Return one JSON object with exactly these fields: term, description, tags, columns, confidence, confidenceReason, temporary, synonyms. columns is an array of {name, term, description, synonyms, enums, role} using only physical column names. role must be one of identifier, dimension, measure, time, unknown. Role rules are strict: primary keys, explicit foreign keys, columns with a references object, and obvious identifiers such as *_id, *_key, *_code, *_uuid, *_no, or comments containing ID/\u7F16\u53F7/\u7F16\u7801/\u6807\u8BC6 must be identifier, even when they can also be used for filtering. A foreign-key identifier must never be dimension. Use dimension only for descriptive or categorical attributes such as name, status, type, region, or category. Use measure only for numeric amounts, counts, quantities, rates, or scores; use time only for dates and timestamps. Preserve every physical column in the response. confidence may be either a probability from 0 to 1 or a score from 0 to 100; it will be normalized by the host. Return JSON only, without Markdown.`;
1367
+ function heuristicProfile(object) {
1368
+ const temporary = /(?:^|_)(?:tmp|temp|test|bak|backup|stg|stage|cache)(?:_|$)/i.test(object.objectName);
1369
+ let confidence = temporary ? 35 : 85;
1370
+ const reasons = [];
1371
+ if (temporary) reasons.push("object name indicates temporary, test, staging, backup, or cache data");
1372
+ if (!object.columns.some((column) => column.primaryKey)) {
1373
+ confidence -= 10;
1374
+ reasons.push("no primary key was discovered");
1375
+ }
1376
+ if (!object.comment && object.columns.every((column) => !column.comment)) {
1377
+ confidence -= 10;
1378
+ reasons.push("database comments are absent");
1379
+ }
1380
+ if (object.sample.length === 0) {
1381
+ confidence -= 5;
1382
+ reasons.push("sample data was disabled or unavailable");
1383
+ }
1384
+ return {
1385
+ term: object.comment?.trim() || humanize(object.objectName),
1386
+ description: object.comment?.trim() || `${object.objectType} ${object.schemaName}.${object.objectName}`,
1387
+ tags: [object.objectType, ...object.engine ? [object.engine] : [], ...temporary ? ["temporary"] : []],
1388
+ synonyms: [],
1389
+ semanticColumns: object.columns.map((column) => ({
1390
+ name: column.name,
1391
+ term: column.comment?.trim() || humanize(column.name),
1392
+ description: column.comment?.trim() || `${column.type}${column.nullable ? ", nullable" : ", required"}`,
1393
+ synonyms: [],
1394
+ enums: [],
1395
+ role: fallbackRole(column)
1396
+ })),
1397
+ confidence: Math.max(0, confidence),
1398
+ confidenceReason: reasons.join("; ") || "physical metadata is complete",
1399
+ temporary,
1400
+ modelStatus: "heuristic"
1401
+ };
1402
+ }
1403
+ function humanize(value) {
1404
+ return value.replaceAll(/[_-]+/g, " ").replaceAll(/\s+/g, " ").trim();
1405
+ }
1406
+ function normalizeConfidence(value) {
1407
+ const score = value >= 0 && value <= 1 ? value * 100 : value;
1408
+ return Math.max(0, Math.min(100, Math.round(score)));
1409
+ }
1410
+ function normalizeStringList(value, maxItems, maxLength) {
1411
+ const items = Array.isArray(value) ? value : value !== null && typeof value === "object" ? Object.entries(value).map(([key, item]) => `${key}: ${String(item)}`) : value === void 0 || value === null || value === "" ? [] : [value];
1412
+ return [...new Set(items.map((item) => String(item).trim().slice(0, maxLength)).filter(Boolean))].slice(0, maxItems);
1413
+ }
1414
+ function normalizeRole(role, column) {
1415
+ if (column.primaryKey || column.references !== void 0 || isIdentifierName(column.name) || isIdentifierComment(column.comment)) return "identifier";
1416
+ return role;
1417
+ }
1418
+ function fallbackRole(column) {
1419
+ if (column.primaryKey || column.references !== void 0 || isIdentifierName(column.name) || isIdentifierComment(column.comment)) return "identifier";
1420
+ if (/(?:date|time|timestamp|year|month|day)(?:$|_)/i.test(column.name) || /(?:date|time|timestamp)/i.test(column.type)) return "time";
1421
+ if (/(?:amount|price|cost|total|balance|quantity|count|num|score|points|rate|ratio)(?:$|_)/i.test(column.name) && /(?:int|decimal|numeric|float|double|real)/i.test(column.type)) return "measure";
1422
+ if (/(?:name|status|state|type|category|region|country|city|flag)(?:$|_)/i.test(column.name)) return "dimension";
1423
+ return "unknown";
1424
+ }
1425
+ function isIdentifierName(name2) {
1426
+ return /(?:^|_)(?:id|key|code|uuid|no)(?:$|_)/i.test(name2);
1427
+ }
1428
+ function isIdentifierComment(comment) {
1429
+ return comment !== void 0 && /(?:\bid\b|\bidentifier\b|编号|编码|标识)/i.test(comment);
1430
+ }
1431
+ function formatIssues(error) {
1432
+ return error.issues.slice(0, 3).map((issue) => `${issue.path.join(".")} ${issue.message}`).join("; ");
1433
+ }
1434
+ function parseJson(text) {
1435
+ const trimmed = text.trim().replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/, "");
1436
+ const start = trimmed.indexOf("{");
1437
+ const end = trimmed.lastIndexOf("}");
1438
+ if (start < 0 || end <= start) throw new Error("model returned no JSON object");
1439
+ return JSON.parse(trimmed.slice(start, end + 1));
1440
+ }
1441
+ function safeError(error) {
1442
+ return (error instanceof Error ? error.message : String(error)).replaceAll(/[\r\n]+/g, " ").slice(0, 160);
1443
+ }
1444
+
1445
+ // src/host/metadata/profiler.ts
1446
+ var MetadataProfiler = class {
1447
+ constructor(ctx, store) {
1448
+ this.ctx = ctx;
1449
+ this.store = store;
1450
+ this.enricher = new MetadataEnricher(ctx);
1451
+ }
1452
+ running = /* @__PURE__ */ new Map();
1453
+ enricher;
1454
+ async test(source, signal) {
1455
+ const password = await this.resolvePassword(source);
1456
+ await providerFor(source.type).test(source, password, signal);
1457
+ }
1458
+ async start(sourceId, mode = "incremental") {
1459
+ const source = this.store.dataSource(sourceId);
1460
+ if (source === void 0) throw new Error("Data source not found");
1461
+ if (!source.enabled) throw new Error("Data source is disabled");
1462
+ if (mode === "rebuild-ai" && !source.aiEnrichment) throw new Error("\u8BF7\u5148\u5728\u6570\u636E\u5E93\u8BBE\u7F6E\u4E2D\u5F00\u542F AI \u8BED\u4E49\u589E\u5F3A");
1463
+ if (this.running.has(sourceId)) throw new Error("A metadata scan is already running for this data source");
1464
+ await this.resolvePassword(source);
1465
+ const now = Date.now();
1466
+ const job = {
1467
+ id: randomUUID(),
1468
+ sourceId,
1469
+ status: "queued",
1470
+ mode,
1471
+ total: 0,
1472
+ processed: 0,
1473
+ startedAt: now,
1474
+ updatedAt: now
1475
+ };
1476
+ await this.store.putJob(job);
1477
+ const controller = new AbortController();
1478
+ const done = this.run(source, job, controller.signal).finally(() => {
1479
+ this.running.delete(sourceId);
1480
+ });
1481
+ this.running.set(sourceId, { controller, done });
1482
+ void done.catch(() => {
1483
+ });
1484
+ return job;
1485
+ }
1486
+ async cancel(sourceId) {
1487
+ const active = this.running.get(sourceId);
1488
+ active?.controller.abort("metadata scan cancelled");
1489
+ const current = this.store.jobs(sourceId).find((job) => job.status === "queued" || job.status === "running");
1490
+ if (current !== void 0) await this.store.deleteJob(current.id);
1491
+ return { stopped: true, processed: current?.processed ?? 0, total: current?.total ?? 0 };
1492
+ }
1493
+ async dispose() {
1494
+ const active = [...this.running.values()];
1495
+ for (const job of active) job.controller.abort("dsh-connect is unloading");
1496
+ await Promise.allSettled(active.map((job) => job.done));
1497
+ }
1498
+ async run(source, initial, signal) {
1499
+ const route = source.aiEnrichment ? this.modelRoute() : void 0;
1500
+ let job = { ...initial, status: "running", updatedAt: Date.now() };
1501
+ if (route !== void 0) job = { ...job, modelProvider: route.provider, modelName: route.model };
1502
+ await this.store.putJob(job);
1503
+ try {
1504
+ const password = await this.resolvePassword(source);
1505
+ const objects = await providerFor(source.type).discover(source, password, signal);
1506
+ signal.throwIfAborted();
1507
+ job = { ...job, total: objects.length, updatedAt: Date.now() };
1508
+ await this.store.putJob(job);
1509
+ const keep = /* @__PURE__ */ new Set();
1510
+ for (const object of objects) {
1511
+ signal.throwIfAborted();
1512
+ const id = profileId(source.id, object);
1513
+ keep.add(id);
1514
+ job = { ...job, currentObject: `${object.schemaName}.${object.objectName}`, updatedAt: Date.now() };
1515
+ await this.store.putJob(job);
1516
+ const fingerprint = fingerprintOf(object);
1517
+ const semanticFingerprint = semanticFingerprintOf(fingerprint, source.aiEnrichment, route);
1518
+ const existing = this.store.profile(id);
1519
+ const rebuild = initial.mode === "full" || initial.mode === "rebuild-ai" && source.aiEnrichment;
1520
+ if (rebuild || existing === void 0 || existing.semanticFingerprint !== semanticFingerprint) {
1521
+ const semantic = await this.enricher.enrich(object, source.aiEnrichment, signal, route);
1522
+ signal.throwIfAborted();
1523
+ const profile = {
1524
+ id,
1525
+ sourceId: source.id,
1526
+ schemaName: object.schemaName,
1527
+ objectName: object.objectName,
1528
+ objectType: object.objectType,
1529
+ ...object.engine === void 0 ? {} : { engine: object.engine },
1530
+ ...object.comment === void 0 ? {} : { comment: object.comment },
1531
+ ddl: object.ddl.slice(0, 1e5),
1532
+ columns: object.columns,
1533
+ sample: object.sample,
1534
+ term: semantic.term,
1535
+ description: semantic.description,
1536
+ tags: semantic.tags,
1537
+ synonyms: semantic.synonyms,
1538
+ semanticColumns: semantic.semanticColumns,
1539
+ confidence: semantic.confidence,
1540
+ confidenceReason: semantic.confidenceReason,
1541
+ temporary: semantic.temporary,
1542
+ ignored: existing?.ignoredOverride ?? (semantic.temporary || semantic.confidence < 60),
1543
+ ...existing?.ignoredOverride === void 0 ? {} : { ignoredOverride: existing.ignoredOverride },
1544
+ fingerprint,
1545
+ semanticFingerprint,
1546
+ ...semantic.modelProvider === void 0 ? {} : { modelProvider: semantic.modelProvider },
1547
+ ...semantic.modelName === void 0 ? {} : { modelName: semantic.modelName },
1548
+ ...semantic.modelReasoningEffort === void 0 ? {} : { modelReasoningEffort: semantic.modelReasoningEffort },
1549
+ ...semantic.modelAnalyzedAt === void 0 ? {} : { modelAnalyzedAt: semantic.modelAnalyzedAt },
1550
+ modelStatus: semantic.modelStatus,
1551
+ modelPromptVersion: METADATA_MODEL_PROMPT_VERSION,
1552
+ profiledAt: Date.now()
1553
+ };
1554
+ await this.store.putProfile(profile);
1555
+ await this.store.putChange({
1556
+ id: randomUUID(),
1557
+ sourceId: source.id,
1558
+ profileId: profile.id,
1559
+ action: initial.mode === "rebuild-ai" ? "model" : "scan",
1560
+ summary: initial.mode === "rebuild-ai" ? "AI \u8BED\u4E49\u5EFA\u6A21\u5B8C\u6210" : "\u7269\u7406\u5143\u6570\u636E\u626B\u63CF\u5B8C\u6210",
1561
+ after: { term: profile.term, description: profile.description, modelStatus: profile.modelStatus, confidence: profile.confidence },
1562
+ changedAt: Date.now()
1563
+ });
1564
+ }
1565
+ signal.throwIfAborted();
1566
+ job = { ...job, processed: job.processed + 1, updatedAt: Date.now() };
1567
+ await this.store.putJob(job);
1568
+ }
1569
+ signal.throwIfAborted();
1570
+ await this.store.deleteStaleProfiles(source.id, keep);
1571
+ const { currentObject: _currentObject, ...completedJob } = job;
1572
+ await this.store.putJob({
1573
+ ...completedJob,
1574
+ status: "completed",
1575
+ processed: objects.length,
1576
+ updatedAt: Date.now(),
1577
+ finishedAt: Date.now()
1578
+ });
1579
+ } catch (error) {
1580
+ const cancelled = signal.aborted;
1581
+ if (cancelled) {
1582
+ await this.store.deleteJob(job.id);
1583
+ } else {
1584
+ const { currentObject: _currentObject, ...failedJob } = job;
1585
+ await this.store.putJob({
1586
+ ...failedJob,
1587
+ status: "failed",
1588
+ error: safeError2(error),
1589
+ updatedAt: Date.now(),
1590
+ finishedAt: Date.now()
1591
+ });
1592
+ }
1593
+ }
1594
+ }
1595
+ async resolvePassword(source) {
1596
+ const resolved = await this.ctx.credentials.resolve(credentialRef2(source.credentialRef));
1597
+ if (resolved === void 0) throw new Error("Database credential is not configured");
1598
+ return resolved.value;
1599
+ }
1600
+ modelRoute() {
1601
+ const defaults = this.ctx.get("agentDefaultModel");
1602
+ if (defaults === void 0) return void 0;
1603
+ try {
1604
+ return defaults.currentSelection();
1605
+ } catch {
1606
+ return void 0;
1607
+ }
1608
+ }
1609
+ };
1610
+ function profileId(sourceId, object) {
1611
+ return `${sourceId}:${object.schemaName}:${object.objectName}`;
1612
+ }
1613
+ function fingerprintOf(object) {
1614
+ return createHash("sha256").update(JSON.stringify({
1615
+ schemaName: object.schemaName,
1616
+ objectName: object.objectName,
1617
+ objectType: object.objectType,
1618
+ engine: object.engine,
1619
+ comment: object.comment,
1620
+ ddl: object.ddl,
1621
+ columns: object.columns,
1622
+ sample: object.sample
1623
+ })).digest("hex");
1624
+ }
1625
+ function semanticFingerprintOf(physicalFingerprint, aiEnabled, route) {
1626
+ return createHash("sha256").update(JSON.stringify({
1627
+ physicalFingerprint,
1628
+ aiEnabled,
1629
+ promptVersion: METADATA_MODEL_PROMPT_VERSION,
1630
+ provider: aiEnabled ? route?.provider : void 0,
1631
+ model: aiEnabled ? route?.model : void 0,
1632
+ reasoningEffort: aiEnabled ? route?.reasoningEffort : void 0
1633
+ })).digest("hex");
1634
+ }
1635
+ function safeError2(error) {
1636
+ return (error instanceof Error ? error.message : String(error)).replaceAll(/[\r\n]+/g, " ").slice(0, 1e3);
1637
+ }
1638
+
1639
+ // src/host/metadata/tools.ts
1640
+ import { defineTool as defineTool2 } from "@deepseek-ai/dsh-tools";
1641
+ import { credentialRef as credentialRef3 } from "@deepseek-ai/dsh-credentials";
1642
+
1643
+ // src/host/metadata/governance.ts
1644
+ function recommendMetrics(profiles, sourceId, existing) {
1645
+ const result = [];
1646
+ const known = new Set(existing.filter((metric) => metric.sourceId === sourceId).map((metric) => `${metric.profileId}:${metric.columnName}:${metric.aggregation}`));
1647
+ for (const profile of profiles) {
1648
+ if (profile.sourceId !== sourceId || profile.ignored) continue;
1649
+ const identifier = profile.columns.find((column) => column.primaryKey);
1650
+ if (!known.has(`${profile.id}::count`)) {
1651
+ result.push({
1652
+ profileId: profile.id,
1653
+ name: `${profile.objectName}_count`,
1654
+ term: `${profile.term || profile.objectName}\u6570\u91CF`,
1655
+ description: `\u7EDF\u8BA1${profile.term || profile.objectName}\u8BB0\u5F55\u6570`,
1656
+ aggregation: "count",
1657
+ columnName: "",
1658
+ expression: "COUNT(*)",
1659
+ unit: "\u6761",
1660
+ reason: "\u6BCF\u4E2A\u4E1A\u52A1\u8868\u90FD\u53EF\u63D0\u4F9B\u8BB0\u5F55\u6570\u6307\u6807"
1661
+ });
1662
+ }
1663
+ for (const column of profile.columns) {
1664
+ if (column.primaryKey || !isNumeric(column.type) || known.has(`${profile.id}:${column.name}:sum`)) continue;
1665
+ const label = profile.semanticColumns.find((item) => item.name === column.name)?.term || column.comment || column.name;
1666
+ const lower = column.name.toLowerCase();
1667
+ const aggregation = /(?:amount|price|cost|total|balance|quantity|count|num|score|points|金额|数量|余额|积分)/i.test(lower) ? "sum" : "avg";
1668
+ result.push({
1669
+ profileId: profile.id,
1670
+ name: `${profile.objectName}_${column.name}_${aggregation}`,
1671
+ term: `${profile.term || profile.objectName}${label}${aggregation === "sum" ? "\u5408\u8BA1" : "\u5E73\u5747\u503C"}`,
1672
+ description: `${aggregation === "sum" ? "\u6C47\u603B" : "\u8BA1\u7B97\u5E73\u5747"}\u5B57\u6BB5 ${label}`,
1673
+ aggregation,
1674
+ columnName: column.name,
1675
+ expression: `${aggregation.toUpperCase()}(${quoteIdentifier(column.name)})`,
1676
+ unit: "",
1677
+ reason: `\u5B57\u6BB5\u7C7B\u578B ${column.type} \u9002\u5408${aggregation === "sum" ? "\u6C42\u548C" : "\u6C42\u5E73\u5747"}`
1678
+ });
1679
+ if (result.length >= 50) return result;
1680
+ }
1681
+ if (identifier === void 0 && result.length >= 50) break;
1682
+ }
1683
+ return result.slice(0, 50);
1684
+ }
1685
+ function metadataYaml(profiles, metrics, sourceId) {
1686
+ const lines = ["version: 1", `sourceId: ${yamlScalar(sourceId)}`, "tables:"];
1687
+ for (const profile of profiles.filter((item) => item.sourceId === sourceId)) {
1688
+ lines.push(` - id: ${yamlScalar(profile.id)}`, ` name: ${yamlScalar(`${profile.schemaName}.${profile.objectName}`)}`, ` type: ${yamlScalar(profile.objectType)}`, ` term: ${yamlScalar(profile.term)}`, ` description: ${yamlScalar(profile.description)}`, ` synonyms: ${yamlList(profile.synonyms)}`, ` tags: ${yamlList(profile.tags)}`, ` ignored: ${String(profile.ignored)}`, " columns:");
1689
+ for (const column of profile.columns) {
1690
+ const semantic = profile.semanticColumns.find((item) => item.name === column.name);
1691
+ lines.push(` - name: ${yamlScalar(column.name)}`, ` type: ${yamlScalar(column.type)}`, ` nullable: ${String(column.nullable)}`, ` primaryKey: ${String(column.primaryKey)}`, ` term: ${yamlScalar(semantic?.term ?? column.comment ?? "")}`, ` description: ${yamlScalar(semantic?.description ?? "")}`, ` synonyms: ${yamlList(semantic?.synonyms ?? [])}`, ` enums: ${yamlList(semantic?.enums ?? [])}`);
1692
+ }
1693
+ }
1694
+ lines.push("metrics:");
1695
+ for (const metric of metrics.filter((item) => item.sourceId === sourceId)) {
1696
+ lines.push(` - id: ${yamlScalar(metric.id)}`, ` name: ${yamlScalar(metric.name)}`, ` term: ${yamlScalar(metric.term)}`, ` profileId: ${yamlScalar(metric.profileId)}`, ` aggregation: ${yamlScalar(metric.aggregation)}`, ` columnName: ${yamlScalar(metric.columnName)}`, ` expression: ${yamlScalar(metric.expression)}`, ` description: ${yamlScalar(metric.description)}`);
1697
+ }
1698
+ return `${lines.join("\n")}
1699
+ `;
1700
+ }
1701
+ function isNumeric(type) {
1702
+ return /(?:int|decimal|numeric|number|float|double|real|money|serial)/i.test(type);
1703
+ }
1704
+ function quoteIdentifier(value) {
1705
+ return `"${value.replaceAll('"', '""')}"`;
1706
+ }
1707
+ function yamlScalar(value) {
1708
+ return JSON.stringify(value ?? "");
1709
+ }
1710
+ function yamlList(values) {
1711
+ return `[${values.map(yamlScalar).join(", ")}]`;
1712
+ }
1713
+
1714
+ // src/host/metadata/tools.ts
1715
+ var jsonOutput = {
1716
+ schema: { type: "json" },
1717
+ render: (_args, value) => [{ type: "text", text: JSON.stringify(value, null, 2) }]
1718
+ };
1719
+ function registerMetadataTools(ctx, store) {
1720
+ return [
1721
+ ctx.tools.register(defineTool2({
1722
+ name: "dsh_connect_list_data_sources",
1723
+ description: "List configured data sources that have metadata available. This exposes connection names and metadata counts, never credentials or host addresses.",
1724
+ parameters: {},
1725
+ output: jsonOutput,
1726
+ isConcurrencySafe: () => true,
1727
+ async execute() {
1728
+ const views = await store.dataSourceViews();
1729
+ return views.filter((source) => source.enabled).map((source) => ({
1730
+ id: source.id,
1731
+ name: source.name,
1732
+ type: source.type,
1733
+ database: source.database,
1734
+ profileCount: source.profileCount,
1735
+ lastScanStatus: source.latestJob?.status ?? "never-scanned"
1736
+ }));
1737
+ },
1738
+ presentCall: () => ({ card: "generic", title: "List data sources", kind: "read" })
1739
+ })),
1740
+ ctx.tools.register(defineTool2({
1741
+ name: "dsh_connect_search_metadata",
1742
+ description: "Search recognized table, view, and column metadata. Use this to understand available data structures; it does not query business rows or execute SQL.",
1743
+ parameters: {
1744
+ query: { type: "string", required: true, description: "Name, semantic term, description, tag, or column text to search." },
1745
+ sourceId: { type: "string", description: "Optional exact data source id from dsh_connect_list_data_sources." },
1746
+ limit: { type: "integer", description: "Optional result limit from 1 to 20. Default 10." }
1747
+ },
1748
+ output: jsonOutput,
1749
+ isConcurrencySafe: () => true,
1750
+ execute(args) {
1751
+ const query = args.query.trim().toLocaleLowerCase();
1752
+ if (query === "") throw new Error("query must not be blank");
1753
+ const limit = Math.min(20, Math.max(1, args.limit ?? 10));
1754
+ const results = store.profiles(args.sourceId).filter((profile) => {
1755
+ const haystack = [
1756
+ profile.schemaName,
1757
+ profile.objectName,
1758
+ profile.term,
1759
+ profile.description,
1760
+ profile.tags.join(" "),
1761
+ profile.columns.map((column) => `${column.name} ${column.comment ?? ""}`).join(" "),
1762
+ profile.semanticColumns.map((column) => `${column.name} ${column.term} ${column.description}`).join(" ")
1763
+ ].join(" ").toLocaleLowerCase();
1764
+ return haystack.includes(query);
1765
+ }).slice(0, limit);
1766
+ return Promise.resolve(results.map((profile) => ({
1767
+ id: profile.id,
1768
+ sourceId: profile.sourceId,
1769
+ schema: profile.schemaName,
1770
+ name: profile.objectName,
1771
+ type: profile.objectType,
1772
+ term: profile.term,
1773
+ description: profile.description,
1774
+ tags: profile.tags,
1775
+ confidence: profile.confidence,
1776
+ temporary: profile.temporary,
1777
+ ignored: profile.ignored,
1778
+ columns: profile.semanticColumns.map((column) => ({ name: column.name, term: column.term }))
1779
+ })));
1780
+ },
1781
+ presentCall: (args) => ({ card: "generic", title: `Search metadata: ${args.query}`, kind: "search" })
1782
+ })),
1783
+ ctx.tools.register(defineTool2({
1784
+ name: "dsh_connect_get_table_metadata",
1785
+ description: "Read one recognized table or view metadata profile by the exact profile id returned by dsh_connect_search_metadata. It returns schema and semantic metadata, never sample rows.",
1786
+ parameters: {
1787
+ profileId: { type: "string", required: true, description: "Exact metadata profile id." }
1788
+ },
1789
+ output: jsonOutput,
1790
+ isConcurrencySafe: () => true,
1791
+ execute(args) {
1792
+ const profile = store.profile(args.profileId);
1793
+ if (profile === void 0) throw new Error("Metadata profile not found");
1794
+ return Promise.resolve({
1795
+ id: profile.id,
1796
+ sourceId: profile.sourceId,
1797
+ schema: profile.schemaName,
1798
+ name: profile.objectName,
1799
+ type: profile.objectType,
1800
+ ...profile.engine === void 0 ? {} : { engine: profile.engine },
1801
+ ...profile.comment === void 0 ? {} : { databaseComment: profile.comment },
1802
+ ddl: profile.ddl,
1803
+ columns: profile.columns.map((column) => {
1804
+ const semantic = profile.semanticColumns.find((item) => item.name === column.name);
1805
+ return {
1806
+ name: column.name,
1807
+ type: column.type,
1808
+ nullable: column.nullable,
1809
+ primaryKey: column.primaryKey,
1810
+ ...column.defaultValue === void 0 ? {} : { defaultValue: column.defaultValue },
1811
+ ...column.comment === void 0 ? {} : { comment: column.comment },
1812
+ ...column.references === void 0 ? {} : { references: column.references },
1813
+ ...semantic === void 0 ? {} : { semantic }
1814
+ };
1815
+ }),
1816
+ term: profile.term,
1817
+ description: profile.description,
1818
+ tags: profile.tags,
1819
+ confidence: profile.confidence,
1820
+ confidenceReason: profile.confidenceReason,
1821
+ temporary: profile.temporary,
1822
+ ignored: profile.ignored,
1823
+ profiledAt: profile.profiledAt
1824
+ });
1825
+ },
1826
+ presentCall: () => ({ card: "generic", title: "Read table metadata", kind: "read" })
1827
+ })),
1828
+ ctx.tools.register(defineTool2({
1829
+ name: "dsh_connect_query_data_source",
1830
+ description: "Execute a bounded read-only SELECT or WITH query against a configured data source. Never use this for writes or schema changes.",
1831
+ parameters: {
1832
+ sourceId: { type: "string", required: true, description: "Exact data source id." },
1833
+ sql: { type: "string", required: true, description: "One SELECT or WITH query. No semicolon or comments." },
1834
+ limit: { type: "integer", description: "Maximum rows, 1 to 1000. Default 100." }
1835
+ },
1836
+ output: jsonOutput,
1837
+ isConcurrencySafe: () => false,
1838
+ async execute(args, exec) {
1839
+ const source = store.dataSource(args.sourceId);
1840
+ if (source === void 0) throw new Error("Data source not found");
1841
+ if (!source.enabled) throw new Error("Data source is disabled");
1842
+ const password = await ctx.credentials.resolve(credentialRef3(source.credentialRef));
1843
+ if (password === void 0) throw new Error("Database credential is not configured");
1844
+ return JSON.parse(JSON.stringify(await providerFor(source.type).query(source, password.value, args.sql, queryLimit(args.limit), exec.signal)));
1845
+ },
1846
+ presentCall: (args) => ({ card: "generic", title: `Query ${args.sourceId}`, kind: "read" })
1847
+ })),
1848
+ ctx.tools.register(defineTool2({
1849
+ name: "dsh_connect_export_metadata_yaml",
1850
+ description: "Export recognized metadata and metrics as YAML text for review or version control.",
1851
+ parameters: { sourceId: { type: "string", required: true } },
1852
+ output: { schema: { type: "string" }, render: (_args, value) => [{ type: "text", text: String(value) }] },
1853
+ isConcurrencySafe: () => true,
1854
+ async execute(args) {
1855
+ return metadataYaml(store.profiles(args.sourceId), store.metrics(args.sourceId), args.sourceId);
1856
+ },
1857
+ presentCall: () => ({ card: "generic", title: "Export metadata YAML", kind: "read" })
1858
+ }))
1859
+ ];
1860
+ }
1861
+
1862
+ // src/host/rpc.ts
1863
+ import { randomUUID as randomUUID2 } from "node:crypto";
1864
+ import { credentialRef as credentialRef4 } from "@deepseek-ai/dsh-credentials";
1865
+ import { z as z4 } from "zod";
1866
+ var idInput = z4.object({ id: z4.string().uuid() });
1867
+ var sourceInput = z4.object({ sourceId: z4.string().uuid() });
1868
+ var scanInput = z4.object({ sourceId: z4.string().uuid(), mode: z4.enum(["incremental", "rebuild-ai", "full"]).default("incremental") });
1869
+ var metadataListInput = z4.object({ sourceId: z4.string().uuid().optional() });
1870
+ var profileInput = z4.object({ id: z4.string().min(1) });
1871
+ var apiTestInput = z4.object({ id: z4.string().uuid(), args: z4.record(z4.string(), z4.unknown()).default({}) });
1872
+ var queryInput = z4.object({ sourceId: z4.string().uuid(), sql: z4.string().min(1).max(2e4), limit: z4.number().int().min(1).max(1e3).default(100) });
1873
+ var metricsListInput = z4.object({ sourceId: z4.string().uuid() });
1874
+ var metricSaveInput = z4.object({
1875
+ id: z4.string().uuid().optional(),
1876
+ sourceId: z4.string().uuid(),
1877
+ profileId: z4.string().min(1),
1878
+ name: z4.string().min(1).max(120),
1879
+ term: z4.string().min(1).max(120),
1880
+ description: z4.string().max(500).default(""),
1881
+ aggregation: z4.enum(["count", "sum", "avg", "min", "max", "formula"]),
1882
+ columnName: z4.string().max(255).default(""),
1883
+ expression: z4.string().max(2e3),
1884
+ unit: z4.string().max(50).default(""),
1885
+ tags: z4.array(z4.string().max(50)).max(20).default([]),
1886
+ enabled: z4.boolean().default(true)
1887
+ });
1888
+ var metricBatchSaveInput = z4.object({ metrics: z4.array(metricSaveInput).min(1).max(5e3) });
1889
+ var profileUpdateInput = z4.object({
1890
+ id: z4.string().min(1),
1891
+ term: z4.string().max(100).optional(),
1892
+ description: z4.string().max(500).optional(),
1893
+ tags: z4.array(z4.string().max(50)).max(20).optional(),
1894
+ synonyms: z4.array(z4.string().max(100)).max(30).optional(),
1895
+ ignored: z4.boolean().optional(),
1896
+ semanticColumns: z4.array(z4.object({ name: z4.string(), term: z4.string().max(100), description: z4.string().max(300), synonyms: z4.array(z4.string().max(100)).max(20).default([]), enums: z4.array(z4.string().max(100)).max(50).default([]), role: z4.enum(["identifier", "dimension", "measure", "time", "unknown"]).default("unknown") })).optional()
1897
+ });
1898
+ var saveDataSourceInput = z4.object({
1899
+ id: z4.string().uuid().optional(),
1900
+ name: z4.string().min(1).max(100),
1901
+ type: databaseTypeSchema,
1902
+ host: z4.string().min(1).max(255),
1903
+ port: z4.number().int().min(1).max(65535),
1904
+ database: z4.string().min(1).max(255),
1905
+ username: z4.string().min(1).max(255),
1906
+ secret: z4.string().min(1).optional(),
1907
+ schemaInclude: z4.array(z4.string().min(1).max(255)).max(100).default([]),
1908
+ tls: z4.boolean().default(false),
1909
+ sampleRows: z4.number().int().min(0).max(3).default(0),
1910
+ aiEnrichment: z4.boolean().default(false),
1911
+ enabled: z4.boolean().default(true)
1912
+ });
1913
+ var saveAuthInput = z4.discriminatedUnion("type", [
1914
+ z4.object({ type: z4.literal("none") }),
1915
+ z4.object({ type: z4.literal("bearer") }),
1916
+ z4.object({ type: z4.literal("api-key"), location: z4.enum(["header", "query"]), name: z4.string().min(1).max(100) }),
1917
+ z4.object({ type: z4.literal("basic"), username: z4.string().min(1).max(200) })
1918
+ ]);
1919
+ var saveApiInput = z4.object({
1920
+ id: z4.string().uuid().optional(),
1921
+ name: z4.string().min(1).max(100),
1922
+ slug: z4.string().min(2).max(48).regex(/^[a-z][a-z0-9_]*$/),
1923
+ description: z4.string().min(1).max(1e3),
1924
+ method: apiMethodSchema,
1925
+ baseUrl: z4.string().url().max(2048),
1926
+ pathTemplate: z4.string().min(1).max(2048).default("/"),
1927
+ parameters: z4.array(apiParameterSchema).max(50).default([]),
1928
+ auth: saveAuthInput,
1929
+ secret: z4.string().min(1).optional(),
1930
+ timeoutMs: z4.number().int().min(1e3).max(12e4).default(3e4),
1931
+ maxResponseBytes: z4.number().int().min(1024).max(2 * 1024 * 1024).default(131072),
1932
+ responsePointer: z4.string().max(500).default(""),
1933
+ allowPrivateNetwork: z4.boolean().default(false),
1934
+ enabled: z4.boolean().default(true)
1935
+ });
1936
+ var ConnectRpc = class {
1937
+ constructor(ctx, store, profiler, tools) {
1938
+ this.ctx = ctx;
1939
+ this.store = store;
1940
+ this.profiler = profiler;
1941
+ this.tools = tools;
1942
+ this.executor = new SecureHttpExecutor(ctx);
1943
+ }
1944
+ executor;
1945
+ async handle(endpoint, payload, signal) {
1946
+ try {
1947
+ switch (endpoint) {
1948
+ case "sources/list":
1949
+ return ok(await this.store.dataSourceViews());
1950
+ case "sources/save":
1951
+ return ok(await this.saveDataSource(saveDataSourceInput.parse(payload)));
1952
+ case "sources/delete":
1953
+ return ok(await this.deleteDataSource(idInput.parse(payload).id));
1954
+ case "sources/test": {
1955
+ const source = this.requireSource(idInput.parse(payload).id);
1956
+ await this.profiler.test(source, signal);
1957
+ return ok({ connected: true });
1958
+ }
1959
+ case "sources/scan": {
1960
+ const input = scanInput.parse(payload);
1961
+ return ok(await this.profiler.start(input.sourceId, input.mode));
1962
+ }
1963
+ case "sources/cancel": {
1964
+ return ok(await this.profiler.cancel(sourceInput.parse(payload).sourceId));
1965
+ }
1966
+ case "sources/query": {
1967
+ const input = queryInput.parse(payload);
1968
+ const source = this.requireSource(input.sourceId);
1969
+ if (!source.enabled) throw new Error("Data source is disabled");
1970
+ const password = await this.ctx.credentials.resolve(credentialRef4(source.credentialRef));
1971
+ if (password === void 0) throw new Error("Database credential is not configured");
1972
+ return ok(await providerFor(source.type).query(source, password.value, input.sql, input.limit, signal));
1973
+ }
1974
+ case "jobs/list":
1975
+ return ok(this.store.jobs(sourceInput.parse(payload).sourceId));
1976
+ case "metadata/list":
1977
+ return ok(this.store.profiles(metadataListInput.parse(payload).sourceId));
1978
+ case "metadata/get": {
1979
+ const profile = this.store.profile(profileInput.parse(payload).id);
1980
+ if (profile === void 0) throw new Error("Metadata profile not found");
1981
+ return ok(profile);
1982
+ }
1983
+ case "metadata/update":
1984
+ return ok(await this.updateMetadata(profileUpdateInput.parse(payload)));
1985
+ case "metadata/yaml": {
1986
+ const input = sourceInput.parse(payload);
1987
+ return ok(metadataYaml(this.store.profiles(input.sourceId), this.store.metrics(input.sourceId), input.sourceId));
1988
+ }
1989
+ case "metadata/changes": {
1990
+ const input = z4.object({
1991
+ sourceId: z4.string().uuid(),
1992
+ profileId: z4.string().min(1).optional(),
1993
+ offset: z4.number().int().min(0).default(0),
1994
+ limit: z4.number().int().min(1).max(100).default(20)
1995
+ }).parse(payload);
1996
+ return ok(this.store.changePage(input.sourceId, input.profileId, input.offset, input.limit));
1997
+ }
1998
+ case "metrics/list":
1999
+ return ok(this.store.metrics(metricsListInput.parse(payload).sourceId));
2000
+ case "metrics/recommend": {
2001
+ const input = metricsListInput.parse(payload);
2002
+ return ok(recommendMetrics(this.store.profiles(input.sourceId), input.sourceId, this.store.metrics(input.sourceId)));
2003
+ }
2004
+ case "metrics/save":
2005
+ return ok(await this.saveMetric(metricSaveInput.parse(payload)));
2006
+ case "metrics/save-batch": {
2007
+ const input = metricBatchSaveInput.parse(payload);
2008
+ const saved = [];
2009
+ for (const metric of input.metrics) saved.push(await this.saveMetric(metric));
2010
+ return ok({ saved: saved.length });
2011
+ }
2012
+ case "metrics/delete": {
2013
+ const metric = this.store.metric(idInput.parse(payload).id);
2014
+ if (metric === void 0) throw new Error("Metric not found");
2015
+ await this.store.deleteMetric(metric.id);
2016
+ await this.store.putChange({ id: randomUUID2(), sourceId: metric.sourceId, profileId: metric.profileId, action: "update", summary: `\u5220\u9664\u6307\u6807\uFF1A${metric.term}`, before: { metric }, changedAt: Date.now() });
2017
+ return ok({ deleted: true });
2018
+ }
2019
+ case "apis/list":
2020
+ return ok(await this.store.apiDefinitionViews());
2021
+ case "apis/save":
2022
+ return ok(await this.saveApi(saveApiInput.parse(payload)));
2023
+ case "apis/delete":
2024
+ return ok(await this.deleteApi(idInput.parse(payload).id));
2025
+ case "apis/test": {
2026
+ const input = apiTestInput.parse(payload);
2027
+ const definition = this.requireApi(input.id);
2028
+ return ok(await this.executor.execute(definition, input.args, signal));
2029
+ }
2030
+ default:
2031
+ return badRequest(`Unknown dsh-connect endpoint: ${endpoint}`);
2032
+ }
2033
+ } catch (error) {
2034
+ if (signal.aborted) return { ok: false, error: { code: "cancelled", message: "Request cancelled", details: {} } };
2035
+ if (error instanceof z4.ZodError) return badRequest("Invalid dsh-connect request", error.issues);
2036
+ return { ok: false, error: { code: "internal", message: safeError3(error), details: {} } };
2037
+ }
2038
+ }
2039
+ validateConversationApi(input) {
2040
+ const prepared = this.prepareConversationApi(input);
2041
+ if (this.store.apiDefinitions().some((api) => api.slug === prepared.slug)) {
2042
+ throw new Error(`API slug already exists: ${prepared.slug}`);
2043
+ }
2044
+ const baseUrl = parseConfiguredBaseUrl(prepared.baseUrl);
2045
+ if (baseUrl.search !== "") throw new Error("Base URL query strings are not allowed; declare query parameters instead");
2046
+ if (prepared.pathTemplate.includes("?")) throw new Error("Path template query strings are not allowed; declare query parameters instead");
2047
+ validateApiInput(prepared);
2048
+ }
2049
+ async createConversationApi(input) {
2050
+ const prepared = this.prepareConversationApi(input);
2051
+ this.validateConversationApi(input);
2052
+ return this.saveApi(prepared, { allowMissingCredential: true });
2053
+ }
2054
+ async saveDataSource(input) {
2055
+ const existing = input.id === void 0 ? void 0 : this.store.dataSource(input.id);
2056
+ if (input.id !== void 0 && existing === void 0) throw new Error("Data source not found");
2057
+ const duplicate = this.store.dataSources().find((source) => source.name === input.name && source.id !== input.id);
2058
+ if (duplicate !== void 0) throw new Error(`Data source name already exists: ${input.name}`);
2059
+ const id = existing?.id ?? randomUUID2();
2060
+ const credentialName = existing?.credentialRef ?? databaseCredentialRef(id);
2061
+ if (existing === void 0 && input.secret === void 0) throw new Error("Password is required for a new data source");
2062
+ if (input.secret !== void 0) await this.ctx.credentials.set(credentialRef4(credentialName), input.secret);
2063
+ const now = Date.now();
2064
+ const definition = dataSourceSchema.parse({
2065
+ id,
2066
+ name: input.name.trim(),
2067
+ type: input.type,
2068
+ host: input.host.trim(),
2069
+ port: input.port,
2070
+ database: input.database.trim(),
2071
+ username: input.username.trim(),
2072
+ credentialRef: credentialName,
2073
+ schemaInclude: input.schemaInclude.map((value) => value.trim()).filter(Boolean),
2074
+ tls: input.tls,
2075
+ sampleRows: input.sampleRows,
2076
+ aiEnrichment: input.aiEnrichment,
2077
+ enabled: input.enabled,
2078
+ createdAt: existing?.createdAt ?? now,
2079
+ updatedAt: now
2080
+ });
2081
+ await this.store.putDataSource(definition);
2082
+ return (await this.store.dataSourceViews()).find((source) => source.id === id);
2083
+ }
2084
+ async deleteDataSource(id) {
2085
+ const existing = this.requireSource(id);
2086
+ await this.profiler.cancel(id);
2087
+ await this.store.deleteDataSource(id);
2088
+ await this.unsetIfWritable(existing.credentialRef);
2089
+ return { deleted: true };
2090
+ }
2091
+ async saveApi(input, options = {}) {
2092
+ const existing = input.id === void 0 ? void 0 : this.store.apiDefinition(input.id);
2093
+ if (input.id !== void 0 && existing === void 0) throw new Error("API definition not found");
2094
+ const duplicate = this.store.apiDefinitions().find((api) => api.slug === input.slug && api.id !== input.id);
2095
+ if (duplicate !== void 0) throw new Error(`API slug already exists: ${input.slug}`);
2096
+ validateApiInput(input);
2097
+ const id = existing?.id ?? randomUUID2();
2098
+ const nextAuth = await this.resolveApiAuth(id, input.auth, input.secret, existing?.auth, options.allowMissingCredential ?? false);
2099
+ const now = Date.now();
2100
+ const definition = apiDefinitionSchema.parse({
2101
+ id,
2102
+ name: input.name.trim(),
2103
+ slug: input.slug,
2104
+ description: input.description.trim(),
2105
+ method: input.method,
2106
+ baseUrl: input.baseUrl,
2107
+ pathTemplate: input.pathTemplate,
2108
+ parameters: input.parameters,
2109
+ auth: nextAuth,
2110
+ timeoutMs: input.timeoutMs,
2111
+ maxResponseBytes: input.maxResponseBytes,
2112
+ responsePointer: input.responsePointer,
2113
+ allowPrivateNetwork: input.allowPrivateNetwork,
2114
+ enabled: input.enabled,
2115
+ createdAt: existing?.createdAt ?? now,
2116
+ updatedAt: now
2117
+ });
2118
+ await this.store.putApiDefinition(definition);
2119
+ try {
2120
+ this.tools.replace(this.store.apiDefinitions());
2121
+ } catch (error) {
2122
+ if (existing === void 0) await this.store.deleteApiDefinition(id);
2123
+ else await this.store.putApiDefinition(existing);
2124
+ this.tools.replace(this.store.apiDefinitions());
2125
+ throw error;
2126
+ }
2127
+ if (existing !== void 0 && existing.auth.type !== "none" && nextAuth.type === "none") {
2128
+ await this.unsetIfWritable(existing.auth.credentialRef);
2129
+ }
2130
+ return (await this.store.apiDefinitionViews()).find((api) => api.id === id);
2131
+ }
2132
+ async deleteApi(id) {
2133
+ const existing = this.requireApi(id);
2134
+ await this.store.deleteApiDefinition(id);
2135
+ this.tools.replace(this.store.apiDefinitions());
2136
+ if (existing.auth.type !== "none") await this.unsetIfWritable(existing.auth.credentialRef);
2137
+ return { deleted: true };
2138
+ }
2139
+ async resolveApiAuth(id, input, secret, existing, allowMissingCredential) {
2140
+ if (input.type === "none") return { type: "none" };
2141
+ const ref = existing?.type === "none" || existing === void 0 ? apiCredentialRef(id) : existing.credentialRef;
2142
+ const configured = (await this.ctx.credentials.describe(credentialRef4(ref))).configured;
2143
+ if (!configured && secret === void 0 && !allowMissingCredential) throw new Error("Credential is required for this authentication method");
2144
+ if (secret !== void 0) await this.ctx.credentials.set(credentialRef4(ref), secret);
2145
+ if (input.type === "bearer") return { type: "bearer", credentialRef: ref };
2146
+ if (input.type === "basic") return { type: "basic", credentialRef: ref, username: input.username };
2147
+ return { type: "api-key", credentialRef: ref, location: input.location, name: input.name };
2148
+ }
2149
+ prepareConversationApi(input) {
2150
+ const auth = input.authType === "none" ? { type: "none" } : input.authType === "bearer" ? { type: "bearer" } : input.authType === "basic" ? { type: "basic", username: input.basicUsername ?? "" } : { type: "api-key", location: input.authLocation ?? "header", name: input.authName ?? "" };
2151
+ return saveApiInput.parse({
2152
+ name: input.name,
2153
+ slug: input.slug,
2154
+ description: input.description,
2155
+ method: input.method,
2156
+ baseUrl: input.baseUrl,
2157
+ pathTemplate: input.pathTemplate,
2158
+ parameters: input.parameters,
2159
+ auth,
2160
+ timeoutMs: input.timeoutMs,
2161
+ maxResponseBytes: input.maxResponseBytes,
2162
+ responsePointer: input.responsePointer,
2163
+ allowPrivateNetwork: false,
2164
+ enabled: input.authType === "none"
2165
+ });
2166
+ }
2167
+ requireSource(id) {
2168
+ const source = this.store.dataSource(id);
2169
+ if (source === void 0) throw new Error("Data source not found");
2170
+ return source;
2171
+ }
2172
+ requireApi(id) {
2173
+ const definition = this.store.apiDefinition(id);
2174
+ if (definition === void 0) throw new Error("API definition not found");
2175
+ return definition;
2176
+ }
2177
+ async updateMetadata(input) {
2178
+ const existing = this.store.profile(input.id);
2179
+ if (existing === void 0) throw new Error("Metadata profile not found");
2180
+ const next = {
2181
+ ...existing,
2182
+ ...input.term === void 0 ? {} : { term: input.term.trim() },
2183
+ ...input.description === void 0 ? {} : { description: input.description.trim() },
2184
+ ...input.tags === void 0 ? {} : { tags: [...new Set(input.tags.map((tag) => tag.trim()).filter(Boolean))] },
2185
+ ...input.synonyms === void 0 ? {} : { synonyms: [...new Set(input.synonyms.map((value) => value.trim()).filter(Boolean))] },
2186
+ ...input.ignored === void 0 ? {} : { ignored: input.ignored, ignoredOverride: input.ignored },
2187
+ ...input.semanticColumns === void 0 ? {} : { semanticColumns: input.semanticColumns },
2188
+ editedAt: Date.now()
2189
+ };
2190
+ await this.store.putProfile(next);
2191
+ await this.store.putChange({ id: randomUUID2(), sourceId: next.sourceId, profileId: next.id, action: "update", summary: "\u624B\u52A8\u66F4\u65B0\u5143\u6570\u636E", before: { term: existing.term, description: existing.description, tags: existing.tags, synonyms: existing.synonyms, ignored: existing.ignored }, after: { term: next.term, description: next.description, tags: next.tags, synonyms: next.synonyms, ignored: next.ignored }, changedAt: Date.now() });
2192
+ return next;
2193
+ }
2194
+ async saveMetric(input) {
2195
+ const existing = input.id === void 0 ? void 0 : this.store.metric(input.id);
2196
+ if (input.id !== void 0 && existing === void 0) throw new Error("Metric not found");
2197
+ if (this.store.profile(input.profileId)?.sourceId !== input.sourceId) throw new Error("Metric profile does not belong to this data source");
2198
+ if (this.store.metrics(input.sourceId).some((metric2) => metric2.name === input.name && metric2.id !== input.id)) throw new Error(`Metric name already exists: ${input.name}`);
2199
+ const now = Date.now();
2200
+ const metric = {
2201
+ ...input,
2202
+ id: existing?.id ?? randomUUID2(),
2203
+ createdAt: existing?.createdAt ?? now,
2204
+ updatedAt: now
2205
+ };
2206
+ await this.store.putMetric(metric);
2207
+ await this.store.putChange({ id: randomUUID2(), sourceId: metric.sourceId, profileId: metric.profileId, action: "update", summary: `${existing === void 0 ? "\u521B\u5EFA" : "\u66F4\u65B0"}\u6307\u6807\uFF1A${metric.term}`, ...existing === void 0 ? {} : { before: { metric: existing } }, after: { metric }, changedAt: Date.now() });
2208
+ return metric;
2209
+ }
2210
+ async unsetIfWritable(ref) {
2211
+ const info = await this.ctx.credentials.describe(credentialRef4(ref));
2212
+ if (info.configured && info.writable) await this.ctx.credentials.unset(credentialRef4(ref));
2213
+ }
2214
+ };
2215
+ function validateApiInput(input) {
2216
+ parseConfiguredBaseUrl(input.baseUrl);
2217
+ if (input.responsePointer !== "" && !input.responsePointer.startsWith("/")) {
2218
+ throw new Error("Response pointer must be empty or start with /");
2219
+ }
2220
+ const names = /* @__PURE__ */ new Set();
2221
+ for (const parameter of input.parameters) {
2222
+ if (names.has(parameter.name)) throw new Error(`Duplicate API parameter: ${parameter.name}`);
2223
+ names.add(parameter.name);
2224
+ if (parameter.location === "header") assertSafeHeaderName(parameter.name);
2225
+ if (parameter.location === "path" && !input.pathTemplate.includes(`{${parameter.name}}`)) {
2226
+ throw new Error(`Path parameter ${parameter.name} has no matching placeholder`);
2227
+ }
2228
+ }
2229
+ for (const match of input.pathTemplate.matchAll(/\{([^}]+)\}/g)) {
2230
+ const name2 = match[1];
2231
+ if (name2 === void 0 || !input.parameters.some((parameter) => parameter.name === name2 && parameter.location === "path")) {
2232
+ throw new Error(`Path placeholder ${name2 ?? ""} has no path parameter definition`);
2233
+ }
2234
+ }
2235
+ if (input.auth.type === "api-key" && input.auth.location === "header") assertSafeHeaderName(input.auth.name);
2236
+ }
2237
+ function databaseCredentialRef(id) {
2238
+ return `DSH_CONNECT_DB_${id.replaceAll("-", "_").toUpperCase()}_PASSWORD`;
2239
+ }
2240
+ function apiCredentialRef(id) {
2241
+ return `DSH_CONNECT_API_${id.replaceAll("-", "_").toUpperCase()}_SECRET`;
2242
+ }
2243
+ function ok(value) {
2244
+ return { ok: true, value };
2245
+ }
2246
+ function badRequest(message, issues = []) {
2247
+ return { ok: false, error: { code: "bad-request", message, details: { issues } } };
2248
+ }
2249
+ function safeError3(error) {
2250
+ return (error instanceof Error ? error.message : String(error)).replaceAll(/[\r\n]+/g, " ").slice(0, 1e3);
2251
+ }
2252
+
2253
+ // src/index.ts
2254
+ var name = "dsh-connect";
2255
+ var inject = ["storageDomain", "credentials", "tools", "connection"];
2256
+ var CONNECT_SETTINGS_NAMESPACE = settingsNamespace("dsh-connect");
2257
+ var ConnectSettingsSchema = z5.object({
2258
+ version: z5.number().step(1).min(1).default(1)
2259
+ });
2260
+ async function apply(ctx) {
2261
+ ctx.inject(["settings"], (settingsCtx) => {
2262
+ settingsCtx.settings.register(CONNECT_SETTINGS_NAMESPACE, ConnectSettingsSchema);
2263
+ });
2264
+ const domain = await ctx.storageDomain.open(connectDomainSpec);
2265
+ const store = new ConnectStore(ctx, domain);
2266
+ const apiTools = new ApiToolRegistry(ctx);
2267
+ const profiler = new MetadataProfiler(ctx, store);
2268
+ const fixedTools = [];
2269
+ let disposeRpc;
2270
+ try {
2271
+ apiTools.replace(store.apiDefinitions());
2272
+ fixedTools.push(...registerMetadataTools(ctx, store));
2273
+ const rpc = new ConnectRpc(ctx, store, profiler, apiTools);
2274
+ fixedTools.push(...registerConversationApiTool(
2275
+ ctx,
2276
+ (input) => rpc.validateConversationApi(input),
2277
+ (input) => rpc.createConversationApi(input)
2278
+ ));
2279
+ disposeRpc = ctx.connection.rpc.handle(
2280
+ "/dsh-connect",
2281
+ (endpoint, payload, signal) => rpc.handle(endpoint, payload, signal),
2282
+ { authority: "loopback" }
2283
+ );
2284
+ } catch (error) {
2285
+ apiTools.dispose();
2286
+ for (const dispose of fixedTools.reverse()) dispose();
2287
+ await domain.close();
2288
+ throw error;
2289
+ }
2290
+ ctx.effect(() => async () => {
2291
+ await profiler.dispose();
2292
+ apiTools.dispose();
2293
+ for (const dispose of fixedTools.reverse()) dispose();
2294
+ await disposeRpc?.();
2295
+ await domain.close();
2296
+ }, "dsh-connect runtime");
2297
+ }
2298
+ export {
2299
+ apply,
2300
+ inject,
2301
+ name
2302
+ };
2303
+ //# sourceMappingURL=index.js.map