| 194 | } |
| 195 | |
| 196 | export function registerSlotsFunctions(sdk: ISdk, kv: StateKV): void { |
| 197 | void seedDefaults(kv).catch((err) => { |
| 198 | logger.warn("slot defaults seed failed", { |
| 199 | error: err instanceof Error ? err.message : String(err), |
| 200 | }); |
| 201 | }); |
| 202 | |
| 203 | sdk.registerFunction("mem::slot-list", async () => { |
| 204 | const [project, global] = await Promise.all([ |
| 205 | kv.list<MemorySlot>(KV.slots), |
| 206 | kv.list<MemorySlot>(KV.globalSlots), |
| 207 | ]); |
| 208 | const merged = new Map<string, MemorySlot>(); |
| 209 | for (const s of global) merged.set(s.label, s); |
| 210 | for (const s of project) merged.set(s.label, s); |
| 211 | const slots = Array.from(merged.values()).sort((a, b) => |
| 212 | a.label.localeCompare(b.label), |
| 213 | ); |
| 214 | return { success: true, slots }; |
| 215 | }); |
| 216 | |
| 217 | sdk.registerFunction( |
| 218 | "mem::slot-get", |
| 219 | async (data: { label?: string }) => { |
| 220 | const label = validateLabel(data?.label); |
| 221 | if (!label) return { success: false, error: "label required (lowercase, starts with letter, [a-z0-9_])" }; |
| 222 | const { slot, scope } = await readSlot(kv, label); |
| 223 | if (!slot) return { success: false, error: "slot not found" }; |
| 224 | return { success: true, slot, scope }; |
| 225 | }, |
| 226 | ); |
| 227 | |
| 228 | sdk.registerFunction( |
| 229 | "mem::slot-create", |
| 230 | async (data: { |
| 231 | label?: string; |
| 232 | content?: string; |
| 233 | sizeLimit?: number; |
| 234 | description?: string; |
| 235 | pinned?: boolean; |
| 236 | scope?: SlotScope; |
| 237 | }) => { |
| 238 | const label = validateLabel(data?.label); |
| 239 | if (!label) return { success: false, error: "label required (lowercase, starts with letter, [a-z0-9_])" }; |
| 240 | const scope = validateScope(data?.scope); |
| 241 | if (!scope) return { success: false, error: "scope must be 'project' or 'global'" }; |
| 242 | const sizeLimit = validateSizeLimit(data?.sizeLimit); |
| 243 | if (sizeLimit === null) { |
| 244 | return { success: false, error: "sizeLimit must be an integer between 1 and 20000" }; |
| 245 | } |
| 246 | const content = typeof data?.content === "string" ? data.content : ""; |
| 247 | if (content.length > sizeLimit) { |
| 248 | return { success: false, error: `content exceeds sizeLimit (${content.length} > ${sizeLimit})` }; |
| 249 | } |
| 250 | const description = typeof data?.description === "string" ? data.description : ""; |
| 251 | const pinned = typeof data?.pinned === "boolean" ? data.pinned : true; |
| 252 | return withKeyedLock(`slot:${label}`, async () => { |
| 253 | // Duplicate check is scope-local so a project slot can shadow a |