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