@desplega.ai/agent-swarm 1.103.0 → 1.104.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 (122) hide show
  1. package/README.md +2 -0
  2. package/dist/{actions-7txktrdj.js → actions-2expzt47.js} +6 -6
  3. package/dist/{app-xm3xsdye.js → app-dr8hwx7z.js} +3 -3
  4. package/dist/{assistant-qpv63wk6.js → assistant-9rhgsw7v.js} +9 -10
  5. package/dist/{boot-reembed-chxhrwv8.js → boot-reembed-3q1aywnz.js} +5 -4
  6. package/dist/{boot-reembed-9cxqka3e.js → boot-reembed-9kt97g1z.js} +4 -3
  7. package/dist/{boot-scrub-logs-8qtfkcz8.js → boot-scrub-logs-vskq108e.js} +2 -2
  8. package/dist/{cli-fedn86nz.js → cli-0jw6j755.js} +8 -3
  9. package/dist/{cli-yeemwevj.js → cli-23zk7xqy.js} +20 -16
  10. package/dist/{cli-v9283nma.js → cli-2g9jf9ek.js} +1 -1
  11. package/dist/{cli-rvae030h.js → cli-2hap19dv.js} +3 -3
  12. package/dist/{cli-wjyyv518.js → cli-3j01d33e.js} +9 -14
  13. package/dist/{cli-8hyv6c2v.js → cli-54h5daha.js} +41 -23
  14. package/dist/{cli-fygaw191.js → cli-5pf0b84v.js} +2 -2
  15. package/dist/{cli-m8f6qzck.js → cli-7v4y3hyd.js} +1 -1
  16. package/dist/{cli-fwxdt7kt.js → cli-d4z1csms.js} +30 -5
  17. package/dist/{cli-jt9d9fjw.js → cli-e7bwjntj.js} +1 -1
  18. package/dist/{cli-s14sb64w.js → cli-etvgz5x8.js} +5 -7
  19. package/dist/{cli-2qynerth.js → cli-gj2n2ec1.js} +1 -1
  20. package/dist/{cli-dz3d6zjg.js → cli-nzc0h54v.js} +121 -2
  21. package/dist/{cli-8w95v19d.js → cli-p4cj4vhc.js} +351 -24
  22. package/dist/{cli-xmtsan9k.js → cli-q5tshxd6.js} +1 -1
  23. package/dist/{http-zts4haq7.js → cli-qvzgncd8.js} +83966 -76422
  24. package/dist/{cli-4fnp2tc3.js → cli-s42f54ja.js} +1 -1
  25. package/dist/{cli-krvxq8bc.js → cli-t9v14cnd.js} +23 -4
  26. package/dist/{cli-gzepjz54.js → cli-v1syqkcb.js} +900 -352
  27. package/dist/{cli-ksq38x4n.js → cli-w1nfskkg.js} +2 -2
  28. package/dist/{cli-ns0r7mkr.js → cli-y9p3e94h.js} +3 -3
  29. package/dist/{cli-85pf4z38.js → cli-yjt8a36k.js} +3 -3
  30. package/dist/{cli-yfwwjeft.js → cli-yvektv79.js} +1 -1
  31. package/dist/{cli-kk51f3tf.js → cli-zaxksbgj.js} +5 -5
  32. package/dist/{cli-rttgde5f.js → cli-zghef0em.js} +1 -1
  33. package/dist/cli.js +10 -10
  34. package/dist/{commands-aqn63mcw.js → commands-wfrkef57.js} +2 -2
  35. package/dist/{db-azrvwsnj.js → db-c42pxt7h.js} +2 -2
  36. package/dist/{handlers-hf61har3.js → handlers-n36zkfvx.js} +10 -12
  37. package/dist/{hook-306p3yz4.js → hook-gw3vhv6s.js} +7 -5
  38. package/dist/http-59ygpn2h.js +17263 -0
  39. package/dist/{index-h2yqjyk0.js → index-881c1baf.js} +9 -8
  40. package/dist/{index-29vg51x6.js → index-jca7jdpt.js} +23 -10
  41. package/dist/{index-bfh9hgek.js → index-xf6fp4y1.js} +5 -5
  42. package/dist/{index-3c651yfk.js → index-xz8vaa5c.js} +14 -11
  43. package/dist/{keepalive-a0dg5pds.js → keepalive-mj4302vw.js} +4 -4
  44. package/dist/{lead-7mcxedkc.js → lead-qm0xjvkv.js} +19 -19
  45. package/dist/{maintenance-mrzqd53t.js → maintenance-xppxbe4t.js} +5 -4
  46. package/dist/{onboard-4dkpv9wa.js → onboard-ejx6yxh8.js} +2 -2
  47. package/dist/{otel-impl-jt7gpyp9.js → otel-impl-zs5exkpk.js} +1 -1
  48. package/dist/{pricing-refresh-xqnzh8dt.js → pricing-refresh-nqyf8bkb.js} +4 -4
  49. package/dist/{seed-pricing-56tjm1fj.js → seed-pricing-w3x550cj.js} +3 -3
  50. package/dist/{setup-tbwk8j10.js → setup-7729ktfj.js} +2 -2
  51. package/dist/{worker-e03za92t.js → worker-4ttemwhw.js} +19 -19
  52. package/openapi.json +90 -4
  53. package/package.json +3 -3
  54. package/src/be/memory/constants.ts +6 -0
  55. package/src/be/memory/providers/sqlite-store.ts +468 -71
  56. package/src/be/memory/raters/retrieval.ts +4 -2
  57. package/src/be/memory/reranker.ts +5 -2
  58. package/src/be/memory/retrieval-store.ts +2 -0
  59. package/src/be/memory/types.ts +46 -0
  60. package/src/be/migrations/099_memory_structured_key_versioning.sql +41 -0
  61. package/src/be/migrations/100_memory_retrieval_source.sql +8 -0
  62. package/src/be/migrations/101_script_connections.sql +67 -0
  63. package/src/be/script-connections.ts +744 -0
  64. package/src/be/script-credential-broker.ts +38 -0
  65. package/src/be/scripts/typecheck.ts +23 -2
  66. package/src/be/seed-scripts/catalog/boot-triage.inline.ts +16 -4
  67. package/src/be/seed-scripts/catalog/boot-triage.ts +16 -4
  68. package/src/commands/runner.ts +8 -6
  69. package/src/heartbeat/heartbeat.ts +42 -1
  70. package/src/heartbeat/index.ts +7 -1
  71. package/src/http/memory.ts +127 -14
  72. package/src/http/scripts.ts +15 -14
  73. package/src/scripts-runtime/api-client.ts +100 -0
  74. package/src/scripts-runtime/api-types.ts +45 -0
  75. package/src/scripts-runtime/credential-broker/README.md +40 -0
  76. package/src/scripts-runtime/credential-broker/broker.ts +51 -0
  77. package/src/scripts-runtime/credential-broker/default-bindings.ts +19 -0
  78. package/src/scripts-runtime/credential-broker/fetch-patch.ts +102 -0
  79. package/src/scripts-runtime/credential-broker/index.ts +17 -0
  80. package/src/scripts-runtime/credential-broker/store.ts +72 -0
  81. package/src/scripts-runtime/credential-broker/types.ts +54 -0
  82. package/src/scripts-runtime/ctx.ts +11 -1
  83. package/src/scripts-runtime/egress-secrets.ts +14 -76
  84. package/src/scripts-runtime/eval-harness.ts +1 -1
  85. package/src/scripts-runtime/executors/types.ts +5 -5
  86. package/src/scripts-runtime/loader.ts +6 -1
  87. package/src/scripts-runtime/sdk-allowlist.ts +1 -0
  88. package/src/scripts-runtime/types/stdlib.d.ts +11 -0
  89. package/src/scripts-runtime/types/swarm-sdk.d.ts +11 -0
  90. package/src/server.ts +6 -0
  91. package/src/tests/credential-broker.test.ts +281 -0
  92. package/src/tests/heartbeat.test.ts +239 -0
  93. package/src/tests/memory-edit.test.ts +177 -0
  94. package/src/tests/memory-hybrid.test.ts +212 -0
  95. package/src/tests/runner-repo-autostash.test.ts +39 -8
  96. package/src/tests/script-connections.test.ts +360 -0
  97. package/src/tests/scripts-http.test.ts +16 -10
  98. package/src/tests/scripts-runtime-secret-egress.test.ts +26 -5
  99. package/src/tests/swarm-config-reserved-keys.test.ts +23 -0
  100. package/src/tests/update-schedule-mcp-tool.test.ts +83 -4
  101. package/src/tools/credential-bindings/index.ts +1 -0
  102. package/src/tools/credential-bindings/tool.ts +238 -0
  103. package/src/tools/memory-edit.ts +148 -0
  104. package/src/tools/memory-search.ts +17 -10
  105. package/src/tools/schedules/delete-schedule.ts +18 -8
  106. package/src/tools/schedules/update-schedule.ts +4 -9
  107. package/src/tools/script-connections/index.ts +1 -0
  108. package/src/tools/script-connections/tool.ts +167 -0
  109. package/src/tools/swarm-config/delete-config.ts +5 -0
  110. package/src/tools/swarm-config/set-config.ts +23 -1
  111. package/src/tools/tool-config.ts +6 -1
  112. package/src/types.ts +6 -0
  113. package/src/workflows/executors/swarm-script.ts +2 -0
  114. package/templates/community/dora-metrics/PLAYBOOK.md +294 -0
  115. package/templates/community/dora-metrics/README.md +39 -0
  116. package/templates/community/dora-metrics/lead-prompt.md +56 -0
  117. package/templates/community/dora-metrics/report.mjs +393 -0
  118. package/templates/community/dora-metrics/run.sh +113 -0
  119. package/templates/schedules/weekly-dora-metrics/config.json +13 -0
  120. package/templates/schedules/weekly-dora-metrics/content.md +61 -0
  121. package/dist/cli-2307phk1.js +0 -24223
  122. package/dist/cli-2deg0cen.js +0 -120
