@desplega.ai/agent-swarm 1.80.0 → 1.80.2

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 (100) hide show
  1. package/openapi.json +399 -14
  2. package/package.json +3 -1
  3. package/src/artifact-sdk/server.ts +2 -1
  4. package/src/be/db.ts +1 -1
  5. package/src/be/migrations/064_scripts.sql +39 -0
  6. package/src/be/migrations/065_script_embeddings.sql +7 -0
  7. package/src/be/migrations/066_scripts_args_json_schema.sql +1 -0
  8. package/src/be/scripts/db.ts +417 -0
  9. package/src/be/scripts/embeddings.ts +233 -0
  10. package/src/be/scripts/extract-schema.ts +55 -0
  11. package/src/be/scripts/maintenance.ts +9 -0
  12. package/src/be/scripts/typecheck.ts +199 -0
  13. package/src/cli.tsx +22 -5
  14. package/src/commands/artifact.ts +3 -2
  15. package/src/commands/claude-managed-setup.ts +2 -1
  16. package/src/commands/codex-login.ts +5 -3
  17. package/src/commands/onboard.tsx +2 -1
  18. package/src/commands/runner.ts +153 -20
  19. package/src/commands/setup.tsx +5 -3
  20. package/src/hooks/hook.ts +4 -3
  21. package/src/http/index.ts +40 -29
  22. package/src/http/memory.ts +28 -0
  23. package/src/http/openapi.ts +1 -0
  24. package/src/http/page-proxy.ts +2 -1
  25. package/src/http/route-def.ts +1 -0
  26. package/src/http/schedules.ts +37 -0
  27. package/src/http/scripts.ts +388 -0
  28. package/src/linear/outbound.ts +9 -2
  29. package/src/otel.ts +5 -0
  30. package/src/providers/claude-adapter.ts +23 -1
  31. package/src/providers/types.ts +8 -0
  32. package/src/scripts-runtime/ctx.ts +23 -0
  33. package/src/scripts-runtime/eval-harness.ts +63 -0
  34. package/src/scripts-runtime/executors/native.ts +232 -0
  35. package/src/scripts-runtime/executors/registry.ts +16 -0
  36. package/src/scripts-runtime/executors/types.ts +63 -0
  37. package/src/scripts-runtime/extract-args-schema.ts +69 -0
  38. package/src/scripts-runtime/extract-signature.ts +81 -0
  39. package/src/scripts-runtime/import-allowlist.ts +109 -0
  40. package/src/scripts-runtime/loader.ts +96 -0
  41. package/src/scripts-runtime/redacted.ts +48 -0
  42. package/src/scripts-runtime/sdk-allowlist.ts +29 -0
  43. package/src/scripts-runtime/stdlib/fetch.ts +46 -0
  44. package/src/scripts-runtime/stdlib/glob.ts +8 -0
  45. package/src/scripts-runtime/stdlib/grep.ts +34 -0
  46. package/src/scripts-runtime/stdlib/index.ts +16 -0
  47. package/src/scripts-runtime/stdlib/table.ts +17 -0
  48. package/src/scripts-runtime/swarm-config.ts +35 -0
  49. package/src/scripts-runtime/swarm-sdk.ts +197 -0
  50. package/src/scripts-runtime/types/stdlib.d.ts +104 -0
  51. package/src/scripts-runtime/types/swarm-sdk.d.ts +86 -0
  52. package/src/server.ts +12 -0
  53. package/src/tests/api-key.test.ts +33 -0
  54. package/src/tests/codex-login.test.ts +1 -1
  55. package/src/tests/error-tracker.test.ts +44 -0
  56. package/src/tests/linear-outbound-sync.test.ts +109 -0
  57. package/src/tests/mcp-tools.test.ts +69 -0
  58. package/src/tests/rate-limit-event.test.ts +292 -0
  59. package/src/tests/redacted.test.ts +29 -0
  60. package/src/tests/runner-tool-spans.test.ts +268 -0
  61. package/src/tests/script-executor-conformance.test.ts +142 -0
  62. package/src/tests/script-executor-registry.test.ts +17 -0
  63. package/src/tests/scripts-db.test.ts +329 -0
  64. package/src/tests/scripts-embeddings.test.ts +291 -0
  65. package/src/tests/scripts-extract-signature.test.ts +47 -0
  66. package/src/tests/scripts-http.test.ts +403 -0
  67. package/src/tests/scripts-import-allowlist.test.ts +55 -0
  68. package/src/tests/scripts-mcp-e2e.test.ts +269 -0
  69. package/src/tests/scripts-runtime-secret-egress.test.ts +44 -0
  70. package/src/tests/scripts-runtime.test.ts +344 -0
  71. package/src/tests/sdk-allowlist.test.ts +59 -0
  72. package/src/tests/secret-scrubber.test.ts +35 -1
  73. package/src/tests/swarm-config.test.ts +38 -0
  74. package/src/tests/tool-annotations.test.ts +2 -2
  75. package/src/tests/tool-call-progress.test.ts +30 -0
  76. package/src/tests/workflow-e2e.test.ts +218 -0
  77. package/src/tests/workflow-executors.test.ts +32 -2
  78. package/src/tests/workflow-input-redaction.test.ts +232 -0
  79. package/src/tests/workflow-swarm-script.test.ts +273 -0
  80. package/src/tools/memory-rate.ts +2 -1
  81. package/src/tools/script-common.ts +88 -0
  82. package/src/tools/script-delete.ts +35 -0
  83. package/src/tools/script-query-types.ts +37 -0
  84. package/src/tools/script-run.ts +43 -0
  85. package/src/tools/script-search.ts +32 -0
  86. package/src/tools/script-upsert.ts +43 -0
  87. package/src/tools/tool-config.ts +7 -0
  88. package/src/types.ts +61 -1
  89. package/src/utils/api-key.ts +28 -0
  90. package/src/utils/error-tracker.ts +58 -0
  91. package/src/utils/page-session.ts +8 -6
  92. package/src/utils/secret-scrubber.ts +22 -1
  93. package/src/workflows/engine.ts +12 -4
  94. package/src/workflows/executors/index.ts +1 -0
  95. package/src/workflows/executors/registry.ts +2 -0
  96. package/src/workflows/executors/script.ts +12 -1
  97. package/src/workflows/executors/swarm-script.ts +170 -0
  98. package/src/workflows/input.ts +65 -0
  99. package/src/workflows/recovery.ts +31 -3
  100. package/src/workflows/resume.ts +43 -5