@@ -0,0 +1,744 @@
1
+ import { getDb, getSwarmConfigs } from "@/be/db";
2
+ import type {
3
+ ScriptApiConnectionDescriptor,
4
+ ScriptApiJsonSchema,
5
+ ScriptApiJsonValue,
6
+ ScriptApiOperationDescriptor,
7
+ } from "@/scripts-runtime/api-types";
8
+ import {
9
+ CREDENTIAL_BINDINGS_CONFIG_KEY,
10
+ type CredentialBinding,
11
+ normalizeCredentialBindingsDocument,
12
+ } from "@/scripts-runtime/credential-broker";
13
+
14
+ export type ScriptConnectionScope = "global" | "agent" | "repo";
15
+ export type ScriptConnectionKind = "raw" | "openapi" | "mcp";
16
+
17
+ export type ScriptCredentialBindingRecord = CredentialBinding & {
18
+ id: string;
19
+ source: "default" | "user" | "migration";
20
+ createdAt: string;
21
+ updatedAt: string;
22
+ createdBy: string | null;
23
+ updatedBy: string | null;
24
+ };
25
+
26
+ export type ScriptConnectionRecord = {
27
+ id: string;
28
+ slug: string;
29
+ displayName: string | null;
30
+ kind: ScriptConnectionKind;
31
+ scope: ScriptConnectionScope;
32
+ scopeId: string | null;
33
+ baseUrl: string | null;
34
+ allowedHosts: string[];
35
+ credentialBindingId: string | null;
36
+ openapiSpecSourceKind: "url" | "inline" | "agent_fs" | null;
37
+ openapiSpecSource: string | null;
38
+ openapiSpecJson: string | null;
39
+ openapiSpecEtag: string | null;
40
+ openapiSpecFetchedAt: string | null;
41
+ mcpServerId: string | null;
42
+ generatedTypes: string | null;
43
+ generatedRuntimeJson: string | null;
44
+ generatedAt: string | null;
45
+ generationError: string | null;
46
+ enabled: boolean;
47
+ version: number;
48
+ createdAt: string;
49
+ updatedAt: string;
50
+ createdBy: string | null;
51
+ updatedBy: string | null;
52
+ };
53
+
54
+ type BindingRow = {
55
+ id: string;
56
+ config_key: string;
57
+ allowed_hosts_json: string;
58
+ header_template: string | null;
59
+ query_template: string | null;
60
+ scope: string;
61
+ scope_id: string | null;
62
+ active: number;
63
+ source: string;
64
+ created_at: string;
65
+ updated_at: string;
66
+ created_by: string | null;
67
+ updated_by: string | null;
68
+ };
69
+
70
+ type ConnectionRow = {
71
+ id: string;
72
+ slug: string;
73
+ display_name: string | null;
74
+ kind: string;
75
+ scope: string;
76
+ scope_id: string | null;
77
+ base_url: string | null;
78
+ allowed_hosts_json: string;
79
+ credential_binding_id: string | null;
80
+ openapi_spec_source_kind: string | null;
81
+ openapi_spec_source: string | null;
82
+ openapi_spec_json: string | null;
83
+ openapi_spec_etag: string | null;
84
+ openapi_spec_fetched_at: string | null;
85
+ mcp_server_id: string | null;
86
+ generated_types: string | null;
87
+ generated_runtime_json: string | null;
88
+ generated_at: string | null;
89
+ generation_error: string | null;
90
+ enabled: number;
91
+ version: number;
92
+ created_at: string;
93
+ updated_at: string;
94
+ created_by: string | null;
95
+ updated_by: string | null;
96
+ };
97
+
98
+ function parseJsonArray(value: string): string[] {
99
+ try {
100
+ const parsed = JSON.parse(value);
101
+ return Array.isArray(parsed) ? parsed.filter((item) => typeof item === "string") : [];
102
+ } catch {
103
+ return [];
104
+ }
105
+ }
106
+
107
+ function bindingFromRow(row: BindingRow): ScriptCredentialBindingRecord {
108
+ return {
109
+ id: row.id,
110
+ configKey: row.config_key,
111
+ allowedHosts: parseJsonArray(row.allowed_hosts_json),
112
+ headerTemplate: row.header_template ?? undefined,
113
+ queryTemplate: row.query_template ?? undefined,
114
+ scope: row.scope as ScriptConnectionScope,
115
+ scopeId: row.scope_id,
116
+ active: row.active === 1,
117
+ source: row.source as ScriptCredentialBindingRecord["source"],
118
+ createdAt: row.created_at,
119
+ updatedAt: row.updated_at,
120
+ createdBy: row.created_by,
121
+ updatedBy: row.updated_by,
122
+ };
123
+ }
124
+
125
+ function connectionFromRow(row: ConnectionRow): ScriptConnectionRecord {
126
+ return {
127
+ id: row.id,
128
+ slug: row.slug,
129
+ displayName: row.display_name,
130
+ kind: row.kind as ScriptConnectionKind,
131
+ scope: row.scope as ScriptConnectionScope,
132
+ scopeId: row.scope_id,
133
+ baseUrl: row.base_url,
134
+ allowedHosts: parseJsonArray(row.allowed_hosts_json),
135
+ credentialBindingId: row.credential_binding_id,
136
+ openapiSpecSourceKind:
137
+ row.openapi_spec_source_kind as ScriptConnectionRecord["openapiSpecSourceKind"],
138
+ openapiSpecSource: row.openapi_spec_source,
139
+ openapiSpecJson: row.openapi_spec_json,
140
+ openapiSpecEtag: row.openapi_spec_etag,
141
+ openapiSpecFetchedAt: row.openapi_spec_fetched_at,
142
+ mcpServerId: row.mcp_server_id,
143
+ generatedTypes: row.generated_types,
144
+ generatedRuntimeJson: row.generated_runtime_json,
145
+ generatedAt: row.generated_at,
146
+ generationError: row.generation_error,
147
+ enabled: row.enabled === 1,
148
+ version: row.version,
149
+ createdAt: row.created_at,
150
+ updatedAt: row.updated_at,
151
+ createdBy: row.created_by,
152
+ updatedBy: row.updated_by,
153
+ };
154
+ }
155
+
156
+ function applies(
157
+ scope: ScriptConnectionScope,
158
+ scopeId: string | null,
159
+ context: { agentId?: string; repoId?: string },
160
+ ) {
161
+ if (scope === "global") return true;
162
+ if (scope === "agent") return Boolean(context.agentId && scopeId === context.agentId);
163
+ return Boolean(context.repoId && scopeId === context.repoId);
164
+ }
165
+
166
+ function normalizeSlug(slug: string): string {
167
+ const cleaned = slug
168
+ .trim()
169
+ .replace(/[^A-Za-z0-9]+(.)/g, (_m, chr: string) => chr.toUpperCase())
170
+ .replace(/^[^A-Za-z_]+/, "")
171
+ .replace(/[^A-Za-z0-9_]/g, "");
172
+ if (!cleaned) throw new Error("slug must contain at least one letter");
173
+ return `${cleaned[0]?.toLowerCase()}${cleaned.slice(1)}`;
174
+ }
175
+
176
+ function pascal(name: string): string {
177
+ const camel = normalizeSlug(name);
178
+ return `${camel[0]?.toUpperCase()}${camel.slice(1)}`;
179
+ }
180
+
181
+ export function listRelationalCredentialBindings(context?: {
182
+ agentId?: string;
183
+ repoId?: string;
184
+ includeInactive?: boolean;
185
+ }): ScriptCredentialBindingRecord[] {
186
+ const rows = getDb()
187
+ .prepare<BindingRow, []>("SELECT * FROM script_credential_bindings ORDER BY config_key ASC")
188
+ .all();
189
+ return rows
190
+ .map(bindingFromRow)
191
+ .filter((binding) => context?.includeInactive || binding.active !== false)
192
+ .filter((binding) => !context || applies(binding.scope, binding.scopeId ?? null, context));
193
+ }
194
+
195
+ export function getCredentialBindingById(id: string): ScriptCredentialBindingRecord | null {
196
+ const row = getDb()
197
+ .prepare<BindingRow, [string]>("SELECT * FROM script_credential_bindings WHERE id = ?")
198
+ .get(id);
199
+ return row ? bindingFromRow(row) : null;
200
+ }
201
+
202
+ function findCredentialBindingByIdentity(data: {
203
+ configKey: string;
204
+ scope: ScriptConnectionScope;
205
+ scopeId: string | null;
206
+ headerTemplate?: string | null;
207
+ queryTemplate?: string | null;
208
+ }): ScriptCredentialBindingRecord | null {
209
+ const row = getDb()
210
+ .prepare<BindingRow, [string, string, string, string, string]>(
211
+ `SELECT * FROM script_credential_bindings
212
+ WHERE config_key = ?
213
+ AND scope = ?
214
+ AND COALESCE(scope_id, '') = ?
215
+ AND COALESCE(header_template, '') = ?
216
+ AND COALESCE(query_template, '') = ?`,
217
+ )
218
+ .get(
219
+ data.configKey,
220
+ data.scope,
221
+ data.scopeId ?? "",
222
+ data.headerTemplate ?? "",
223
+ data.queryTemplate ?? "",
224
+ );
225
+ return row ? bindingFromRow(row) : null;
226
+ }
227
+
228
+ export function upsertCredentialBinding(data: {
229
+ id?: string;
230
+ configKey: string;
231
+ allowedHosts: string[];
232
+ headerTemplate?: string | null;
233
+ queryTemplate?: string | null;
234
+ scope?: ScriptConnectionScope;
235
+ scopeId?: string | null;
236
+ active?: boolean;
237
+ source?: "default" | "user" | "migration";
238
+ userId?: string | null;
239
+ }): ScriptCredentialBindingRecord {
240
+ const now = new Date().toISOString();
241
+ const id = data.id ?? crypto.randomUUID();
242
+ const scope = data.scope ?? "global";
243
+ const scopeId = scope === "global" ? null : (data.scopeId ?? null);
244
+ const active = data.active === false ? 0 : 1;
245
+ const source = data.source ?? "user";
246
+ const existing =
247
+ (data.id ? getCredentialBindingById(data.id) : null) ??
248
+ findCredentialBindingByIdentity({
249
+ configKey: data.configKey,
250
+ scope,
251
+ scopeId,
252
+ headerTemplate: data.headerTemplate,
253
+ queryTemplate: data.queryTemplate,
254
+ });
255
+
256
+ if (existing) {
257
+ const targetId = existing.id;
258
+ const row = getDb()
259
+ .prepare<
260
+ BindingRow,
261
+ [
262
+ string,
263
+ string,
264
+ string | null,
265
+ string | null,
266
+ string,
267
+ string | null,
268
+ number,
269
+ string,
270
+ string | null,
271
+ string,
272
+ string,
273
+ ]
274
+ >(
275
+ `UPDATE script_credential_bindings
276
+ SET config_key = ?, allowed_hosts_json = ?, header_template = ?, query_template = ?,
277
+ scope = ?, scope_id = ?, active = ?, source = ?, updated_by = ?, updated_at = ?
278
+ WHERE id = ? RETURNING *`,
279
+ )
280
+ .get(
281
+ data.configKey,
282
+ JSON.stringify(data.allowedHosts),
283
+ data.headerTemplate ?? null,
284
+ data.queryTemplate ?? null,
285
+ scope,
286
+ scopeId,
287
+ active,
288
+ source,
289
+ data.userId ?? null,
290
+ now,
291
+ targetId,
292
+ );
293
+ if (!row) throw new Error("Failed to update credential binding");
294
+ return bindingFromRow(row);
295
+ }
296
+
297
+ const row = getDb()
298
+ .prepare<
299
+ BindingRow,
300
+ [
301
+ string,
302
+ string,
303
+ string,
304
+ string | null,
305
+ string | null,
306
+ string,
307
+ string | null,
308
+ number,
309
+ string,
310
+ string,
311
+ string,
312
+ string | null,
313
+ string | null,
314
+ ]
315
+ >(
316
+ `INSERT INTO script_credential_bindings
317
+ (id, config_key, allowed_hosts_json, header_template, query_template, scope, scope_id,
318
+ active, source, created_at, updated_at, created_by, updated_by)
319
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *`,
320
+ )
321
+ .get(
322
+ id,
323
+ data.configKey,
324
+ JSON.stringify(data.allowedHosts),
325
+ data.headerTemplate ?? null,
326
+ data.queryTemplate ?? null,
327
+ scope,
328
+ scopeId,
329
+ active,
330
+ source,
331
+ now,
332
+ now,
333
+ data.userId ?? null,
334
+ data.userId ?? null,
335
+ );
336
+ if (!row) throw new Error("Failed to create credential binding");
337
+ return bindingFromRow(row);
338
+ }
339
+
340
+ export function disableCredentialBinding(
341
+ id: string,
342
+ userId?: string | null,
343
+ ): ScriptCredentialBindingRecord | null {
344
+ const row = getDb()
345
+ .prepare<BindingRow, [string, string | null, string]>(
346
+ `UPDATE script_credential_bindings SET active = 0, updated_at = ?, updated_by = ? WHERE id = ? RETURNING *`,
347
+ )
348
+ .get(new Date().toISOString(), userId ?? null, id);
349
+ return row ? bindingFromRow(row) : null;
350
+ }
351
+
352
+ export function importLegacyCredentialBindings(userId?: string | null): number {
353
+ let imported = 0;
354
+ for (const config of getSwarmConfigs({ key: CREDENTIAL_BINDINGS_CONFIG_KEY })) {
355
+ let bindings: CredentialBinding[];
356
+ try {
357
+ bindings = normalizeCredentialBindingsDocument(JSON.parse(config.value));
358
+ } catch {
359
+ continue;
360
+ }
361
+ for (const binding of bindings) {
362
+ const scope = binding.scope ?? (config.scope as ScriptConnectionScope);
363
+ upsertCredentialBinding({
364
+ configKey: binding.configKey,
365
+ allowedHosts: binding.allowedHosts,
366
+ headerTemplate: binding.headerTemplate,
367
+ queryTemplate: binding.queryTemplate,
368
+ scope,
369
+ scopeId: binding.scopeId ?? config.scopeId ?? null,
370
+ active: binding.active !== false,
371
+ source: "migration",
372
+ userId,
373
+ });
374
+ imported += 1;
375
+ }
376
+ }
377
+ return imported;
378
+ }
379
+
380
+ export function listScriptConnections(context?: {
381
+ agentId?: string;
382
+ repoId?: string;
383
+ kind?: ScriptConnectionKind;
384
+ includeDisabled?: boolean;
385
+ }): ScriptConnectionRecord[] {
386
+ const rows = getDb()
387
+ .prepare<ConnectionRow, []>("SELECT * FROM script_connections ORDER BY slug ASC")
388
+ .all();
389
+ return rows
390
+ .map(connectionFromRow)
391
+ .filter((connection) => !context?.kind || connection.kind === context.kind)
392
+ .filter((connection) => context?.includeDisabled || connection.enabled)
393
+ .filter((connection) => !context || applies(connection.scope, connection.scopeId, context));
394
+ }
395
+
396
+ function schemaToTs(schema: unknown): string {
397
+ if (!schema || typeof schema !== "object") return "JsonValue";
398
+ const s = schema as Record<string, unknown>;
399
+ if (typeof s.$ref === "string") return "JsonValue";
400
+ if (Array.isArray(s.enum)) return s.enum.map((v) => JSON.stringify(v)).join(" | ") || "JsonValue";
401
+ const type = s.type;
402
+ if (type === "string") return "string";
403
+ if (type === "integer" || type === "number") return "number";
404
+ if (type === "boolean") return "boolean";
405
+ if (type === "array") return `${schemaToTs(s.items)}[]`;
406
+ if (type === "object" || s.properties) {
407
+ const required = new Set(
408
+ Array.isArray(s.required) ? s.required.filter((v): v is string => typeof v === "string") : [],
409
+ );
410
+ const props =
411
+ s.properties && typeof s.properties === "object"
412
+ ? (s.properties as Record<string, unknown>)
413
+ : {};
414
+ const entries = Object.entries(props);
415
+ if (entries.length === 0) return "{ [key: string]: JsonValue }";
416
+ return `{ ${entries.map(([key, value]) => `${JSON.stringify(key)}${required.has(key) ? "" : "?"}: ${schemaToTs(value)}`).join("; ")} }`;
417
+ }
418
+ return "JsonValue";
419
+ }
420
+
421
+ function toJsonValue(value: unknown): ScriptApiJsonValue | undefined {
422
+ if (value === null) return null;
423
+ if (["boolean", "number", "string"].includes(typeof value)) return value as ScriptApiJsonValue;
424
+ if (Array.isArray(value)) {
425
+ return value
426
+ .map((item) => toJsonValue(item))
427
+ .filter((item): item is ScriptApiJsonValue => item !== undefined);
428
+ }
429
+ if (value && typeof value === "object") {
430
+ return Object.fromEntries(
431
+ Object.entries(value as Record<string, unknown>)
432
+ .map(([key, item]) => [key, toJsonValue(item)] as const)
433
+ .filter((entry): entry is readonly [string, ScriptApiJsonValue] => entry[1] !== undefined),
434
+ );
435
+ }
436
+ return undefined;
437
+ }
438
+
439
+ function jsonSchema(schema: unknown): ScriptApiJsonSchema {
440
+ if (typeof schema === "boolean") return schema;
441
+ const value = toJsonValue(schema);
442
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
443
+ }
444
+
445
+ function methodName(operationId: string | undefined, method: string, path: string): string {
446
+ if (operationId) return normalizeSlug(operationId);
447
+ return normalizeSlug(`${method}_${path.replace(/[{}]/g, "").replace(/\//g, "_")}`);
448
+ }
449
+
450
+ function extractOperations(
451
+ spec: unknown,
452
+ slug: string,
453
+ ): { operations: ScriptApiOperationDescriptor[]; types: string } {
454
+ if (!spec || typeof spec !== "object") throw new Error("OpenAPI spec must be an object");
455
+ const root = spec as Record<string, unknown>;
456
+ const paths = root.paths;
457
+ if (!paths || typeof paths !== "object") throw new Error("OpenAPI spec must contain paths");
458
+ const operations: ScriptApiOperationDescriptor[] = [];
459
+ const typeBlocks: string[] = [];
460
+ const verbs = new Set(["get", "post", "put", "patch", "delete"]);
461
+
462
+ for (const [path, pathItem] of Object.entries(paths as Record<string, unknown>)) {
463
+ if (!pathItem || typeof pathItem !== "object") continue;
464
+ const inheritedParams = Array.isArray((pathItem as { parameters?: unknown }).parameters)
465
+ ? (pathItem as { parameters: unknown[] }).parameters
466
+ : [];
467
+ for (const [method, op] of Object.entries(pathItem as Record<string, unknown>)) {
468
+ if (!verbs.has(method) || !op || typeof op !== "object") continue;
469
+ const operation = op as Record<string, unknown>;
470
+ const name = methodName(
471
+ typeof operation.operationId === "string" ? operation.operationId : undefined,
472
+ method,
473
+ path,
474
+ );
475
+ const typeBase = `${pascal(slug)}${pascal(name)}`;
476
+ const parameters = [
477
+ ...inheritedParams,
478
+ ...(Array.isArray(operation.parameters) ? operation.parameters : []),
479
+ ]
480
+ .filter((param): param is Record<string, unknown> =>
481
+ Boolean(
482
+ param &&
483
+ typeof param === "object" &&
484
+ typeof (param as Record<string, unknown>).name === "string",
485
+ ),
486
+ )
487
+ .map((param) => ({
488
+ name: String(param.name),
489
+ in: (["path", "query", "header"].includes(String(param.in))
490
+ ? String(param.in)
491
+ : "query") as "path" | "query" | "header",
492
+ required: param.required === true,
493
+ type: schemaToTs(param.schema),
494
+ schema: jsonSchema(param.schema),
495
+ }));
496
+ const requestBody =
497
+ operation.requestBody && typeof operation.requestBody === "object"
498
+ ? (operation.requestBody as Record<string, unknown>)
499
+ : null;
500
+ const jsonContent =
501
+ requestBody && typeof requestBody.content === "object"
502
+ ? (requestBody.content as Record<string, { schema?: unknown }>)["application/json"]
503
+ : undefined;
504
+ const requestBodySchema = jsonContent?.schema ? jsonSchema(jsonContent.schema) : undefined;
505
+ const bodyType = jsonContent?.schema ? schemaToTs(jsonContent.schema) : "JsonValue";
506
+ const responses =
507
+ operation.responses && typeof operation.responses === "object"
508
+ ? (operation.responses as Record<string, unknown>)
509
+ : {};
510
+ const successStatus =
511
+ Object.keys(responses).find((status) => /^2\d\d$/.test(status)) ?? "default";
512
+ const response = responses[successStatus];
513
+ const responseContent =
514
+ response &&
515
+ typeof response === "object" &&
516
+ typeof (response as { content?: unknown }).content === "object"
517
+ ? (response as { content: Record<string, { schema?: unknown }> }).content[
518
+ "application/json"
519
+ ]
520
+ : undefined;
521
+ const responseType = responseContent?.schema
522
+ ? schemaToTs(responseContent.schema)
523
+ : "JsonValue";
524
+ const responseSchema = responseContent?.schema ? jsonSchema(responseContent.schema) : {};
525
+ const paramsByPlace = (place: "path" | "query" | "header") =>
526
+ parameters
527
+ .filter((param) => param.in === place)
528
+ .map(
529
+ (param) => `${JSON.stringify(param.name)}${param.required ? "" : "?"}: ${param.type}`,
530
+ )
531
+ .join("; ");
532
+ const hasBody = Boolean(jsonContent?.schema || requestBody);
533
+ const requestType = `export type ${typeBase}Args = {${paramsByPlace("path") ? ` path: { ${paramsByPlace("path")} };` : ""}${paramsByPlace("query") ? ` query?: { ${paramsByPlace("query")} };` : ""}${paramsByPlace("header") ? ` header?: { ${paramsByPlace("header")} };` : ""}${hasBody ? ` body: ${bodyType};` : ""}};`;
534
+ const responseDecl = `export type ${typeBase}Response = ${responseType};`;
535
+ typeBlocks.push(requestType, responseDecl);
536
+ operations.push({
537
+ name,
538
+ method: method.toUpperCase(),
539
+ path,
540
+ parameters: parameters.map(({ name, in: where, required, schema }) => ({
541
+ name,
542
+ in: where,
543
+ required,
544
+ schema,
545
+ })),
546
+ hasBody,
547
+ successStatus,
548
+ requestBodySchema,
549
+ responseSchema,
550
+ requestType: `${typeBase}Args`,
551
+ responseType: `${typeBase}Response`,
552
+ });
553
+ }
554
+ }
555
+
556
+ if (operations.length === 0) throw new Error("OpenAPI spec did not contain supported operations");
557
+ return { operations, types: typeBlocks.join("\n") };
558
+ }
559
+
560
+ export function buildGeneratedArtifacts(input: {
561
+ slug: string;
562
+ baseUrl: string;
563
+ credentialBinding: ScriptCredentialBindingRecord | null;
564
+ openapiSpec: unknown;
565
+ }): { generatedTypes: string; generatedRuntimeJson: string } {
566
+ const slug = normalizeSlug(input.slug);
567
+ const { operations, types } = extractOperations(input.openapiSpec, slug);
568
+ const credential = input.credentialBinding
569
+ ? {
570
+ configKey: input.credentialBinding.configKey,
571
+ headerTemplate: input.credentialBinding.headerTemplate,
572
+ queryTemplate: input.credentialBinding.queryTemplate,
573
+ }
574
+ : null;
575
+ const descriptor: ScriptApiConnectionDescriptor = {
576
+ slug,
577
+ baseUrl: input.baseUrl,
578
+ credential,
579
+ operations,
580
+ };
581
+ const generatedTypes = [
582
+ types,
583
+ `export interface ${pascal(slug)}Api {`,
584
+ ...operations.map(
585
+ (operation) =>
586
+ ` ${operation.name}(args: ${operation.requestType}): Promise<${operation.responseType}>;`,
587
+ ),
588
+ "}",
589
+ ].join("\n");
590
+ return { generatedTypes, generatedRuntimeJson: JSON.stringify(descriptor) };
591
+ }
592
+
593
+ export function upsertScriptConnection(data: {
594
+ id?: string;
595
+ slug: string;
596
+ displayName?: string | null;
597
+ kind: ScriptConnectionKind;
598
+ scope?: ScriptConnectionScope;
599
+ scopeId?: string | null;
600
+ baseUrl?: string | null;
601
+ allowedHosts?: string[];
602
+ credentialBindingId?: string | null;
603
+ openapiSpecSourceKind?: "url" | "inline" | "agent_fs" | null;
604
+ openapiSpecSource?: string | null;
605
+ openapiSpecJson?: string | null;
606
+ mcpServerId?: string | null;
607
+ enabled?: boolean;
608
+ userId?: string | null;
609
+ }): ScriptConnectionRecord {
610
+ const now = new Date().toISOString();
611
+ const id = data.id ?? crypto.randomUUID();
612
+ const scope = data.scope ?? "global";
613
+ const scopeId = scope === "global" ? null : (data.scopeId ?? null);
614
+ let generatedTypes: string | null = null;
615
+ let generatedRuntimeJson: string | null = null;
616
+ let generationError: string | null = null;
617
+ let generatedAt: string | null = null;
618
+
619
+ if (data.kind === "openapi") {
620
+ try {
621
+ const spec = JSON.parse(data.openapiSpecJson ?? "{}");
622
+ const binding = data.credentialBindingId
623
+ ? getCredentialBindingById(data.credentialBindingId)
624
+ : null;
625
+ const artifacts = buildGeneratedArtifacts({
626
+ slug: data.slug,
627
+ baseUrl: data.baseUrl ?? "",
628
+ credentialBinding: binding,
629
+ openapiSpec: spec,
630
+ });
631
+ generatedTypes = artifacts.generatedTypes;
632
+ generatedRuntimeJson = artifacts.generatedRuntimeJson;
633
+ generatedAt = now;
634
+ } catch (err) {
635
+ generationError = err instanceof Error ? err.message : String(err);
636
+ }
637
+ }
638
+
639
+ const existing = data.id
640
+ ? getDb()
641
+ .prepare<{ id: string; version: number }, [string]>(
642
+ "SELECT id, version FROM script_connections WHERE id = ?",
643
+ )
644
+ .get(data.id)
645
+ : getDb()
646
+ .prepare<{ id: string; version: number }, [string, string, string | null]>(
647
+ "SELECT id, version FROM script_connections WHERE slug = ? AND scope = ? AND COALESCE(scope_id, '') = COALESCE(?, '')",
648
+ )
649
+ .get(normalizeSlug(data.slug), scope, scopeId);
650
+
651
+ const params = [
652
+ normalizeSlug(data.slug),
653
+ data.displayName ?? null,
654
+ data.kind,
655
+ scope,
656
+ scopeId,
657
+ data.baseUrl ?? null,
658
+ JSON.stringify(data.allowedHosts ?? []),
659
+ data.credentialBindingId ?? null,
660
+ data.openapiSpecSourceKind ?? (data.kind === "openapi" ? "inline" : null),
661
+ data.openapiSpecSource ?? null,
662
+ data.openapiSpecJson ?? null,
663
+ data.mcpServerId ?? null,
664
+ generatedTypes,
665
+ generatedRuntimeJson,
666
+ generatedAt,
667
+ generationError,
668
+ data.enabled === false ? 0 : 1,
669
+ ] as const;
670
+
671
+ if (existing) {
672
+ const row = getDb()
673
+ .prepare<ConnectionRow, [...typeof params, string, string | null, number, string]>(
674
+ `UPDATE script_connections SET
675
+ slug = ?, display_name = ?, kind = ?, scope = ?, scope_id = ?, base_url = ?,
676
+ allowed_hosts_json = ?, credential_binding_id = ?, openapi_spec_source_kind = ?,
677
+ openapi_spec_source = ?, openapi_spec_json = ?, mcp_server_id = ?, generated_types = ?,
678
+ generated_runtime_json = ?, generated_at = ?, generation_error = ?, enabled = ?,
679
+ updated_at = ?, updated_by = ?, version = ?
680
+ WHERE id = ? RETURNING *`,
681
+ )
682
+ .get(...params, now, data.userId ?? null, existing.version + 1, existing.id);
683
+ if (!row) throw new Error("Failed to update script connection");
684
+ return connectionFromRow(row);
685
+ }
686
+
687
+ const row = getDb()
688
+ .prepare<
689
+ ConnectionRow,
690
+ [string, ...typeof params, string, string, string | null, string | null]
691
+ >(
692
+ `INSERT INTO script_connections
693
+ (id, slug, display_name, kind, scope, scope_id, base_url, allowed_hosts_json,
694
+ credential_binding_id, openapi_spec_source_kind, openapi_spec_source, openapi_spec_json,
695
+ mcp_server_id, generated_types, generated_runtime_json, generated_at, generation_error,
696
+ enabled, created_at, updated_at, created_by, updated_by)
697
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *`,
698
+ )
699
+ .get(id, ...params, now, now, data.userId ?? null, data.userId ?? null);
700
+ if (!row) throw new Error("Failed to create script connection");
701
+ return connectionFromRow(row);
702
+ }
703
+
704
+ export function setScriptConnectionEnabled(
705
+ id: string,
706
+ enabled: boolean,
707
+ userId?: string | null,
708
+ ): ScriptConnectionRecord | null {
709
+ const row = getDb()
710
+ .prepare<ConnectionRow, [number, string, string | null, string]>(
711
+ `UPDATE script_connections
712
+ SET enabled = ?, updated_at = ?, updated_by = ?, version = version + 1
713
+ WHERE id = ? RETURNING *`,
714
+ )
715
+ .get(enabled ? 1 : 0, new Date().toISOString(), userId ?? null, id);
716
+ return row ? connectionFromRow(row) : null;
717
+ }
718
+
719
+ export function getScriptApiConnectionDescriptors(
720
+ context: { agentId?: string; repoId?: string } = {},
721
+ ): ScriptApiConnectionDescriptor[] {
722
+ return listScriptConnections({ ...context, kind: "openapi" })
723
+ .map((connection) => {
724
+ if (!connection.generatedRuntimeJson) return null;
725
+ try {
726
+ return JSON.parse(connection.generatedRuntimeJson) as ScriptApiConnectionDescriptor;
727
+ } catch {
728
+ return null;
729
+ }
730
+ })
731
+ .filter((descriptor): descriptor is ScriptApiConnectionDescriptor => Boolean(descriptor));
732
+ }
733
+
734
+ export function getScriptApiTypes(context: { agentId?: string; repoId?: string } = {}): string {
735
+ const connections = listScriptConnections({ ...context, kind: "openapi" });
736
+ const blocks = connections
737
+ .filter((connection) => connection.generatedTypes)
738
+ .map((connection) => connection.generatedTypes as string);
739
+ if (blocks.length === 0) return "export interface ScriptApiRegistry {}\n";
740
+ const members = connections
741
+ .filter((connection) => connection.generatedTypes)
742
+ .map((connection) => ` ${connection.slug}: ${pascal(connection.slug)}Api;`);
743
+ return `${blocks.join("\n\n")}\n\nexport interface ScriptApiRegistry {\n${members.join("\n")}\n}\n`;
744
+ }