@@ -0,0 +1,417 @@
1
+ import type { ScriptFsMode, ScriptRecord, ScriptScope, ScriptVersionRecord } from "../../types";
2
+ import { computeContentHash, getDb } from "../db";
3
+ import { embedScript } from "./embeddings";
4
+
5
+ type ScriptRow = Omit<ScriptRecord, "isScratch" | "typeChecked"> & {
6
+ isScratch: number;
7
+ typeChecked: number;
8
+ };
9
+
10
+ type ScriptVersionRow = ScriptVersionRecord;
11
+
12
+ type ScriptIdentity = {
13
+ name: string;
14
+ scope: ScriptScope;
15
+ scopeId?: string | null;
16
+ };
17
+
18
+ type ScriptWriteArgs = ScriptIdentity & {
19
+ source: string;
20
+ description: string;
21
+ intent: string;
22
+ signatureJson: string;
23
+ argsJsonSchema?: string | null;
24
+ isScratch?: boolean;
25
+ typeChecked?: boolean;
26
+ fsMode?: ScriptFsMode;
27
+ agentId?: string | null;
28
+ changeReason?: string | null;
29
+ };
30
+
31
+ export type UpsertScriptResult = {
32
+ script: ScriptRecord;
33
+ isNew: boolean;
34
+ contentDeduped: boolean;
35
+ };
36
+
37
+ function normalizeScopeId(scope: ScriptScope, scopeId?: string | null): string | null {
38
+ if (scope === "global") return null;
39
+ if (!scopeId) {
40
+ throw new Error("scopeId is required for agent-scoped scripts");
41
+ }
42
+ return scopeId;
43
+ }
44
+
45
+ function rowToScript(row: ScriptRow): ScriptRecord {
46
+ return {
47
+ ...row,
48
+ scopeId: row.scopeId ?? null,
49
+ isScratch: row.isScratch === 1,
50
+ typeChecked: row.typeChecked === 1,
51
+ createdByAgentId: row.createdByAgentId ?? null,
52
+ };
53
+ }
54
+
55
+ function rowToScriptVersion(row: ScriptVersionRow): ScriptVersionRecord {
56
+ return {
57
+ ...row,
58
+ changedByAgentId: row.changedByAgentId ?? null,
59
+ changeReason: row.changeReason ?? null,
60
+ };
61
+ }
62
+
63
+ function insertScriptVersion(args: {
64
+ scriptId: string;
65
+ version: number;
66
+ source: string;
67
+ description: string;
68
+ intent: string;
69
+ signatureJson: string;
70
+ contentHash: string;
71
+ changedByAgentId?: string | null;
72
+ changeReason?: string | null;
73
+ }): void {
74
+ getDb()
75
+ .prepare(
76
+ `INSERT INTO script_versions (
77
+ id, scriptId, version, source, description, intent, signatureJson,
78
+ contentHash, changedByAgentId, changedAt, changeReason
79
+ )
80
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
81
+ )
82
+ .run(
83
+ crypto.randomUUID(),
84
+ args.scriptId,
85
+ args.version,
86
+ args.source,
87
+ args.description,
88
+ args.intent,
89
+ args.signatureJson,
90
+ args.contentHash,
91
+ args.changedByAgentId ?? null,
92
+ new Date().toISOString(),
93
+ args.changeReason ?? null,
94
+ );
95
+ }
96
+
97
+ export function insertScript(args: ScriptWriteArgs): ScriptRecord {
98
+ const now = new Date().toISOString();
99
+ const id = crypto.randomUUID();
100
+ const scopeId = normalizeScopeId(args.scope, args.scopeId);
101
+ const contentHash = computeContentHash(args.source);
102
+ const fsMode = args.fsMode ?? "none";
103
+ const isScratch = args.isScratch ? 1 : 0;
104
+ const typeChecked = args.typeChecked ? 1 : 0;
105
+
106
+ const txn = getDb().transaction(() => {
107
+ const row = getDb()
108
+ .prepare<
109
+ ScriptRow,
110
+ [
111
+ string,
112
+ string,
113
+ ScriptScope,
114
+ string | null,
115
+ string,
116
+ string,
117
+ string,
118
+ string,
119
+ string | null,
120
+ string,
121
+ number,
122
+ number,
123
+ string,
124
+ string | null,
125
+ string,
126
+ string,
127
+ ]
128
+ >(
129
+ `INSERT INTO scripts (
130
+ id, name, scope, scopeId, source, description, intent, signatureJson,
131
+ argsJsonSchema, contentHash, isScratch, typeChecked, fsMode, createdByAgentId, createdAt, updatedAt
132
+ )
133
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
134
+ RETURNING *`,
135
+ )
136
+ .get(
137
+ id,
138
+ args.name,
139
+ args.scope,
140
+ scopeId,
141
+ args.source,
142
+ args.description,
143
+ args.intent,
144
+ args.signatureJson,
145
+ args.argsJsonSchema ?? null,
146
+ contentHash,
147
+ isScratch,
148
+ typeChecked,
149
+ fsMode,
150
+ args.agentId ?? null,
151
+ now,
152
+ now,
153
+ );
154
+
155
+ if (!row) throw new Error("Failed to insert script");
156
+
157
+ insertScriptVersion({
158
+ scriptId: row.id,
159
+ version: row.version,
160
+ source: row.source,
161
+ description: row.description,
162
+ intent: row.intent,
163
+ signatureJson: row.signatureJson,
164
+ contentHash: row.contentHash,
165
+ changedByAgentId: args.agentId ?? null,
166
+ changeReason: args.changeReason ?? "Initial creation",
167
+ });
168
+
169
+ return rowToScript(row);
170
+ });
171
+
172
+ return txn();
173
+ }
174
+
175
+ /**
176
+ * Scratch saves skip embedding; they become searchable only after explicit promotion via upsert
177
+ * OR after a `scripts reembed` pass. Explicit upserts embed synchronously so search is
178
+ * immediately consistent for authored/promoted scripts.
179
+ */
180
+ export async function upsertScriptByName(args: ScriptWriteArgs): Promise<UpsertScriptResult> {
181
+ const existing = getScript(args);
182
+ if (!existing) {
183
+ const script = insertScript(args);
184
+ if (!script.isScratch) {
185
+ await embedScript(script);
186
+ }
187
+ return {
188
+ script,
189
+ isNew: true,
190
+ contentDeduped: false,
191
+ };
192
+ }
193
+
194
+ const contentHash = computeContentHash(args.source);
195
+ if (existing.contentHash === contentHash) {
196
+ const fsMode = args.fsMode ?? existing.fsMode;
197
+ const isScratch = args.isScratch ?? existing.isScratch;
198
+ const typeChecked = args.typeChecked ?? existing.typeChecked;
199
+ const argsJsonSchema =
200
+ args.argsJsonSchema !== undefined ? args.argsJsonSchema : existing.argsJsonSchema;
201
+ const trackedMetadataChanged =
202
+ args.description !== existing.description ||
203
+ args.intent !== existing.intent ||
204
+ args.signatureJson !== existing.signatureJson ||
205
+ argsJsonSchema !== existing.argsJsonSchema;
206
+ const promotedFromScratch = existing.isScratch && !isScratch;
207
+ if (
208
+ fsMode !== existing.fsMode ||
209
+ isScratch !== existing.isScratch ||
210
+ typeChecked !== existing.typeChecked ||
211
+ trackedMetadataChanged
212
+ ) {
213
+ const row = getDb()
214
+ .prepare<
215
+ ScriptRow,
216
+ [string, string, string, string | null, number, number, string, string, string]
217
+ >(
218
+ `UPDATE scripts
219
+ SET description = ?, intent = ?, signatureJson = ?, argsJsonSchema = ?,
220
+ isScratch = ?, typeChecked = ?, fsMode = ?, updatedAt = ?
221
+ WHERE id = ?
222
+ RETURNING *`,
223
+ )
224
+ .get(
225
+ args.description,
226
+ args.intent,
227
+ args.signatureJson,
228
+ argsJsonSchema ?? null,
229
+ isScratch ? 1 : 0,
230
+ typeChecked ? 1 : 0,
231
+ fsMode,
232
+ new Date().toISOString(),
233
+ existing.id,
234
+ );
235
+
236
+ if (!row) throw new Error("Failed to update script metadata");
237
+ const script = rowToScript(row);
238
+ if (!script.isScratch && (trackedMetadataChanged || promotedFromScratch)) {
239
+ await embedScript(script);
240
+ }
241
+ return {
242
+ script,
243
+ isNew: false,
244
+ contentDeduped: true,
245
+ };
246
+ }
247
+
248
+ return {
249
+ script: existing,
250
+ isNew: false,
251
+ contentDeduped: true,
252
+ };
253
+ }
254
+
255
+ const now = new Date().toISOString();
256
+ const newVersion = existing.version + 1;
257
+ const fsMode = args.fsMode ?? existing.fsMode;
258
+ const isScratch = args.isScratch ?? existing.isScratch;
259
+ const typeChecked = args.typeChecked ?? existing.typeChecked;
260
+ const argsJsonSchema =
261
+ args.argsJsonSchema !== undefined ? args.argsJsonSchema : existing.argsJsonSchema;
262
+
263
+ const txn = getDb().transaction(() => {
264
+ const row = getDb()
265
+ .prepare<
266
+ ScriptRow,
267
+ [
268
+ string,
269
+ string,
270
+ string,
271
+ string,
272
+ string | null,
273
+ string,
274
+ number,
275
+ number,
276
+ number,
277
+ string,
278
+ string,
279
+ string,
280
+ ]
281
+ >(
282
+ `UPDATE scripts
283
+ SET source = ?, description = ?, intent = ?, signatureJson = ?, argsJsonSchema = ?,
284
+ contentHash = ?, version = ?, isScratch = ?, typeChecked = ?, fsMode = ?, updatedAt = ?
285
+ WHERE id = ?
286
+ RETURNING *`,
287
+ )
288
+ .get(
289
+ args.source,
290
+ args.description,
291
+ args.intent,
292
+ args.signatureJson,
293
+ argsJsonSchema ?? null,
294
+ contentHash,
295
+ newVersion,
296
+ isScratch ? 1 : 0,
297
+ typeChecked ? 1 : 0,
298
+ fsMode,
299
+ now,
300
+ existing.id,
301
+ );
302
+
303
+ if (!row) throw new Error("Failed to update script");
304
+
305
+ insertScriptVersion({
306
+ scriptId: row.id,
307
+ version: row.version,
308
+ source: row.source,
309
+ description: row.description,
310
+ intent: row.intent,
311
+ signatureJson: row.signatureJson,
312
+ contentHash: row.contentHash,
313
+ changedByAgentId: args.agentId ?? null,
314
+ changeReason: args.changeReason ?? null,
315
+ });
316
+
317
+ return rowToScript(row);
318
+ });
319
+
320
+ const script = txn();
321
+ if (!script.isScratch) {
322
+ await embedScript(script);
323
+ }
324
+
325
+ return {
326
+ script,
327
+ isNew: false,
328
+ contentDeduped: false,
329
+ };
330
+ }
331
+
332
+ export function getScript(args: ScriptIdentity): ScriptRecord | null {
333
+ const scopeId = normalizeScopeId(args.scope, args.scopeId);
334
+ const row =
335
+ scopeId === null
336
+ ? getDb()
337
+ .prepare<ScriptRow, [string, ScriptScope]>(
338
+ "SELECT * FROM scripts WHERE name = ? AND scope = ? AND scopeId IS NULL",
339
+ )
340
+ .get(args.name, args.scope)
341
+ : getDb()
342
+ .prepare<ScriptRow, [string, ScriptScope, string]>(
343
+ "SELECT * FROM scripts WHERE name = ? AND scope = ? AND scopeId = ?",
344
+ )
345
+ .get(args.name, args.scope, scopeId);
346
+
347
+ return row ? rowToScript(row) : null;
348
+ }
349
+
350
+ export function getScriptVersion(args: {
351
+ scriptId: string;
352
+ version?: number;
353
+ contentHash?: string;
354
+ }): ScriptVersionRecord | null {
355
+ if (args.version === undefined && args.contentHash === undefined) {
356
+ throw new Error("version or contentHash is required");
357
+ }
358
+
359
+ const row =
360
+ args.version !== undefined
361
+ ? getDb()
362
+ .prepare<ScriptVersionRow, [string, number]>(
363
+ "SELECT * FROM script_versions WHERE scriptId = ? AND version = ?",
364
+ )
365
+ .get(args.scriptId, args.version)
366
+ : getDb()
367
+ .prepare<ScriptVersionRow, [string, string]>(
368
+ "SELECT * FROM script_versions WHERE scriptId = ? AND contentHash = ? ORDER BY version DESC LIMIT 1",
369
+ )
370
+ .get(args.scriptId, args.contentHash as string);
371
+
372
+ return row ? rowToScriptVersion(row) : null;
373
+ }
374
+
375
+ export function listScripts(args?: {
376
+ scope?: ScriptScope;
377
+ scopeId?: string | null;
378
+ includeScratch?: boolean;
379
+ }): ScriptRecord[] {
380
+ const conditions: string[] = [];
381
+ const params: (string | number | null)[] = [];
382
+
383
+ if (args?.scope) {
384
+ conditions.push("scope = ?");
385
+ params.push(args.scope);
386
+
387
+ if (args.scope === "global") {
388
+ conditions.push("scopeId IS NULL");
389
+ } else if (args.scopeId !== undefined) {
390
+ conditions.push("scopeId = ?");
391
+ params.push(normalizeScopeId(args.scope, args.scopeId));
392
+ }
393
+ } else if (args?.scopeId !== undefined) {
394
+ conditions.push("scopeId = ?");
395
+ params.push(args.scopeId ?? "");
396
+ }
397
+
398
+ if (!args?.includeScratch) {
399
+ conditions.push("isScratch = 0");
400
+ }
401
+
402
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
403
+ return getDb()
404
+ .prepare<ScriptRow, (string | number | null)[]>(
405
+ `SELECT * FROM scripts ${whereClause} ORDER BY scope ASC, scopeId ASC, name ASC`,
406
+ )
407
+ .all(...params)
408
+ .map(rowToScript);
409
+ }
410
+
411
+ export function deleteScript(args: ScriptIdentity): boolean {
412
+ const existing = getScript(args);
413
+ if (!existing) return false;
414
+
415
+ const result = getDb().run("DELETE FROM scripts WHERE id = ?", [existing.id]);
416
+ return result.changes > 0;
417
+ }
@@ -0,0 +1,233 @@
1
+ import type { ScriptRecord, ScriptScope } from "../../types";
2
+ import { scrubSecrets } from "../../utils/secret-scrubber";
3
+ import { getDb } from "../db";
4
+ import { cosineSimilarity, deserializeEmbedding, serializeEmbedding } from "../embedding";
5
+ import { getEmbeddingProvider } from "../memory";
6
+ import type { EmbeddingProvider } from "../memory/types";
7
+
8
+ type ScriptEmbeddingRow = {
9
+ scriptId: string;
10
+ embedding: Buffer;
11
+ embeddingModel: string;
12
+ embeddedText: string;
13
+ embeddedAt: string;
14
+ };
15
+
16
+ type ScriptEmbeddingCandidateRow = ScriptEmbeddingRow & {
17
+ id: string;
18
+ name: string;
19
+ scope: ScriptScope;
20
+ scopeId: string | null;
21
+ source: string;
22
+ description: string;
23
+ intent: string;
24
+ signatureJson: string;
25
+ argsJsonSchema: string | null;
26
+ contentHash: string;
27
+ version: number;
28
+ isScratch: number;
29
+ typeChecked: number;
30
+ fsMode: "none" | "workspace-rw";
31
+ createdByAgentId: string | null;
32
+ createdAt: string;
33
+ updatedAt: string;
34
+ };
35
+
36
+ export type ScriptSearchResult = {
37
+ script: ScriptRecord;
38
+ score: number;
39
+ semanticScore: number;
40
+ nameMatchBonus: number;
41
+ };
42
+
43
+ let providerOverride: EmbeddingProvider | null = null;
44
+
45
+ function embeddingProvider(): EmbeddingProvider {
46
+ return providerOverride ?? getEmbeddingProvider();
47
+ }
48
+
49
+ export function setScriptEmbeddingProviderForTests(provider: EmbeddingProvider | null): void {
50
+ providerOverride = provider;
51
+ }
52
+
53
+ export function scriptEmbeddingText(script: ScriptRecord): string {
54
+ return scrubSecrets([script.description, script.intent, script.signatureJson].join("\n"));
55
+ }
56
+
57
+ function rowToScript(row: ScriptEmbeddingCandidateRow): ScriptRecord {
58
+ return {
59
+ id: row.id,
60
+ name: row.name,
61
+ scope: row.scope,
62
+ scopeId: row.scopeId ?? null,
63
+ source: row.source,
64
+ description: row.description,
65
+ intent: row.intent,
66
+ signatureJson: row.signatureJson,
67
+ argsJsonSchema: row.argsJsonSchema ?? null,
68
+ contentHash: row.contentHash,
69
+ version: row.version,
70
+ isScratch: row.isScratch === 1,
71
+ typeChecked: row.typeChecked === 1,
72
+ fsMode: row.fsMode,
73
+ createdByAgentId: row.createdByAgentId ?? null,
74
+ createdAt: row.createdAt,
75
+ updatedAt: row.updatedAt,
76
+ };
77
+ }
78
+
79
+ export async function embedScript(script: ScriptRecord): Promise<void> {
80
+ const text = scriptEmbeddingText(script);
81
+ const provider = embeddingProvider();
82
+ const embedding = await provider.embed(text);
83
+ if (!embedding) return;
84
+
85
+ getDb()
86
+ .prepare(
87
+ `INSERT INTO script_embeddings (
88
+ scriptId, embedding, embeddingModel, embeddedText, embeddedAt
89
+ )
90
+ VALUES (?, ?, ?, ?, ?)
91
+ ON CONFLICT(scriptId) DO UPDATE SET
92
+ embedding = excluded.embedding,
93
+ embeddingModel = excluded.embeddingModel,
94
+ embeddedText = excluded.embeddedText,
95
+ embeddedAt = excluded.embeddedAt`,
96
+ )
97
+ .run(script.id, serializeEmbedding(embedding), provider.name, text, new Date().toISOString());
98
+ }
99
+
100
+ function candidateRows(
101
+ scope?: ScriptScope,
102
+ scopeId?: string | null,
103
+ ): ScriptEmbeddingCandidateRow[] {
104
+ const params: string[] = [];
105
+ let where = "s.isScratch = 0";
106
+
107
+ if (scope === "global") {
108
+ where += " AND s.scope = 'global' AND s.scopeId IS NULL";
109
+ } else if (scope === "agent") {
110
+ where += " AND s.scope = 'agent' AND s.scopeId = ?";
111
+ params.push(scopeId ?? "");
112
+ } else if (scopeId) {
113
+ where +=
114
+ " AND ((s.scope = 'agent' AND s.scopeId = ?) OR (s.scope = 'global' AND s.scopeId IS NULL))";
115
+ params.push(scopeId);
116
+ } else {
117
+ where += " AND s.scope = 'global' AND s.scopeId IS NULL";
118
+ }
119
+
120
+ return getDb()
121
+ .prepare<ScriptEmbeddingCandidateRow, string[]>(
122
+ `SELECT
123
+ s.*,
124
+ e.scriptId,
125
+ e.embedding,
126
+ e.embeddingModel,
127
+ e.embeddedText,
128
+ e.embeddedAt
129
+ FROM script_embeddings e
130
+ JOIN scripts s ON s.id = e.scriptId
131
+ WHERE ${where}`,
132
+ )
133
+ .all(...params);
134
+ }
135
+
136
+ function scriptRows(scope?: ScriptScope, scopeId?: string | null): ScriptEmbeddingCandidateRow[] {
137
+ const params: string[] = [];
138
+ let where = "isScratch = 0";
139
+
140
+ if (scope === "global") {
141
+ where += " AND scope = 'global' AND scopeId IS NULL";
142
+ } else if (scope === "agent") {
143
+ where += " AND scope = 'agent' AND scopeId = ?";
144
+ params.push(scopeId ?? "");
145
+ } else if (scopeId) {
146
+ where += " AND ((scope = 'agent' AND scopeId = ?) OR (scope = 'global' AND scopeId IS NULL))";
147
+ params.push(scopeId);
148
+ } else {
149
+ where += " AND scope = 'global' AND scopeId IS NULL";
150
+ }
151
+
152
+ return getDb()
153
+ .prepare<ScriptEmbeddingCandidateRow, string[]>(
154
+ `SELECT *, NULL as scriptId, NULL as embedding, NULL as embeddingModel, NULL as embeddedText, NULL as embeddedAt FROM scripts WHERE ${where}`,
155
+ )
156
+ .all(...params);
157
+ }
158
+
159
+ function nameMatchBonus(script: ScriptRecord, query: string): number {
160
+ const trimmed = query.trim().toLowerCase();
161
+ if (!trimmed) return 0;
162
+ return script.name.toLowerCase().includes(trimmed) ? 1 : 0;
163
+ }
164
+
165
+ function lexicalFallback(args: {
166
+ query: string;
167
+ scope?: ScriptScope;
168
+ scopeId?: string | null;
169
+ limit?: number;
170
+ }): ScriptSearchResult[] {
171
+ const query = args.query.trim().toLowerCase();
172
+ return scriptRows(args.scope, args.scopeId)
173
+ .map(rowToScript)
174
+ .filter((script) => {
175
+ if (!query) return true;
176
+ return [script.name, script.description, script.intent]
177
+ .join("\n")
178
+ .toLowerCase()
179
+ .includes(query);
180
+ })
181
+ .map((script) => {
182
+ const bonus = nameMatchBonus(script, args.query);
183
+ return {
184
+ script,
185
+ score: bonus || 0.5,
186
+ semanticScore: 0,
187
+ nameMatchBonus: bonus,
188
+ };
189
+ })
190
+ .sort((a, b) => b.score - a.score)
191
+ .slice(0, args.limit ?? 10);
192
+ }
193
+
194
+ export async function searchScripts(args: {
195
+ query: string;
196
+ scope?: ScriptScope;
197
+ scopeId?: string | null;
198
+ limit?: number;
199
+ }): Promise<ScriptSearchResult[]> {
200
+ const provider = embeddingProvider();
201
+ const queryEmbedding = await provider.embed(args.query);
202
+ if (!queryEmbedding) return lexicalFallback(args);
203
+
204
+ const candidates = candidateRows(args.scope, args.scopeId);
205
+ if (candidates.length === 0) return lexicalFallback(args);
206
+
207
+ return candidates
208
+ .map((row) => {
209
+ const script = rowToScript(row);
210
+ const semanticScore = cosineSimilarity(queryEmbedding, deserializeEmbedding(row.embedding));
211
+ const bonus = nameMatchBonus(script, args.query);
212
+ return {
213
+ script,
214
+ score: 0.7 * semanticScore + 0.3 * bonus,
215
+ semanticScore,
216
+ nameMatchBonus: bonus,
217
+ };
218
+ })
219
+ .sort((a, b) => b.score - a.score)
220
+ .slice(0, args.limit ?? 10);
221
+ }
222
+
223
+ export async function reembedAllScripts(): Promise<void> {
224
+ const rows = getDb()
225
+ .prepare<ScriptEmbeddingCandidateRow, []>(
226
+ "SELECT *, NULL as scriptId, NULL as embedding, NULL as embeddingModel, NULL as embeddedText, NULL as embeddedAt FROM scripts WHERE isScratch = 0 ORDER BY updatedAt ASC",
227
+ )
228
+ .all();
229
+
230
+ for (const row of rows) {
231
+ await embedScript(rowToScript(row));
232
+ }
233
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Server-side helper: spawns a subprocess to extract the argsJsonSchema
3
+ * from a user script's `argsSchema` Zod export. Returns the JSON Schema
4
+ * serialized as a string, or null if the script has no argsSchema or
5
+ * if extraction fails.
6
+ *
7
+ * Extraction is best-effort and non-blocking — failures return null.
8
+ */
9
+
10
+ const TIMEOUT_MS = 5_000;
11
+
12
+ function extractorPath(): string {
13
+ return new URL("../../scripts-runtime/extract-args-schema.ts", import.meta.url).pathname;
14
+ }
15
+
16
+ export async function extractArgsJsonSchema(source: string): Promise<string | null> {
17
+ const tmpdir = `${process.env.TMPDIR ?? "/tmp"}/schema-extract-${crypto.randomUUID()}`;
18
+
19
+ try {
20
+ await Bun.$`mkdir -p ${tmpdir}`.quiet();
21
+
22
+ const sourceFile = `${tmpdir}/source.ts`;
23
+ const resultFile = `${tmpdir}/result.json`;
24
+ await Bun.write(sourceFile, source);
25
+
26
+ const proc = Bun.spawn(["bun", "run", extractorPath()], {
27
+ env: {
28
+ PATH: process.env.PATH ?? "/usr/bin:/bin",
29
+ HOME: process.env.HOME ?? "/tmp",
30
+ TMPDIR: tmpdir,
31
+ SWARM_SCHEMA_SOURCE_FILE: sourceFile,
32
+ SWARM_SCHEMA_RESULT_FILE: resultFile,
33
+ SWARM_SCHEMA_TMPDIR: tmpdir,
34
+ },
35
+ cwd: tmpdir,
36
+ stdout: "ignore",
37
+ stderr: "ignore",
38
+ });
39
+
40
+ const timeout = setTimeout(() => proc.kill(), TIMEOUT_MS);
41
+ const exitCode = await proc.exited.catch(() => 1);
42
+ clearTimeout(timeout);
43
+
44
+ if (exitCode !== 0) return null;
45
+
46
+ const result = await Bun.file(resultFile).text();
47
+ const parsed: unknown = JSON.parse(result);
48
+ if (parsed === null) return null;
49
+ return JSON.stringify(parsed);
50
+ } catch {
51
+ return null;
52
+ } finally {
53
+ await Bun.$`rm -rf ${tmpdir}`.quiet().nothrow();
54
+ }
55
+ }
@@ -0,0 +1,9 @@
1
+ import { reembedAllScripts } from "./embeddings";
2
+
3
+ export async function runScriptsMaintenanceCommand(args: string[]): Promise<void> {
4
+ const [subcommand] = args;
5
+ if (subcommand !== "reembed") {
6
+ throw new Error("Unknown scripts command. Usage: scripts reembed");
7
+ }
8
+ await reembedAllScripts();
9
+ }