@xnetjs/plugins 0.0.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.
package/dist/index.js ADDED
@@ -0,0 +1,2640 @@
1
+ // src/types.ts
2
+ function getPlatformCapabilities(platform) {
3
+ return {
4
+ platform,
5
+ features: {
6
+ views: true,
7
+ editorExtensions: platform !== "mobile",
8
+ slashCommands: platform !== "mobile",
9
+ services: platform === "electron",
10
+ processes: platform === "electron",
11
+ localAPI: platform === "electron",
12
+ filesystem: platform === "electron",
13
+ clipboard: platform !== "mobile",
14
+ notifications: true,
15
+ p2pSync: true
16
+ }
17
+ };
18
+ }
19
+ function createExtensionStorage() {
20
+ const storage = /* @__PURE__ */ new Map();
21
+ return {
22
+ get(key) {
23
+ return storage.get(key);
24
+ },
25
+ set(key, value) {
26
+ storage.set(key, value);
27
+ },
28
+ delete(key) {
29
+ storage.delete(key);
30
+ },
31
+ keys() {
32
+ return [...storage.keys()];
33
+ }
34
+ };
35
+ }
36
+
37
+ // src/manifest.ts
38
+ var PluginValidationError = class extends Error {
39
+ constructor(message, issues) {
40
+ super(message);
41
+ this.issues = issues;
42
+ this.name = "PluginValidationError";
43
+ }
44
+ };
45
+ function validateManifest(manifest) {
46
+ const issues = [];
47
+ if (!manifest || typeof manifest !== "object") {
48
+ throw new PluginValidationError("Manifest must be an object", ["Invalid manifest type"]);
49
+ }
50
+ const m = manifest;
51
+ if (typeof m.id !== "string" || !m.id) {
52
+ issues.push("id is required and must be a non-empty string");
53
+ } else if (!/^[a-z][a-z0-9]*(\.[a-z][a-z0-9-]*)+$/i.test(m.id)) {
54
+ issues.push("id must be in reverse-domain format (e.g., com.example.my-plugin)");
55
+ }
56
+ if (typeof m.name !== "string" || !m.name) {
57
+ issues.push("name is required and must be a non-empty string");
58
+ }
59
+ if (typeof m.version !== "string" || !m.version) {
60
+ issues.push("version is required and must be a non-empty string");
61
+ } else if (!/^\d+\.\d+\.\d+/.test(m.version)) {
62
+ issues.push("version must be a valid semver (e.g., 1.0.0)");
63
+ }
64
+ if (m.description !== void 0 && typeof m.description !== "string") {
65
+ issues.push("description must be a string");
66
+ }
67
+ if (m.author !== void 0 && typeof m.author !== "string") {
68
+ issues.push("author must be a string");
69
+ }
70
+ if (m.platforms !== void 0) {
71
+ if (!Array.isArray(m.platforms)) {
72
+ issues.push("platforms must be an array");
73
+ } else {
74
+ const validPlatforms = ["web", "electron", "mobile"];
75
+ for (const p of m.platforms) {
76
+ if (!validPlatforms.includes(p)) {
77
+ issues.push(`Invalid platform: ${p}. Must be one of: ${validPlatforms.join(", ")}`);
78
+ }
79
+ }
80
+ }
81
+ }
82
+ if (m.activate !== void 0 && typeof m.activate !== "function") {
83
+ issues.push("activate must be a function");
84
+ }
85
+ if (m.deactivate !== void 0 && typeof m.deactivate !== "function") {
86
+ issues.push("deactivate must be a function");
87
+ }
88
+ if (issues.length > 0) {
89
+ throw new PluginValidationError(
90
+ `Plugin manifest validation failed: ${issues.join("; ")}`,
91
+ issues
92
+ );
93
+ }
94
+ return manifest;
95
+ }
96
+ function defineExtension(manifest) {
97
+ validateManifest(manifest);
98
+ return manifest;
99
+ }
100
+
101
+ // src/contributions.ts
102
+ var TypedRegistry = class {
103
+ items = /* @__PURE__ */ new Map();
104
+ listeners = /* @__PURE__ */ new Set();
105
+ register(item) {
106
+ const key = item.id ?? item.type ?? crypto.randomUUID();
107
+ this.items.set(key, item);
108
+ this.notify();
109
+ return {
110
+ dispose: () => {
111
+ this.items.delete(key);
112
+ this.notify();
113
+ }
114
+ };
115
+ }
116
+ unregister(key) {
117
+ const deleted = this.items.delete(key);
118
+ if (deleted) this.notify();
119
+ return deleted;
120
+ }
121
+ get(key) {
122
+ return this.items.get(key);
123
+ }
124
+ getAll() {
125
+ return [...this.items.values()];
126
+ }
127
+ has(key) {
128
+ return this.items.has(key);
129
+ }
130
+ get size() {
131
+ return this.items.size;
132
+ }
133
+ onChange(listener) {
134
+ this.listeners.add(listener);
135
+ return () => this.listeners.delete(listener);
136
+ }
137
+ notify() {
138
+ for (const listener of this.listeners) {
139
+ try {
140
+ listener();
141
+ } catch (err) {
142
+ console.error("[TypedRegistry] Listener error:", err);
143
+ }
144
+ }
145
+ }
146
+ clear() {
147
+ this.items.clear();
148
+ this.notify();
149
+ }
150
+ };
151
+ var ContributionRegistry = class {
152
+ views = new TypedRegistry();
153
+ commands = new TypedRegistry();
154
+ slashCommands = new TypedRegistry();
155
+ sidebar = new TypedRegistry();
156
+ editor = new TypedRegistry();
157
+ propertyHandlers = new TypedRegistry();
158
+ blocks = new TypedRegistry();
159
+ settings = new TypedRegistry();
160
+ /**
161
+ * Clear all registries (for cleanup/testing)
162
+ */
163
+ clear() {
164
+ this.views.clear();
165
+ this.commands.clear();
166
+ this.slashCommands.clear();
167
+ this.sidebar.clear();
168
+ this.editor.clear();
169
+ this.propertyHandlers.clear();
170
+ this.blocks.clear();
171
+ this.settings.clear();
172
+ }
173
+ };
174
+
175
+ // src/shortcuts.ts
176
+ function isMac() {
177
+ if (typeof navigator === "undefined") return false;
178
+ return navigator.platform?.includes("Mac") || navigator.userAgent?.includes("Mac");
179
+ }
180
+ var ShortcutManager = class {
181
+ shortcuts = /* @__PURE__ */ new Map();
182
+ enabled = true;
183
+ /**
184
+ * Register a command with a keyboard shortcut.
185
+ *
186
+ * @param command - Command with keybinding
187
+ * @returns Disposable to unregister the shortcut
188
+ */
189
+ register(command) {
190
+ if (!command.keybinding) {
191
+ return { dispose: () => {
192
+ } };
193
+ }
194
+ const normalized = this.normalize(command.keybinding);
195
+ this.shortcuts.set(normalized, command);
196
+ return {
197
+ dispose: () => {
198
+ this.shortcuts.delete(normalized);
199
+ }
200
+ };
201
+ }
202
+ /**
203
+ * Unregister a command by ID.
204
+ */
205
+ unregister(commandId) {
206
+ for (const [key, cmd] of this.shortcuts) {
207
+ if (cmd.id === commandId) {
208
+ return this.shortcuts.delete(key);
209
+ }
210
+ }
211
+ return false;
212
+ }
213
+ /**
214
+ * Handle a keyboard event.
215
+ *
216
+ * @param event - Keyboard event
217
+ * @returns True if a shortcut was triggered
218
+ */
219
+ handleKeyDown(event) {
220
+ if (!this.enabled) return false;
221
+ const target = event.target;
222
+ if (target?.tagName === "INPUT" || target?.tagName === "TEXTAREA") {
223
+ if (event.key !== "Escape") {
224
+ return false;
225
+ }
226
+ }
227
+ if (target?.isContentEditable) {
228
+ const key2 = this.eventToString(event);
229
+ const command2 = this.shortcuts.get(key2);
230
+ if (!command2) return false;
231
+ }
232
+ const key = this.eventToString(event);
233
+ const command = this.shortcuts.get(key);
234
+ if (command) {
235
+ if (command.when && !command.when()) {
236
+ return false;
237
+ }
238
+ event.preventDefault();
239
+ event.stopPropagation();
240
+ Promise.resolve(command.execute()).catch((err) => {
241
+ console.error(`[ShortcutManager] Command '${command.id}' failed:`, err);
242
+ });
243
+ return true;
244
+ }
245
+ return false;
246
+ }
247
+ /**
248
+ * Enable or disable shortcut handling.
249
+ */
250
+ setEnabled(enabled) {
251
+ this.enabled = enabled;
252
+ }
253
+ /**
254
+ * Check if shortcuts are enabled.
255
+ */
256
+ isEnabled() {
257
+ return this.enabled;
258
+ }
259
+ /**
260
+ * Get all registered shortcuts.
261
+ */
262
+ getAll() {
263
+ return [...this.shortcuts.values()];
264
+ }
265
+ /**
266
+ * Get the shortcut for a command ID.
267
+ */
268
+ getShortcut(commandId) {
269
+ for (const [key, cmd] of this.shortcuts) {
270
+ if (cmd.id === commandId) {
271
+ return this.denormalize(key);
272
+ }
273
+ }
274
+ return void 0;
275
+ }
276
+ /**
277
+ * Format a keybinding for display (using platform-specific symbols).
278
+ */
279
+ formatForDisplay(keybinding) {
280
+ const mac = isMac();
281
+ return keybinding.replace(/Mod/g, mac ? "\u2318" : "Ctrl").replace(/Ctrl/g, mac ? "\u2303" : "Ctrl").replace(/Alt/g, mac ? "\u2325" : "Alt").replace(/Shift/g, mac ? "\u21E7" : "Shift").replace(/Meta/g, mac ? "\u2318" : "Win").replace(/-/g, mac ? "" : "+");
282
+ }
283
+ /**
284
+ * Normalize a keybinding string for consistent lookup.
285
+ */
286
+ normalize(keybinding) {
287
+ const mac = isMac();
288
+ const parts = keybinding.replace(/Mod/g, mac ? "Meta" : "Ctrl").split("-").map((p) => p.toLowerCase());
289
+ const modifiers = ["ctrl", "meta", "alt", "shift"];
290
+ const sortedModifiers = parts.filter((p) => modifiers.includes(p)).sort();
291
+ const key = parts.filter((p) => !modifiers.includes(p))[0] ?? "";
292
+ return [...sortedModifiers, key].join("-");
293
+ }
294
+ /**
295
+ * Convert normalized form back to display form.
296
+ */
297
+ denormalize(normalized) {
298
+ const mac = isMac();
299
+ return normalized.split("-").map((part) => {
300
+ if (part === "ctrl") return "Ctrl";
301
+ if (part === "meta") return mac ? "Cmd" : "Ctrl";
302
+ if (part === "alt") return "Alt";
303
+ if (part === "shift") return "Shift";
304
+ return part.toUpperCase();
305
+ }).join("-");
306
+ }
307
+ /**
308
+ * Convert a keyboard event to a normalized string.
309
+ */
310
+ eventToString(event) {
311
+ const modifiers = [];
312
+ if (event.ctrlKey) modifiers.push("ctrl");
313
+ if (event.metaKey) modifiers.push("meta");
314
+ if (event.altKey) modifiers.push("alt");
315
+ if (event.shiftKey) modifiers.push("shift");
316
+ let key = event.key.toLowerCase();
317
+ if (key === " ") key = "space";
318
+ if (key === "arrowup") key = "up";
319
+ if (key === "arrowdown") key = "down";
320
+ if (key === "arrowleft") key = "left";
321
+ if (key === "arrowright") key = "right";
322
+ if (["control", "meta", "alt", "shift"].includes(key)) {
323
+ return "";
324
+ }
325
+ return [...modifiers.sort(), key].join("-");
326
+ }
327
+ /**
328
+ * Clear all registered shortcuts.
329
+ */
330
+ clear() {
331
+ this.shortcuts.clear();
332
+ }
333
+ };
334
+ var globalShortcutManager = null;
335
+ function getShortcutManager() {
336
+ if (!globalShortcutManager) {
337
+ globalShortcutManager = new ShortcutManager();
338
+ }
339
+ return globalShortcutManager;
340
+ }
341
+ function installShortcutHandler() {
342
+ const manager = getShortcutManager();
343
+ const handler = (event) => {
344
+ manager.handleKeyDown(event);
345
+ };
346
+ window.addEventListener("keydown", handler, true);
347
+ return () => {
348
+ window.removeEventListener("keydown", handler, true);
349
+ };
350
+ }
351
+
352
+ // src/middleware.ts
353
+ var MiddlewareChain = class {
354
+ middlewares = [];
355
+ /**
356
+ * Add a middleware to the chain
357
+ */
358
+ add(middleware) {
359
+ this.middlewares.push(middleware);
360
+ this.middlewares.sort((a, b) => (a.priority ?? 100) - (b.priority ?? 100));
361
+ return {
362
+ dispose: () => {
363
+ this.middlewares = this.middlewares.filter((m) => m.id !== middleware.id);
364
+ }
365
+ };
366
+ }
367
+ /**
368
+ * Remove a middleware by ID
369
+ */
370
+ remove(id) {
371
+ const before = this.middlewares.length;
372
+ this.middlewares = this.middlewares.filter((m) => m.id !== id);
373
+ return this.middlewares.length < before;
374
+ }
375
+ /**
376
+ * Get all registered middlewares
377
+ */
378
+ getAll() {
379
+ return [...this.middlewares];
380
+ }
381
+ /**
382
+ * Execute beforeChange hooks in priority order
383
+ */
384
+ async executeBefore(change, apply) {
385
+ const chain = this.middlewares.filter((m) => m.beforeChange);
386
+ const execute = async (index) => {
387
+ if (index >= chain.length) {
388
+ return apply();
389
+ }
390
+ return chain[index].beforeChange(change, () => execute(index + 1));
391
+ };
392
+ return execute(0);
393
+ }
394
+ /**
395
+ * Execute afterChange hooks (errors are caught and logged)
396
+ */
397
+ executeAfter(event) {
398
+ for (const middleware of this.middlewares) {
399
+ if (middleware.afterChange) {
400
+ try {
401
+ middleware.afterChange(event);
402
+ } catch (err) {
403
+ console.error(`[Middleware ${middleware.id}] afterChange error:`, err);
404
+ }
405
+ }
406
+ }
407
+ }
408
+ /**
409
+ * Clear all middlewares
410
+ */
411
+ clear() {
412
+ this.middlewares = [];
413
+ }
414
+ /**
415
+ * Get the number of registered middlewares
416
+ */
417
+ get size() {
418
+ return this.middlewares.length;
419
+ }
420
+ };
421
+
422
+ // src/context.ts
423
+ function createExtensionContext(options) {
424
+ const { pluginId, store, contributions, platform, middlewareChain } = options;
425
+ const disposables = [];
426
+ const storage = createExtensionStorage();
427
+ const ctx = {
428
+ pluginId,
429
+ platform,
430
+ store,
431
+ async query(schema, filter) {
432
+ const nodes = await store.list();
433
+ let filtered = schema ? nodes.filter((n) => n.schemaId === schema) : nodes;
434
+ if (filter?.where) {
435
+ filtered = filtered.filter((node) => {
436
+ for (const [key, value] of Object.entries(filter.where)) {
437
+ if (node[key] !== value) return false;
438
+ }
439
+ return true;
440
+ });
441
+ }
442
+ if (filter?.offset) {
443
+ filtered = filtered.slice(filter.offset);
444
+ }
445
+ if (filter?.limit) {
446
+ filtered = filtered.slice(0, filter.limit);
447
+ }
448
+ return filtered;
449
+ },
450
+ subscribe(schema, callback) {
451
+ const storeCallback = (event) => {
452
+ if (!schema || event.node?.schemaId === schema) {
453
+ const change = event.change;
454
+ callback({
455
+ type: change.type === "node-delete" ? "delete" : change.type === "node-change" ? "create" : "update",
456
+ nodeId: change.payload.nodeId,
457
+ node: event.node ?? void 0,
458
+ changes: void 0
459
+ // Raw change available via event.change if needed
460
+ });
461
+ }
462
+ };
463
+ const unsub = store.subscribe(storeCallback);
464
+ const disposable = { dispose: unsub };
465
+ disposables.push(disposable);
466
+ return disposable;
467
+ },
468
+ registerSchema(_schema) {
469
+ const d = {
470
+ dispose: () => {
471
+ }
472
+ };
473
+ disposables.push(d);
474
+ return d;
475
+ },
476
+ registerView(view) {
477
+ const d = contributions.views.register(view);
478
+ disposables.push(d);
479
+ return d;
480
+ },
481
+ registerPropertyHandler(type, handler) {
482
+ const d = contributions.propertyHandlers.register({ type, handler });
483
+ disposables.push(d);
484
+ return d;
485
+ },
486
+ registerCommand(command) {
487
+ const d = contributions.commands.register(command);
488
+ disposables.push(d);
489
+ return d;
490
+ },
491
+ registerSidebarItem(item) {
492
+ const d = contributions.sidebar.register(item);
493
+ disposables.push(d);
494
+ return d;
495
+ },
496
+ registerEditorExtension(ext) {
497
+ const d = contributions.editor.register(ext);
498
+ disposables.push(d);
499
+ return d;
500
+ },
501
+ registerSlashCommand(cmd) {
502
+ const d = contributions.slashCommands.register(cmd);
503
+ disposables.push(d);
504
+ return d;
505
+ },
506
+ registerBlockType(block) {
507
+ const d = contributions.blocks.register(block);
508
+ disposables.push(d);
509
+ return d;
510
+ },
511
+ addMiddleware(middleware) {
512
+ if (!middlewareChain) {
513
+ console.warn(`[Plugin ${pluginId}] Middleware not available on this platform`);
514
+ return { dispose: () => {
515
+ } };
516
+ }
517
+ const namespacedMiddleware = {
518
+ ...middleware,
519
+ id: `${pluginId}:${middleware.id}`
520
+ };
521
+ const d = middlewareChain.add(namespacedMiddleware);
522
+ disposables.push(d);
523
+ return d;
524
+ },
525
+ storage,
526
+ capabilities: getPlatformCapabilities(platform),
527
+ subscriptions: disposables
528
+ };
529
+ return ctx;
530
+ }
531
+
532
+ // src/schemas/plugin.ts
533
+ import { defineSchema, text, checkbox, date } from "@xnetjs/data";
534
+ var PluginSchema = defineSchema({
535
+ name: "Plugin",
536
+ namespace: "xnet://xnet.fyi/",
537
+ properties: {
538
+ /** Unique plugin identifier (reverse-domain format) */
539
+ pluginId: text({ required: true }),
540
+ /** Human-readable name */
541
+ name: text({ required: true }),
542
+ /** Semantic version */
543
+ version: text({ required: true }),
544
+ /** Plugin description */
545
+ description: text({}),
546
+ /** Author name or organization */
547
+ author: text({}),
548
+ /** Whether plugin is enabled */
549
+ enabled: checkbox({ default: true }),
550
+ /** JSON-serialized manifest */
551
+ manifest: text({ required: true }),
552
+ /** Plugin source (URL or inline bundle) */
553
+ source: text({}),
554
+ /** JSON-serialized permissions */
555
+ permissions: text({}),
556
+ /** Installation timestamp */
557
+ installedAt: date({})
558
+ }
559
+ });
560
+
561
+ // src/registry.ts
562
+ var PluginError = class extends Error {
563
+ constructor(message) {
564
+ super(message);
565
+ this.name = "PluginError";
566
+ }
567
+ };
568
+ var PluginRegistry = class {
569
+ constructor(store, platform) {
570
+ this.store = store;
571
+ this.platform = platform;
572
+ }
573
+ plugins = /* @__PURE__ */ new Map();
574
+ contributions = new ContributionRegistry();
575
+ middleware = new MiddlewareChain();
576
+ listeners = /* @__PURE__ */ new Set();
577
+ // ─── Lifecycle ─────────────────────────────────────────────────────────
578
+ /**
579
+ * Install and activate a plugin
580
+ */
581
+ async install(manifest) {
582
+ try {
583
+ validateManifest(manifest);
584
+ } catch (err) {
585
+ if (err instanceof PluginValidationError) {
586
+ throw new PluginError(`Invalid manifest: ${err.issues.join(", ")}`);
587
+ }
588
+ throw err;
589
+ }
590
+ if (manifest.platforms && !manifest.platforms.includes(this.platform)) {
591
+ throw new PluginError(
592
+ `Plugin '${manifest.id}' requires platforms: ${manifest.platforms.join(", ")} (current: ${this.platform})`
593
+ );
594
+ }
595
+ if (this.plugins.has(manifest.id)) {
596
+ throw new PluginError(`Plugin '${manifest.id}' is already installed`);
597
+ }
598
+ await this.store.create({
599
+ schemaId: PluginSchema._schemaId,
600
+ properties: {
601
+ pluginId: manifest.id,
602
+ name: manifest.name,
603
+ version: manifest.version,
604
+ description: manifest.description ?? "",
605
+ author: manifest.author ?? "",
606
+ enabled: true,
607
+ manifest: JSON.stringify(manifest),
608
+ installedAt: Date.now()
609
+ }
610
+ });
611
+ this.plugins.set(manifest.id, { manifest, status: "installed" });
612
+ this.notify();
613
+ await this.activate(manifest.id);
614
+ }
615
+ /**
616
+ * Activate an installed plugin
617
+ */
618
+ async activate(pluginId) {
619
+ const plugin = this.plugins.get(pluginId);
620
+ if (!plugin) {
621
+ throw new PluginError(`Plugin '${pluginId}' not found`);
622
+ }
623
+ if (plugin.status === "active") {
624
+ return;
625
+ }
626
+ try {
627
+ const context = createExtensionContext({
628
+ pluginId,
629
+ store: this.store,
630
+ contributions: this.contributions,
631
+ platform: this.platform,
632
+ middlewareChain: this.middleware
633
+ });
634
+ this.registerStaticContributions(plugin.manifest, context);
635
+ if (plugin.manifest.activate) {
636
+ await plugin.manifest.activate(context);
637
+ }
638
+ plugin.context = context;
639
+ plugin.status = "active";
640
+ plugin.error = void 0;
641
+ this.notify();
642
+ } catch (err) {
643
+ plugin.status = "error";
644
+ plugin.error = err instanceof Error ? err : new Error(String(err));
645
+ console.error(`Plugin '${pluginId}' activation failed:`, err);
646
+ this.notify();
647
+ throw plugin.error;
648
+ }
649
+ }
650
+ /**
651
+ * Deactivate an active plugin
652
+ */
653
+ async deactivate(pluginId) {
654
+ const plugin = this.plugins.get(pluginId);
655
+ if (!plugin || plugin.status !== "active") {
656
+ return;
657
+ }
658
+ if (plugin.manifest.deactivate) {
659
+ try {
660
+ await plugin.manifest.deactivate();
661
+ } catch (err) {
662
+ console.error(`Plugin '${pluginId}' deactivate error:`, err);
663
+ }
664
+ }
665
+ if (plugin.context) {
666
+ for (const d of plugin.context.subscriptions) {
667
+ try {
668
+ d.dispose();
669
+ } catch (err) {
670
+ console.error(`Error disposing subscription for plugin '${pluginId}':`, err);
671
+ }
672
+ }
673
+ }
674
+ plugin.context = void 0;
675
+ plugin.status = "disabled";
676
+ this.notify();
677
+ }
678
+ /**
679
+ * Uninstall a plugin (deactivates first if active)
680
+ */
681
+ async uninstall(pluginId) {
682
+ await this.deactivate(pluginId);
683
+ this.plugins.delete(pluginId);
684
+ const nodes = await this.store.list();
685
+ const pluginNode = nodes.find(
686
+ (n) => n.schemaId === PluginSchema._schemaId && n.properties.pluginId === pluginId
687
+ );
688
+ if (pluginNode) {
689
+ await this.store.delete(pluginNode.id);
690
+ }
691
+ this.notify();
692
+ }
693
+ // ─── Queries ───────────────────────────────────────────────────────────
694
+ /**
695
+ * Get all registered plugins
696
+ */
697
+ getAll() {
698
+ return [...this.plugins.values()];
699
+ }
700
+ /**
701
+ * Get a specific plugin
702
+ */
703
+ get(pluginId) {
704
+ return this.plugins.get(pluginId);
705
+ }
706
+ /**
707
+ * Check if a plugin is installed
708
+ */
709
+ has(pluginId) {
710
+ return this.plugins.has(pluginId);
711
+ }
712
+ /**
713
+ * Rehydrate a plugin loaded from store with a live manifest.
714
+ *
715
+ * When plugins are persisted via `install()`, the manifest is serialized
716
+ * with `JSON.stringify()`, which strips non-serializable values like
717
+ * TipTap Extension instances, React components, and functions. When
718
+ * `loadFromStore()` deserializes with `JSON.parse()`, these values are
719
+ * lost and the plugin's contributions (e.g., editor extensions) are broken.
720
+ *
721
+ * For bundled plugins, the `BundledPluginInstaller` has access to the live
722
+ * manifest objects. This method replaces the deserialized manifest with the
723
+ * live one and re-registers its static contributions so that extension
724
+ * objects with methods (like `renderHTML`, `addNodeView`) are properly
725
+ * available to the editor.
726
+ */
727
+ async rehydrate(liveManifest) {
728
+ const plugin = this.plugins.get(liveManifest.id);
729
+ if (!plugin) return;
730
+ if (plugin.status === "active") {
731
+ await this.deactivate(liveManifest.id);
732
+ }
733
+ plugin.manifest = liveManifest;
734
+ plugin.status = "installed";
735
+ await this.activate(liveManifest.id);
736
+ }
737
+ /**
738
+ * Get contribution registry
739
+ */
740
+ getContributions() {
741
+ return this.contributions;
742
+ }
743
+ /**
744
+ * Get middleware chain
745
+ */
746
+ getMiddleware() {
747
+ return this.middleware;
748
+ }
749
+ // ─── Events ────────────────────────────────────────────────────────────
750
+ /**
751
+ * Subscribe to plugin changes
752
+ */
753
+ onChange(listener) {
754
+ this.listeners.add(listener);
755
+ return {
756
+ dispose: () => this.listeners.delete(listener)
757
+ };
758
+ }
759
+ notify() {
760
+ for (const listener of this.listeners) {
761
+ try {
762
+ listener();
763
+ } catch (err) {
764
+ console.error("[PluginRegistry] Listener error:", err);
765
+ }
766
+ }
767
+ }
768
+ // ─── Static Contributions ──────────────────────────────────────────────
769
+ registerStaticContributions(manifest, ctx) {
770
+ const c = manifest.contributes;
771
+ if (!c) return;
772
+ if (c.views) {
773
+ for (const view of c.views) {
774
+ ctx.registerView(view);
775
+ }
776
+ }
777
+ if (c.commands) {
778
+ for (const cmd of c.commands) {
779
+ ctx.registerCommand(cmd);
780
+ }
781
+ }
782
+ if (c.slashCommands) {
783
+ for (const cmd of c.slashCommands) {
784
+ ctx.registerSlashCommand(cmd);
785
+ }
786
+ }
787
+ if (c.sidebarItems) {
788
+ for (const item of c.sidebarItems) {
789
+ ctx.registerSidebarItem(item);
790
+ }
791
+ }
792
+ if (c.editorExtensions) {
793
+ for (const ext of c.editorExtensions) {
794
+ ctx.registerEditorExtension(ext);
795
+ }
796
+ }
797
+ if (c.propertyHandlers) {
798
+ for (const h of c.propertyHandlers) {
799
+ ctx.registerPropertyHandler(h.type, h.handler);
800
+ }
801
+ }
802
+ if (c.blocks) {
803
+ for (const block of c.blocks) {
804
+ ctx.registerBlockType(block);
805
+ }
806
+ }
807
+ }
808
+ // ─── Load from Store ───────────────────────────────────────────────────
809
+ /**
810
+ * Load and activate plugins from stored Nodes
811
+ */
812
+ async loadFromStore() {
813
+ const nodes = await this.store.list();
814
+ const pluginNodes = nodes.filter((n) => n.schemaId === PluginSchema._schemaId);
815
+ for (const node of pluginNodes) {
816
+ const props = node.properties;
817
+ if (!props.enabled) continue;
818
+ try {
819
+ const manifest = JSON.parse(props.manifest);
820
+ if (this.plugins.has(manifest.id)) continue;
821
+ this.plugins.set(manifest.id, { manifest, status: "installed" });
822
+ await this.activate(manifest.id);
823
+ } catch (err) {
824
+ console.error(`Failed to load plugin '${props.pluginId}':`, err);
825
+ }
826
+ }
827
+ }
828
+ };
829
+
830
+ // src/schemas/script.ts
831
+ import { defineSchema as defineSchema2, text as text2, checkbox as checkbox2, date as date2, select } from "@xnetjs/data";
832
+ var TRIGGER_OPTIONS = [
833
+ { id: "manual", name: "Manual", color: "gray" },
834
+ { id: "onChange", name: "On Change", color: "blue" },
835
+ { id: "onView", name: "On View", color: "green" },
836
+ { id: "scheduled", name: "Scheduled", color: "purple" }
837
+ ];
838
+ var OUTPUT_OPTIONS = [
839
+ { id: "value", name: "Value", color: "blue" },
840
+ { id: "mutation", name: "Mutation", color: "orange" },
841
+ { id: "decoration", name: "Decoration", color: "purple" },
842
+ { id: "void", name: "Void", color: "gray" }
843
+ ];
844
+ var ScriptSchema = defineSchema2({
845
+ name: "Script",
846
+ namespace: "xnet://xnet.dev/",
847
+ properties: {
848
+ /** Human-readable script name */
849
+ name: text2({ required: true }),
850
+ /** Description of what the script does */
851
+ description: text2({}),
852
+ /** The script code (JavaScript expression or arrow function) */
853
+ code: text2({ required: true }),
854
+ /** When the script should execute */
855
+ triggerType: select({
856
+ options: TRIGGER_OPTIONS,
857
+ default: "manual"
858
+ }),
859
+ /** For onChange trigger: which property to watch (empty = any) */
860
+ triggerProperty: text2({}),
861
+ /** For scheduled trigger: cron expression */
862
+ triggerCron: text2({}),
863
+ /** Schema IRI this script operates on (e.g., 'xnet://myapp/Task') */
864
+ inputSchema: text2({}),
865
+ /** What the script returns */
866
+ outputType: select({
867
+ options: OUTPUT_OPTIONS,
868
+ default: "value"
869
+ }),
870
+ /** Whether the script is active */
871
+ enabled: checkbox2({ default: true }),
872
+ /** Last execution error message (null if last run succeeded) */
873
+ lastError: text2({}),
874
+ /** Timestamp of last execution */
875
+ lastRun: date2({})
876
+ }
877
+ });
878
+ function isScriptNode(node) {
879
+ if (!node || typeof node !== "object") return false;
880
+ const n = node;
881
+ return typeof n.id === "string" && typeof n.name === "string" && typeof n.code === "string" && typeof n.triggerType === "string" && typeof n.outputType === "string" && typeof n.enabled === "boolean";
882
+ }
883
+
884
+ // src/sandbox/context.ts
885
+ function createFormatHelpers() {
886
+ return {
887
+ date: (ts, options) => {
888
+ try {
889
+ return new Intl.DateTimeFormat("en-US", options).format(new Date(ts));
890
+ } catch {
891
+ return String(ts);
892
+ }
893
+ },
894
+ number: (val, options) => {
895
+ try {
896
+ return new Intl.NumberFormat("en-US", options).format(val);
897
+ } catch {
898
+ return String(val);
899
+ }
900
+ },
901
+ currency: (val, currency = "USD", locale = "en-US") => {
902
+ try {
903
+ return new Intl.NumberFormat(locale, {
904
+ style: "currency",
905
+ currency
906
+ }).format(val);
907
+ } catch {
908
+ return `${currency} ${val}`;
909
+ }
910
+ },
911
+ relative: (ts) => {
912
+ const diff = Date.now() - ts;
913
+ const absDiff = Math.abs(diff);
914
+ const suffix = diff >= 0 ? "ago" : "from now";
915
+ const seconds = Math.floor(absDiff / 1e3);
916
+ if (seconds < 60) return `${seconds}s ${suffix}`;
917
+ const minutes = Math.floor(seconds / 60);
918
+ if (minutes < 60) return `${minutes}m ${suffix}`;
919
+ const hours = Math.floor(minutes / 60);
920
+ if (hours < 24) return `${hours}h ${suffix}`;
921
+ const days = Math.floor(hours / 24);
922
+ if (days < 30) return `${days}d ${suffix}`;
923
+ const months = Math.floor(days / 30);
924
+ if (months < 12) return `${months}mo ${suffix}`;
925
+ const years = Math.floor(months / 12);
926
+ return `${years}y ${suffix}`;
927
+ },
928
+ bytes: (bytes) => {
929
+ const units = ["B", "KB", "MB", "GB", "TB"];
930
+ let size = Math.abs(bytes);
931
+ let unitIndex = 0;
932
+ while (size >= 1024 && unitIndex < units.length - 1) {
933
+ size /= 1024;
934
+ unitIndex++;
935
+ }
936
+ return `${size.toFixed(unitIndex === 0 ? 0 : 1)} ${units[unitIndex]}`;
937
+ }
938
+ };
939
+ }
940
+ function createMathHelpers() {
941
+ return {
942
+ sum: (vals) => {
943
+ if (!Array.isArray(vals) || vals.length === 0) return 0;
944
+ return vals.reduce((a, b) => a + (Number(b) || 0), 0);
945
+ },
946
+ avg: (vals) => {
947
+ if (!Array.isArray(vals) || vals.length === 0) return 0;
948
+ const sum = vals.reduce((a, b) => a + (Number(b) || 0), 0);
949
+ return sum / vals.length;
950
+ },
951
+ min: (vals) => {
952
+ if (!Array.isArray(vals) || vals.length === 0) return 0;
953
+ const numbers = vals.map((v) => Number(v) || 0);
954
+ return Math.min(...numbers);
955
+ },
956
+ max: (vals) => {
957
+ if (!Array.isArray(vals) || vals.length === 0) return 0;
958
+ const numbers = vals.map((v) => Number(v) || 0);
959
+ return Math.max(...numbers);
960
+ },
961
+ round: (val, decimals = 0) => {
962
+ const factor = Math.pow(10, decimals);
963
+ return Math.round(val * factor) / factor;
964
+ },
965
+ clamp: (val, min, max) => Math.min(Math.max(val, min), max),
966
+ abs: (val) => Math.abs(val),
967
+ floor: (val) => Math.floor(val),
968
+ ceil: (val) => Math.ceil(val)
969
+ };
970
+ }
971
+ function createTextHelpers() {
972
+ return {
973
+ slugify: (t) => String(t || "").toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, ""),
974
+ truncate: (t, len) => {
975
+ const str = String(t || "");
976
+ return str.length > len ? str.slice(0, len) + "..." : str;
977
+ },
978
+ capitalize: (t) => {
979
+ const str = String(t || "");
980
+ return str.charAt(0).toUpperCase() + str.slice(1);
981
+ },
982
+ titleCase: (t) => String(t || "").toLowerCase().replace(/\b\w/g, (c) => c.toUpperCase()),
983
+ contains: (t, s) => String(t || "").toLowerCase().includes(String(s || "").toLowerCase()),
984
+ template: (tmpl, vars) => String(tmpl || "").replace(/\{(\w+)\}/g, (_, key) => {
985
+ const val = vars[key];
986
+ return val !== void 0 && val !== null ? String(val) : "";
987
+ }),
988
+ trim: (t) => String(t || "").trim(),
989
+ lower: (t) => String(t || "").toLowerCase(),
990
+ upper: (t) => String(t || "").toUpperCase()
991
+ };
992
+ }
993
+ function createArrayHelpers() {
994
+ return {
995
+ first: (items) => Array.isArray(items) ? items[0] : void 0,
996
+ last: (items) => Array.isArray(items) ? items[items.length - 1] : void 0,
997
+ sortBy: (items, key, desc = false) => {
998
+ if (!Array.isArray(items)) return [];
999
+ return [...items].sort((a, b) => {
1000
+ const aVal = a[key];
1001
+ const bVal = b[key];
1002
+ if (aVal < bVal) return desc ? 1 : -1;
1003
+ if (aVal > bVal) return desc ? -1 : 1;
1004
+ return 0;
1005
+ });
1006
+ },
1007
+ groupBy: (items, key) => {
1008
+ if (!Array.isArray(items)) return {};
1009
+ const groups = {};
1010
+ for (const item of items) {
1011
+ const groupKey = String(item[key] ?? "undefined");
1012
+ if (!groups[groupKey]) groups[groupKey] = [];
1013
+ groups[groupKey].push(item);
1014
+ }
1015
+ return groups;
1016
+ },
1017
+ unique: (items) => {
1018
+ if (!Array.isArray(items)) return [];
1019
+ return [...new Set(items)];
1020
+ },
1021
+ count: (items) => Array.isArray(items) ? items.length : 0,
1022
+ compact: (items) => {
1023
+ if (!Array.isArray(items)) return [];
1024
+ return items.filter((item) => item != null);
1025
+ }
1026
+ };
1027
+ }
1028
+ function deepFreeze(obj) {
1029
+ if (obj === null || typeof obj !== "object") return obj;
1030
+ if (Array.isArray(obj)) {
1031
+ for (const item of obj) {
1032
+ deepFreeze(item);
1033
+ }
1034
+ return Object.freeze(obj);
1035
+ }
1036
+ for (const key of Object.keys(obj)) {
1037
+ const value = obj[key];
1038
+ if (value !== null && typeof value === "object") {
1039
+ deepFreeze(value);
1040
+ }
1041
+ }
1042
+ return Object.freeze(obj);
1043
+ }
1044
+ function createScriptContext(node, queryFn) {
1045
+ const frozenNode = deepFreeze({ ...node });
1046
+ const format = Object.freeze(createFormatHelpers());
1047
+ const math = Object.freeze(createMathHelpers());
1048
+ const text3 = Object.freeze(createTextHelpers());
1049
+ const array = Object.freeze(createArrayHelpers());
1050
+ const context = {
1051
+ node: frozenNode,
1052
+ nodes: (schemaIRI) => {
1053
+ const results = queryFn(schemaIRI);
1054
+ return Object.freeze(results.map((n) => deepFreeze({ ...n })));
1055
+ },
1056
+ now: () => Date.now(),
1057
+ format,
1058
+ math,
1059
+ text: text3,
1060
+ array
1061
+ };
1062
+ return Object.freeze(context);
1063
+ }
1064
+
1065
+ // src/sandbox/ast-validator.ts
1066
+ import * as acorn from "acorn";
1067
+ var FORBIDDEN_GLOBALS = /* @__PURE__ */ new Set([
1068
+ // Browser globals
1069
+ "window",
1070
+ "document",
1071
+ "globalThis",
1072
+ "self",
1073
+ "parent",
1074
+ "top",
1075
+ "frames",
1076
+ // Network access
1077
+ "fetch",
1078
+ "XMLHttpRequest",
1079
+ "WebSocket",
1080
+ "EventSource",
1081
+ "Request",
1082
+ "Response",
1083
+ // Storage
1084
+ "localStorage",
1085
+ "sessionStorage",
1086
+ "indexedDB",
1087
+ "caches",
1088
+ // Code execution (dangerous)
1089
+ "eval",
1090
+ "Function",
1091
+ // Node.js globals
1092
+ "require",
1093
+ "module",
1094
+ "exports",
1095
+ "process",
1096
+ "Buffer",
1097
+ "__dirname",
1098
+ "__filename",
1099
+ // Timers (could be used for timing attacks or resource exhaustion)
1100
+ "setTimeout",
1101
+ "setInterval",
1102
+ "setImmediate",
1103
+ "requestAnimationFrame",
1104
+ "requestIdleCallback",
1105
+ "clearTimeout",
1106
+ "clearInterval",
1107
+ "clearImmediate",
1108
+ "cancelAnimationFrame",
1109
+ "cancelIdleCallback",
1110
+ // Browser APIs
1111
+ "navigator",
1112
+ "location",
1113
+ "history",
1114
+ "screen",
1115
+ "alert",
1116
+ "confirm",
1117
+ "prompt",
1118
+ "open",
1119
+ "close",
1120
+ "print",
1121
+ // Web Workers
1122
+ "Worker",
1123
+ "SharedWorker",
1124
+ "ServiceWorker",
1125
+ // Other dangerous APIs
1126
+ "Proxy",
1127
+ "Reflect",
1128
+ "SharedArrayBuffer",
1129
+ "Atomics",
1130
+ "WebAssembly",
1131
+ "crypto",
1132
+ "performance"
1133
+ ]);
1134
+ var FORBIDDEN_PROPERTIES = /* @__PURE__ */ new Set([
1135
+ "__proto__",
1136
+ "constructor",
1137
+ "prototype",
1138
+ "__defineGetter__",
1139
+ "__defineSetter__",
1140
+ "__lookupGetter__",
1141
+ "__lookupSetter__"
1142
+ ]);
1143
+ function walkAST(node, visitors) {
1144
+ if (!node || typeof node !== "object") return;
1145
+ const nodeType = node.type;
1146
+ const visitor = visitors[nodeType];
1147
+ if (visitor) {
1148
+ visitor(node);
1149
+ }
1150
+ for (const key of Object.keys(node)) {
1151
+ if (key === "type" || key === "start" || key === "end" || key === "loc" || key === "range") {
1152
+ continue;
1153
+ }
1154
+ const child = node[key];
1155
+ if (Array.isArray(child)) {
1156
+ for (const item of child) {
1157
+ if (item && typeof item === "object" && "type" in item) {
1158
+ walkAST(item, visitors);
1159
+ }
1160
+ }
1161
+ } else if (child && typeof child === "object" && "type" in child) {
1162
+ walkAST(child, visitors);
1163
+ }
1164
+ }
1165
+ }
1166
+ function validateScriptAST(code) {
1167
+ const errors = [];
1168
+ const declaredVars = /* @__PURE__ */ new Set();
1169
+ try {
1170
+ const trimmed = code.trim();
1171
+ const wrappedCode = trimmed.startsWith("(") || trimmed.startsWith("function") ? trimmed : `(${trimmed})`;
1172
+ const ast = acorn.parse(wrappedCode, {
1173
+ ecmaVersion: 2022,
1174
+ sourceType: "script",
1175
+ // No import/export
1176
+ allowReturnOutsideFunction: true
1177
+ });
1178
+ walkAST(ast, {
1179
+ VariableDeclarator: (node) => {
1180
+ if (node.id?.type === "Identifier") {
1181
+ declaredVars.add(node.id.name);
1182
+ }
1183
+ },
1184
+ FunctionDeclaration: (node) => {
1185
+ if (node.id?.name) {
1186
+ declaredVars.add(node.id.name);
1187
+ }
1188
+ },
1189
+ FunctionExpression: (node) => {
1190
+ if (node.id?.name) {
1191
+ declaredVars.add(node.id.name);
1192
+ }
1193
+ },
1194
+ ArrowFunctionExpression: (node) => {
1195
+ for (const param of node.params || []) {
1196
+ if (param.type === "Identifier") {
1197
+ declaredVars.add(param.name);
1198
+ }
1199
+ }
1200
+ }
1201
+ });
1202
+ walkAST(ast, {
1203
+ FunctionDeclaration: (node) => {
1204
+ for (const param of node.params || []) {
1205
+ if (param.type === "Identifier") {
1206
+ declaredVars.add(param.name);
1207
+ }
1208
+ }
1209
+ },
1210
+ FunctionExpression: (node) => {
1211
+ for (const param of node.params || []) {
1212
+ if (param.type === "Identifier") {
1213
+ declaredVars.add(param.name);
1214
+ }
1215
+ }
1216
+ }
1217
+ });
1218
+ walkAST(ast, {
1219
+ VariableDeclarator: (node) => {
1220
+ if (node.id.type === "Identifier") {
1221
+ declaredVars.add(node.id.name);
1222
+ }
1223
+ },
1224
+ FunctionDeclaration: (node) => {
1225
+ if (node.id?.name) {
1226
+ declaredVars.add(node.id.name);
1227
+ }
1228
+ },
1229
+ FunctionExpression: (node) => {
1230
+ if (node.id?.name) {
1231
+ declaredVars.add(node.id.name);
1232
+ }
1233
+ },
1234
+ ArrowFunctionExpression: (node) => {
1235
+ for (const param of node.params) {
1236
+ if (param.type === "Identifier") {
1237
+ declaredVars.add(param.name);
1238
+ }
1239
+ }
1240
+ }
1241
+ });
1242
+ walkAST(ast, {
1243
+ FunctionDeclaration: (node) => {
1244
+ for (const param of node.params) {
1245
+ if (param.type === "Identifier") {
1246
+ declaredVars.add(param.name);
1247
+ }
1248
+ }
1249
+ },
1250
+ FunctionExpression: (node) => {
1251
+ for (const param of node.params) {
1252
+ if (param.type === "Identifier") {
1253
+ declaredVars.add(param.name);
1254
+ }
1255
+ }
1256
+ }
1257
+ });
1258
+ walkAST(ast, {
1259
+ // Check for forbidden globals
1260
+ Identifier: (node) => {
1261
+ const name = node.name;
1262
+ if (declaredVars.has(name)) return;
1263
+ const safeBuiltins = /* @__PURE__ */ new Set([
1264
+ "undefined",
1265
+ "null",
1266
+ "true",
1267
+ "false",
1268
+ "NaN",
1269
+ "Infinity",
1270
+ "Array",
1271
+ "Object",
1272
+ "String",
1273
+ "Number",
1274
+ "Boolean",
1275
+ "Date",
1276
+ "Math",
1277
+ "JSON",
1278
+ "Error",
1279
+ "TypeError",
1280
+ "RangeError",
1281
+ "Map",
1282
+ "Set",
1283
+ "WeakMap",
1284
+ "WeakSet",
1285
+ "Symbol",
1286
+ "BigInt",
1287
+ "Promise",
1288
+ "parseInt",
1289
+ "parseFloat",
1290
+ "isNaN",
1291
+ "isFinite",
1292
+ "encodeURI",
1293
+ "encodeURIComponent",
1294
+ "decodeURI",
1295
+ "decodeURIComponent"
1296
+ ]);
1297
+ if (safeBuiltins.has(name)) return;
1298
+ if (FORBIDDEN_GLOBALS.has(name)) {
1299
+ errors.push(`Forbidden global access: '${name}' at position ${node.start}`);
1300
+ }
1301
+ },
1302
+ // No import statements
1303
+ ImportDeclaration: (node) => {
1304
+ errors.push(`Import statements are not allowed at position ${node.start}`);
1305
+ },
1306
+ // No export statements
1307
+ ExportNamedDeclaration: (node) => {
1308
+ errors.push(`Export statements are not allowed at position ${node.start}`);
1309
+ },
1310
+ ExportDefaultDeclaration: (node) => {
1311
+ errors.push(`Export statements are not allowed at position ${node.start}`);
1312
+ },
1313
+ ExportAllDeclaration: (node) => {
1314
+ errors.push(`Export statements are not allowed at position ${node.start}`);
1315
+ },
1316
+ // No dynamic import()
1317
+ ImportExpression: (node) => {
1318
+ errors.push(`Dynamic import() is not allowed at position ${node.start}`);
1319
+ },
1320
+ // No async/await (scripts must be synchronous for predictability)
1321
+ AwaitExpression: (node) => {
1322
+ errors.push(`Async/await is not allowed at position ${node.start}`);
1323
+ },
1324
+ // Check for async functions
1325
+ FunctionDeclaration: (node) => {
1326
+ if (node.async) {
1327
+ errors.push(`Async functions are not allowed at position ${node.start}`);
1328
+ }
1329
+ },
1330
+ FunctionExpression: (node) => {
1331
+ if (node.async) {
1332
+ errors.push(`Async functions are not allowed at position ${node.start}`);
1333
+ }
1334
+ },
1335
+ ArrowFunctionExpression: (node) => {
1336
+ if (node.async) {
1337
+ errors.push(`Async arrow functions are not allowed at position ${node.start}`);
1338
+ }
1339
+ },
1340
+ // No new Function()
1341
+ NewExpression: (node) => {
1342
+ if (node.callee?.type === "Identifier" && node.callee.name === "Function") {
1343
+ errors.push(`new Function() is not allowed at position ${node.start}`);
1344
+ }
1345
+ },
1346
+ // Check for forbidden property access
1347
+ MemberExpression: (node) => {
1348
+ if (node.computed && node.property?.type === "Literal") {
1349
+ const propName = String(node.property.value);
1350
+ if (FORBIDDEN_PROPERTIES.has(propName)) {
1351
+ errors.push(`Access to '${propName}' is forbidden at position ${node.property.start}`);
1352
+ }
1353
+ }
1354
+ if (!node.computed && node.property?.type === "Identifier") {
1355
+ if (FORBIDDEN_PROPERTIES.has(node.property.name)) {
1356
+ errors.push(
1357
+ `Access to '${node.property.name}' is forbidden at position ${node.property.start}`
1358
+ );
1359
+ }
1360
+ }
1361
+ },
1362
+ // No with statements
1363
+ WithStatement: (node) => {
1364
+ errors.push(`'with' statements are not allowed at position ${node.start}`);
1365
+ },
1366
+ // Check for eval() calls
1367
+ CallExpression: (node) => {
1368
+ if (node.callee?.type === "Identifier" && node.callee.name === "eval") {
1369
+ errors.push(`eval() is not allowed at position ${node.start}`);
1370
+ }
1371
+ }
1372
+ });
1373
+ } catch (err) {
1374
+ const message = err instanceof Error ? err.message : String(err);
1375
+ errors.push(`Syntax error: ${message}`);
1376
+ }
1377
+ return {
1378
+ valid: errors.length === 0,
1379
+ errors
1380
+ };
1381
+ }
1382
+ function quickSafetyCheck(code) {
1383
+ const lowerCode = code.toLowerCase();
1384
+ const dangerousPatterns = [
1385
+ "import ",
1386
+ "require(",
1387
+ "eval(",
1388
+ "__proto__",
1389
+ "constructor(",
1390
+ "function(",
1391
+ "fetch(",
1392
+ "xmlhttprequest",
1393
+ "websocket",
1394
+ "localstorage",
1395
+ "sessionstorage",
1396
+ "indexeddb",
1397
+ "document.",
1398
+ "window.",
1399
+ "globalthis",
1400
+ "process.",
1401
+ "child_process"
1402
+ ];
1403
+ for (const pattern of dangerousPatterns) {
1404
+ if (lowerCode.includes(pattern)) {
1405
+ return false;
1406
+ }
1407
+ }
1408
+ return true;
1409
+ }
1410
+
1411
+ // src/sandbox/sandbox.ts
1412
+ var ScriptError = class extends Error {
1413
+ constructor(message, details) {
1414
+ super(message);
1415
+ this.details = details;
1416
+ this.name = "ScriptError";
1417
+ }
1418
+ };
1419
+ var ScriptTimeoutError = class extends ScriptError {
1420
+ constructor(timeoutMs) {
1421
+ super("Script execution timed out", [`Exceeded ${timeoutMs}ms limit`]);
1422
+ this.name = "ScriptTimeoutError";
1423
+ }
1424
+ };
1425
+ var ScriptValidationError = class extends ScriptError {
1426
+ constructor(errors) {
1427
+ super("Script validation failed", errors);
1428
+ this.name = "ScriptValidationError";
1429
+ }
1430
+ };
1431
+ var ScriptSandbox = class {
1432
+ options;
1433
+ constructor(options = {}) {
1434
+ this.options = {
1435
+ timeoutMs: options.timeoutMs ?? 1e3,
1436
+ validateAST: options.validateAST ?? true,
1437
+ telemetry: options.telemetry
1438
+ };
1439
+ }
1440
+ /**
1441
+ * Execute a script in the sandbox.
1442
+ *
1443
+ * @param code - JavaScript code (expression or arrow function)
1444
+ * @param context - The frozen ScriptContext
1445
+ * @returns The script's return value (sanitized)
1446
+ * @throws ScriptValidationError if AST validation fails
1447
+ * @throws ScriptTimeoutError if execution exceeds timeout
1448
+ * @throws ScriptError for other execution errors
1449
+ */
1450
+ async execute(code, context) {
1451
+ const start = this.options.telemetry ? Date.now() : 0;
1452
+ if (this.options.validateAST) {
1453
+ const validation = validateScriptAST(code);
1454
+ if (!validation.valid) {
1455
+ this.options.telemetry?.reportUsage("plugins.ast_validation_failure", 1);
1456
+ throw new ScriptValidationError(validation.errors);
1457
+ }
1458
+ }
1459
+ const fn = this.createIsolatedFunction(code);
1460
+ try {
1461
+ const result = await this.executeWithTimeout(fn, context);
1462
+ const sanitized = this.sanitizeOutput(result);
1463
+ this.options.telemetry?.reportPerformance("plugins.execute", Date.now() - start);
1464
+ this.options.telemetry?.reportUsage("plugins.execute", 1);
1465
+ return sanitized;
1466
+ } catch (err) {
1467
+ const error = err instanceof Error ? err : new Error(String(err));
1468
+ this.options.telemetry?.reportCrash(error, { codeNamespace: "plugins.ScriptSandbox.execute" });
1469
+ this.options.telemetry?.reportUsage("plugins.execute_failure", 1);
1470
+ throw err;
1471
+ }
1472
+ }
1473
+ /**
1474
+ * Execute a script synchronously (for computed columns).
1475
+ * Use with caution - no timeout protection in sync mode.
1476
+ */
1477
+ executeSync(code, context) {
1478
+ const start = this.options.telemetry ? Date.now() : 0;
1479
+ if (this.options.validateAST) {
1480
+ const validation = validateScriptAST(code);
1481
+ if (!validation.valid) {
1482
+ this.options.telemetry?.reportUsage("plugins.ast_validation_failure", 1);
1483
+ throw new ScriptValidationError(validation.errors);
1484
+ }
1485
+ }
1486
+ const fn = this.createIsolatedFunction(code);
1487
+ try {
1488
+ const result = fn(context);
1489
+ const sanitized = this.sanitizeOutput(result);
1490
+ this.options.telemetry?.reportPerformance("plugins.execute_sync", Date.now() - start);
1491
+ this.options.telemetry?.reportUsage("plugins.execute_sync", 1);
1492
+ return sanitized;
1493
+ } catch (err) {
1494
+ const error = err instanceof Error ? err : new Error(String(err));
1495
+ this.options.telemetry?.reportCrash(error, {
1496
+ codeNamespace: "plugins.ScriptSandbox.executeSync"
1497
+ });
1498
+ this.options.telemetry?.reportUsage("plugins.execute_sync_failure", 1);
1499
+ throw new ScriptError("Script execution error", [
1500
+ err instanceof Error ? err.message : String(err)
1501
+ ]);
1502
+ }
1503
+ }
1504
+ /**
1505
+ * Create an isolated function from user code.
1506
+ *
1507
+ * The function receives context values as arguments and has dangerous
1508
+ * globals shadowed to undefined in its scope.
1509
+ */
1510
+ createIsolatedFunction(code) {
1511
+ const wrapped = `
1512
+ "use strict";
1513
+ // Shadow dangerous globals (except 'eval' which is reserved in strict mode)
1514
+ var window = void 0;
1515
+ var document = void 0;
1516
+ var globalThis = void 0;
1517
+ var self = void 0;
1518
+ var parent = void 0;
1519
+ var top = void 0;
1520
+ var frames = void 0;
1521
+ var fetch = void 0;
1522
+ var XMLHttpRequest = void 0;
1523
+ var WebSocket = void 0;
1524
+ var require = void 0;
1525
+ var process = void 0;
1526
+ var Buffer = void 0;
1527
+ var setTimeout = void 0;
1528
+ var setInterval = void 0;
1529
+ var setImmediate = void 0;
1530
+ var requestAnimationFrame = void 0;
1531
+ var localStorage = void 0;
1532
+ var sessionStorage = void 0;
1533
+ var indexedDB = void 0;
1534
+ var navigator = void 0;
1535
+ var location = void 0;
1536
+ var history = void 0;
1537
+ var Worker = void 0;
1538
+ var SharedWorker = void 0;
1539
+ var ServiceWorker = void 0;
1540
+ var Proxy = void 0;
1541
+ var Reflect = void 0;
1542
+ var WebAssembly = void 0;
1543
+ var crypto = void 0;
1544
+ // Return the user's function/expression
1545
+ return (${code.trim()});
1546
+ `;
1547
+ const factory = new Function("node", "nodes", "now", "format", "math", "text", "array", wrapped);
1548
+ return (ctx) => {
1549
+ const userFn = factory(
1550
+ ctx.node,
1551
+ ctx.nodes,
1552
+ ctx.now,
1553
+ ctx.format,
1554
+ ctx.math,
1555
+ ctx.text,
1556
+ ctx.array
1557
+ );
1558
+ if (typeof userFn === "function") {
1559
+ return userFn(ctx.node, ctx);
1560
+ }
1561
+ return userFn;
1562
+ };
1563
+ }
1564
+ /**
1565
+ * Execute a function with a timeout.
1566
+ */
1567
+ executeWithTimeout(fn, ctx) {
1568
+ return new Promise((resolve, reject) => {
1569
+ const timer = setTimeout(() => {
1570
+ reject(new ScriptTimeoutError(this.options.timeoutMs));
1571
+ }, this.options.timeoutMs);
1572
+ try {
1573
+ const result = fn(ctx);
1574
+ clearTimeout(timer);
1575
+ resolve(result);
1576
+ } catch (err) {
1577
+ clearTimeout(timer);
1578
+ reject(
1579
+ new ScriptError("Script execution error", [
1580
+ err instanceof Error ? err.message : String(err)
1581
+ ])
1582
+ );
1583
+ }
1584
+ });
1585
+ }
1586
+ /**
1587
+ * Sanitize script output to ensure only safe values are returned.
1588
+ *
1589
+ * Allowed types: null, undefined, string, number, boolean, plain objects, arrays
1590
+ * Stripped: functions, symbols, circular references, dunder properties
1591
+ */
1592
+ sanitizeOutput(result, seen = /* @__PURE__ */ new WeakSet()) {
1593
+ if (result === null || result === void 0) return result;
1594
+ if (typeof result === "string") return result;
1595
+ if (typeof result === "number") return Number.isFinite(result) ? result : null;
1596
+ if (typeof result === "boolean") return result;
1597
+ if (typeof result === "function") return void 0;
1598
+ if (typeof result === "symbol") return void 0;
1599
+ if (Array.isArray(result)) {
1600
+ if (seen.has(result)) return void 0;
1601
+ seen.add(result);
1602
+ return result.map((item) => this.sanitizeOutput(item, seen));
1603
+ }
1604
+ if (typeof result === "object") {
1605
+ if (seen.has(result)) return void 0;
1606
+ seen.add(result);
1607
+ const proto = Object.getPrototypeOf(result);
1608
+ if (proto !== null && proto !== Object.prototype) {
1609
+ if (result instanceof Date) {
1610
+ return result.getTime();
1611
+ }
1612
+ return void 0;
1613
+ }
1614
+ const clean = {};
1615
+ for (const [key, value] of Object.entries(result)) {
1616
+ if (key.startsWith("__")) continue;
1617
+ if (typeof key === "symbol") continue;
1618
+ const sanitized = this.sanitizeOutput(value, seen);
1619
+ if (sanitized !== void 0) {
1620
+ clean[key] = sanitized;
1621
+ }
1622
+ }
1623
+ return clean;
1624
+ }
1625
+ return void 0;
1626
+ }
1627
+ };
1628
+ async function executeScript(code, context) {
1629
+ const sandbox = new ScriptSandbox();
1630
+ return sandbox.execute(code, context);
1631
+ }
1632
+ function validateScript(code) {
1633
+ return validateScriptAST(code);
1634
+ }
1635
+
1636
+ // src/sandbox/runner.ts
1637
+ var ScriptRunner = class {
1638
+ sandbox;
1639
+ store;
1640
+ subscriptions = [];
1641
+ scriptSubscriptions = /* @__PURE__ */ new Map();
1642
+ started = false;
1643
+ telemetry;
1644
+ constructor(options) {
1645
+ this.store = options.store;
1646
+ this.telemetry = options.telemetry;
1647
+ this.sandbox = new ScriptSandbox({ ...options.sandboxOptions, telemetry: options.telemetry });
1648
+ }
1649
+ /**
1650
+ * Start the script runner.
1651
+ * Registers all enabled scripts and sets up triggers.
1652
+ */
1653
+ async start() {
1654
+ if (this.started) return;
1655
+ this.started = true;
1656
+ const scripts = this.store.list({ schemaIRI: "xnet://xnet.dev/Script" });
1657
+ for (const script of scripts) {
1658
+ if (script.enabled) {
1659
+ this.registerScript(script);
1660
+ }
1661
+ }
1662
+ const unsubScripts = this.store.subscribe((event) => {
1663
+ if (event.node?.schemaIRI === "xnet://xnet.dev/Script") {
1664
+ this.handleScriptChange(event);
1665
+ }
1666
+ });
1667
+ this.subscriptions.push(unsubScripts);
1668
+ }
1669
+ /**
1670
+ * Stop the script runner.
1671
+ * Removes all triggers and subscriptions.
1672
+ */
1673
+ stop() {
1674
+ this.started = false;
1675
+ for (const unsub of this.subscriptions) {
1676
+ unsub();
1677
+ }
1678
+ this.subscriptions = [];
1679
+ for (const unsub of this.scriptSubscriptions.values()) {
1680
+ unsub();
1681
+ }
1682
+ this.scriptSubscriptions.clear();
1683
+ }
1684
+ /**
1685
+ * Execute a script manually for testing/preview.
1686
+ * Does not update lastRun/lastError on the script node.
1687
+ */
1688
+ async executeManual(code, node) {
1689
+ const startTime = Date.now();
1690
+ try {
1691
+ const context = createScriptContext(node, (schemaIRI) => this.store.list({ schemaIRI }));
1692
+ const result = await this.sandbox.execute(code, context);
1693
+ return {
1694
+ success: true,
1695
+ result,
1696
+ durationMs: Date.now() - startTime
1697
+ };
1698
+ } catch (err) {
1699
+ const errorMsg = err instanceof ScriptError ? err.details.join("; ") : err instanceof Error ? err.message : String(err);
1700
+ return {
1701
+ success: false,
1702
+ error: errorMsg,
1703
+ durationMs: Date.now() - startTime
1704
+ };
1705
+ }
1706
+ }
1707
+ /**
1708
+ * Execute a script as a computed value (synchronous).
1709
+ * Used for computed columns in table views.
1710
+ */
1711
+ computeValue(code, node) {
1712
+ const context = createScriptContext(node, (schemaIRI) => this.store.list({ schemaIRI }));
1713
+ return this.sandbox.executeSync(code, context);
1714
+ }
1715
+ // ─── Private Methods ───────────────────────────────────────────────────────
1716
+ /**
1717
+ * Register a script and set up its trigger.
1718
+ */
1719
+ registerScript(script) {
1720
+ this.unregisterScript(script.id);
1721
+ const trigger = script.triggerType;
1722
+ switch (trigger) {
1723
+ case "onChange":
1724
+ this.registerOnChangeTrigger(script);
1725
+ break;
1726
+ case "manual":
1727
+ break;
1728
+ case "onView":
1729
+ break;
1730
+ case "scheduled":
1731
+ console.warn(`Scheduled triggers not yet implemented for script: ${script.name}`);
1732
+ break;
1733
+ }
1734
+ }
1735
+ /**
1736
+ * Unregister a script's trigger.
1737
+ */
1738
+ unregisterScript(scriptId) {
1739
+ const unsub = this.scriptSubscriptions.get(scriptId);
1740
+ if (unsub) {
1741
+ unsub();
1742
+ this.scriptSubscriptions.delete(scriptId);
1743
+ }
1744
+ }
1745
+ /**
1746
+ * Register an onChange trigger for a script.
1747
+ */
1748
+ registerOnChangeTrigger(script) {
1749
+ if (!script.inputSchema) {
1750
+ console.warn(`onChange script '${script.name}' has no inputSchema`);
1751
+ return;
1752
+ }
1753
+ const unsub = this.store.subscribe(async (event) => {
1754
+ if (event.type !== "update" && event.type !== "create") return;
1755
+ if (!event.node || event.node.schemaIRI !== script.inputSchema) return;
1756
+ if (script.triggerProperty && event.change.payload) {
1757
+ if (!(script.triggerProperty in event.change.payload)) {
1758
+ return;
1759
+ }
1760
+ }
1761
+ await this.executeScript(script, event.node);
1762
+ });
1763
+ this.scriptSubscriptions.set(script.id, unsub);
1764
+ }
1765
+ /**
1766
+ * Execute a script and handle results.
1767
+ */
1768
+ async executeScript(script, targetNode) {
1769
+ const startTime = Date.now();
1770
+ try {
1771
+ const context = createScriptContext(targetNode, (schemaIRI) => this.store.list({ schemaIRI }));
1772
+ const result = await this.sandbox.execute(script.code, context);
1773
+ if (result && typeof result === "object") {
1774
+ const outputType = script.outputType;
1775
+ if (outputType === "mutation") {
1776
+ await this.store.update(targetNode.id, result);
1777
+ } else if (outputType === "decoration") {
1778
+ await this.store.update(targetNode.id, { _decoration: result });
1779
+ }
1780
+ }
1781
+ this.telemetry?.reportPerformance("plugins.script_execution", Date.now() - startTime);
1782
+ await this.store.update(script.id, {
1783
+ lastRun: Date.now(),
1784
+ lastError: null
1785
+ });
1786
+ } catch (err) {
1787
+ const errorMsg = err instanceof ScriptError ? err.details.join("; ") : err instanceof Error ? err.message : String(err);
1788
+ this.telemetry?.reportUsage("plugins.crash_recovery", 1);
1789
+ await this.store.update(script.id, {
1790
+ lastRun: Date.now(),
1791
+ lastError: errorMsg
1792
+ });
1793
+ console.error(`Script '${script.name}' failed:`, errorMsg);
1794
+ }
1795
+ }
1796
+ /**
1797
+ * Handle changes to script nodes.
1798
+ */
1799
+ handleScriptChange(event) {
1800
+ const script = event.node;
1801
+ if (!script) return;
1802
+ switch (event.type) {
1803
+ case "create":
1804
+ if (script.enabled) {
1805
+ this.registerScript(script);
1806
+ }
1807
+ break;
1808
+ case "update":
1809
+ if (script.enabled) {
1810
+ this.registerScript(script);
1811
+ } else {
1812
+ this.unregisterScript(script.id);
1813
+ }
1814
+ break;
1815
+ case "delete":
1816
+ this.unregisterScript(script.id);
1817
+ break;
1818
+ }
1819
+ }
1820
+ };
1821
+
1822
+ // src/ai/prompt.ts
1823
+ var SYSTEM_CONTEXT = `You are generating a JavaScript script for xNet, a collaborative data platform.
1824
+ The script is a pure function that receives a node (data record) and a context object.
1825
+ It must return a value, a mutation object, or null.
1826
+
1827
+ CRITICAL CONSTRAINTS:
1828
+ - NO imports, require, or dynamic import
1829
+ - NO fetch, XMLHttpRequest, or network access
1830
+ - NO DOM access (window, document, globalThis)
1831
+ - NO async/await, setTimeout, setInterval
1832
+ - NO eval, Function constructor, or __proto__ access
1833
+ - Pure synchronous function only
1834
+ - Must use arrow function syntax: (node, ctx) => ...`;
1835
+ var API_DOCUMENTATION = `
1836
+ ## Available API
1837
+
1838
+ The function signature is: (node, ctx) => result
1839
+
1840
+ ### node (read-only)
1841
+ The current data record. Access properties directly: node.propertyName
1842
+
1843
+ ### ctx object (ScriptContext)
1844
+
1845
+ **Query:**
1846
+ - ctx.nodes(schemaIRI?) - Query nodes, optionally filtered by schema. Returns frozen array.
1847
+
1848
+ **Time:**
1849
+ - ctx.now() - Current timestamp in milliseconds
1850
+
1851
+ **Format Helpers (ctx.format):**
1852
+ - ctx.format.date(timestamp, options?) - Format timestamp as date string
1853
+ - ctx.format.number(value, options?) - Format number with locale
1854
+ - ctx.format.currency(value, currency?, locale?) - Format as currency (default: USD)
1855
+ - ctx.format.relative(timestamp) - Relative time ("2h ago", "3d ago")
1856
+ - ctx.format.bytes(bytes) - Format bytes ("1.5 KB", "2.3 MB")
1857
+
1858
+ **Math Helpers (ctx.math):**
1859
+ - ctx.math.sum(numbers[]) - Sum of array
1860
+ - ctx.math.avg(numbers[]) - Average of array
1861
+ - ctx.math.min(numbers[]) - Minimum value
1862
+ - ctx.math.max(numbers[]) - Maximum value
1863
+ - ctx.math.round(value, decimals?) - Round to decimal places
1864
+ - ctx.math.clamp(value, min, max) - Clamp between min and max
1865
+ - ctx.math.abs(value) - Absolute value
1866
+ - ctx.math.floor(value) - Floor
1867
+ - ctx.math.ceil(value) - Ceiling
1868
+
1869
+ **Text Helpers (ctx.text):**
1870
+ - ctx.text.slugify(str) - Convert to URL-safe slug
1871
+ - ctx.text.truncate(str, maxLength) - Truncate with ellipsis
1872
+ - ctx.text.capitalize(str) - Capitalize first letter
1873
+ - ctx.text.titleCase(str) - Title Case Each Word
1874
+ - ctx.text.contains(str, search) - Case-insensitive contains
1875
+ - ctx.text.template(template, vars) - Simple {var} replacement
1876
+ - ctx.text.trim(str) - Trim whitespace
1877
+ - ctx.text.lower(str) - Lowercase
1878
+ - ctx.text.upper(str) - Uppercase
1879
+
1880
+ **Array Helpers (ctx.array):**
1881
+ - ctx.array.first(items) - Get first element
1882
+ - ctx.array.last(items) - Get last element
1883
+ - ctx.array.sortBy(items, key, desc?) - Sort by property
1884
+ - ctx.array.groupBy(items, key) - Group by property
1885
+ - ctx.array.unique(items) - Remove duplicates
1886
+ - ctx.array.count(items) - Count items
1887
+ - ctx.array.compact(items) - Remove null/undefined
1888
+ `;
1889
+ function formatSchemaProperties(properties) {
1890
+ if (properties.length === 0) {
1891
+ return "(No properties defined)";
1892
+ }
1893
+ return properties.map((prop) => {
1894
+ let line = `- node.${prop.name}: ${prop.type}`;
1895
+ if (prop.required) line += " (required)";
1896
+ if (prop.description) line += ` - ${prop.description}`;
1897
+ return line;
1898
+ }).join("\n");
1899
+ }
1900
+ function getOutputTypeDescription(type) {
1901
+ switch (type) {
1902
+ case "value":
1903
+ return `Return a computed value (string, number, boolean, or object).
1904
+ Used as a virtual/computed column in table views.
1905
+ Example: (node) => node.quantity * node.unitPrice`;
1906
+ case "mutation":
1907
+ return `Return an object with property updates to merge into the node.
1908
+ The returned object's properties will be applied to the node.
1909
+ Example: (node) => node.amount > 1000 ? { priority: 'high' } : null`;
1910
+ case "decoration":
1911
+ return `Return an object with visual decorations (not persisted to node data).
1912
+ Use for tags, badges, or highlights based on conditions.
1913
+ Example: (node) => node.dueDate < ctx.now() ? { _decoration: 'OVERDUE' } : null`;
1914
+ case "void":
1915
+ return `Return null. Script runs for side effects only (logging, etc).
1916
+ Example: (node) => { console.log(node.id); return null }`;
1917
+ }
1918
+ }
1919
+ function getTriggerTypeDescription(type) {
1920
+ switch (type) {
1921
+ case "manual":
1922
+ return "Run manually via UI button";
1923
+ case "onChange":
1924
+ return "Run automatically when the node is created or updated";
1925
+ case "onView":
1926
+ return "Run when computing the value for display (lazy evaluation)";
1927
+ case "scheduled":
1928
+ return "Run on a schedule (cron)";
1929
+ }
1930
+ }
1931
+ function formatExamples(examples) {
1932
+ if (examples.length === 0) return "";
1933
+ const simplified = examples.slice(0, 3).map((node) => {
1934
+ const { id, schemaIRI, ...rest } = node;
1935
+ return rest;
1936
+ });
1937
+ return `
1938
+ ## Sample Data
1939
+ Here are example nodes to understand the data structure:
1940
+ \`\`\`json
1941
+ ${JSON.stringify(simplified, null, 2)}
1942
+ \`\`\``;
1943
+ }
1944
+ function buildScriptPrompt(request) {
1945
+ const {
1946
+ intent,
1947
+ schema,
1948
+ outputType,
1949
+ triggerType = "onChange",
1950
+ examples = [],
1951
+ constraints = []
1952
+ } = request;
1953
+ const sections = [];
1954
+ sections.push(SYSTEM_CONTEXT);
1955
+ sections.push(API_DOCUMENTATION);
1956
+ sections.push(`
1957
+ ## Schema: ${schema.name}
1958
+ Schema IRI: ${schema.schemaIRI}
1959
+
1960
+ Available properties:
1961
+ ${formatSchemaProperties(schema.properties)}`);
1962
+ sections.push(`
1963
+ ## Output Type: ${outputType}
1964
+ ${getOutputTypeDescription(outputType)}`);
1965
+ sections.push(`
1966
+ ## Trigger: ${triggerType}
1967
+ ${getTriggerTypeDescription(triggerType)}`);
1968
+ if (examples.length > 0) {
1969
+ sections.push(formatExamples(examples));
1970
+ }
1971
+ if (constraints.length > 0) {
1972
+ sections.push(`
1973
+ ## Additional Requirements
1974
+ ${constraints.map((c) => `- ${c}`).join("\n")}`);
1975
+ }
1976
+ sections.push(`
1977
+ ## User Request
1978
+ "${intent}"
1979
+
1980
+ Generate ONLY the arrow function expression. No markdown code fences, no explanation.
1981
+ The function should handle edge cases (null values, missing properties) gracefully.
1982
+
1983
+ Format: (node, ctx) => { ... } or (node, ctx) => expression`);
1984
+ return sections.join("\n");
1985
+ }
1986
+ function buildRetryPrompt(originalPrompt, errors) {
1987
+ return `${originalPrompt}
1988
+
1989
+ ---
1990
+ IMPORTANT: Your previous attempt had validation errors:
1991
+ ${errors.map((e) => `- ${e}`).join("\n")}
1992
+
1993
+ Please fix these issues and generate a corrected version.
1994
+ Remember: NO forbidden globals, NO async/await, NO imports.`;
1995
+ }
1996
+
1997
+ // src/ai/providers.ts
1998
+ var AIGenerationError = class extends Error {
1999
+ constructor(message, provider, cause) {
2000
+ super(message);
2001
+ this.provider = provider;
2002
+ this.cause = cause;
2003
+ this.name = "AIGenerationError";
2004
+ }
2005
+ };
2006
+ var AnthropicProvider = class {
2007
+ name = "Anthropic";
2008
+ apiKey;
2009
+ baseUrl;
2010
+ model;
2011
+ maxTokens;
2012
+ temperature;
2013
+ constructor(options) {
2014
+ if (!options.apiKey) {
2015
+ throw new Error("Anthropic API key is required");
2016
+ }
2017
+ this.apiKey = options.apiKey;
2018
+ this.baseUrl = options.baseUrl ?? "https://api.anthropic.com";
2019
+ this.model = options.model ?? "claude-sonnet-4-20250514";
2020
+ this.maxTokens = options.maxTokens ?? 2048;
2021
+ this.temperature = options.temperature ?? 0.3;
2022
+ }
2023
+ async generate(prompt) {
2024
+ try {
2025
+ const response = await fetch(`${this.baseUrl}/v1/messages`, {
2026
+ method: "POST",
2027
+ headers: {
2028
+ "content-type": "application/json",
2029
+ "x-api-key": this.apiKey,
2030
+ "anthropic-version": "2023-06-01"
2031
+ },
2032
+ body: JSON.stringify({
2033
+ model: this.model,
2034
+ max_tokens: this.maxTokens,
2035
+ temperature: this.temperature,
2036
+ messages: [{ role: "user", content: prompt }]
2037
+ })
2038
+ });
2039
+ if (!response.ok) {
2040
+ const error = await response.text();
2041
+ throw new AIGenerationError(`Anthropic API error: ${response.status} ${error}`, this.name);
2042
+ }
2043
+ const data = await response.json();
2044
+ const textContent = data.content.find((c) => c.type === "text");
2045
+ if (!textContent) {
2046
+ throw new AIGenerationError("No text content in response", this.name);
2047
+ }
2048
+ return textContent.text;
2049
+ } catch (err) {
2050
+ if (err instanceof AIGenerationError) throw err;
2051
+ throw new AIGenerationError(
2052
+ `Anthropic generation failed: ${err instanceof Error ? err.message : String(err)}`,
2053
+ this.name,
2054
+ err
2055
+ );
2056
+ }
2057
+ }
2058
+ };
2059
+ var OpenAIProvider = class {
2060
+ name = "OpenAI";
2061
+ apiKey;
2062
+ baseUrl;
2063
+ model;
2064
+ maxTokens;
2065
+ temperature;
2066
+ constructor(options) {
2067
+ if (!options.apiKey) {
2068
+ throw new Error("OpenAI API key is required");
2069
+ }
2070
+ this.apiKey = options.apiKey;
2071
+ this.baseUrl = options.baseUrl ?? "https://api.openai.com";
2072
+ this.model = options.model ?? "gpt-4o";
2073
+ this.maxTokens = options.maxTokens ?? 2048;
2074
+ this.temperature = options.temperature ?? 0.3;
2075
+ }
2076
+ async generate(prompt) {
2077
+ try {
2078
+ const response = await fetch(`${this.baseUrl}/v1/chat/completions`, {
2079
+ method: "POST",
2080
+ headers: {
2081
+ "content-type": "application/json",
2082
+ authorization: `Bearer ${this.apiKey}`
2083
+ },
2084
+ body: JSON.stringify({
2085
+ model: this.model,
2086
+ max_tokens: this.maxTokens,
2087
+ temperature: this.temperature,
2088
+ messages: [{ role: "user", content: prompt }]
2089
+ })
2090
+ });
2091
+ if (!response.ok) {
2092
+ const error = await response.text();
2093
+ throw new AIGenerationError(`OpenAI API error: ${response.status} ${error}`, this.name);
2094
+ }
2095
+ const data = await response.json();
2096
+ if (!data.choices?.[0]?.message?.content) {
2097
+ throw new AIGenerationError("No content in response", this.name);
2098
+ }
2099
+ return data.choices[0].message.content;
2100
+ } catch (err) {
2101
+ if (err instanceof AIGenerationError) throw err;
2102
+ throw new AIGenerationError(
2103
+ `OpenAI generation failed: ${err instanceof Error ? err.message : String(err)}`,
2104
+ this.name,
2105
+ err
2106
+ );
2107
+ }
2108
+ }
2109
+ };
2110
+ var OllamaProvider = class {
2111
+ name = "Ollama";
2112
+ baseUrl;
2113
+ model;
2114
+ temperature;
2115
+ constructor(options = {}) {
2116
+ this.baseUrl = options.baseUrl ?? "http://localhost:11434";
2117
+ this.model = options.model ?? "codellama";
2118
+ this.temperature = options.temperature ?? 0.3;
2119
+ }
2120
+ async generate(prompt) {
2121
+ try {
2122
+ const response = await fetch(`${this.baseUrl}/api/generate`, {
2123
+ method: "POST",
2124
+ headers: {
2125
+ "content-type": "application/json"
2126
+ },
2127
+ body: JSON.stringify({
2128
+ model: this.model,
2129
+ prompt,
2130
+ stream: false,
2131
+ options: {
2132
+ temperature: this.temperature
2133
+ }
2134
+ })
2135
+ });
2136
+ if (!response.ok) {
2137
+ const error = await response.text();
2138
+ throw new AIGenerationError(`Ollama API error: ${response.status} ${error}`, this.name);
2139
+ }
2140
+ const data = await response.json();
2141
+ if (!data.response) {
2142
+ throw new AIGenerationError("No response content", this.name);
2143
+ }
2144
+ return data.response;
2145
+ } catch (err) {
2146
+ if (err instanceof AIGenerationError) throw err;
2147
+ throw new AIGenerationError(
2148
+ `Ollama generation failed: ${err instanceof Error ? err.message : String(err)}`,
2149
+ this.name,
2150
+ err
2151
+ );
2152
+ }
2153
+ }
2154
+ };
2155
+ function createAIProvider(config) {
2156
+ switch (config.type) {
2157
+ case "anthropic":
2158
+ return new AnthropicProvider(config.options);
2159
+ case "openai":
2160
+ return new OpenAIProvider(config.options);
2161
+ case "ollama":
2162
+ return new OllamaProvider(config.options);
2163
+ case "custom":
2164
+ throw new Error("Custom provider requires manual instantiation");
2165
+ default:
2166
+ throw new Error(`Unknown AI provider type: ${config.type}`);
2167
+ }
2168
+ }
2169
+ async function isOllamaAvailable(baseUrl = "http://localhost:11434") {
2170
+ try {
2171
+ const response = await fetch(`${baseUrl}/api/tags`, {
2172
+ method: "GET",
2173
+ signal: AbortSignal.timeout(2e3)
2174
+ });
2175
+ return response.ok;
2176
+ } catch {
2177
+ return false;
2178
+ }
2179
+ }
2180
+ async function listOllamaModels(baseUrl = "http://localhost:11434") {
2181
+ try {
2182
+ const response = await fetch(`${baseUrl}/api/tags`);
2183
+ if (!response.ok) return [];
2184
+ const data = await response.json();
2185
+ return data.models?.map((m) => m.name) ?? [];
2186
+ } catch {
2187
+ return [];
2188
+ }
2189
+ }
2190
+
2191
+ // src/ai/generator.ts
2192
+ var ScriptGenerationError = class extends Error {
2193
+ constructor(message, validationErrors, attempts) {
2194
+ super(message);
2195
+ this.validationErrors = validationErrors;
2196
+ this.attempts = attempts;
2197
+ this.name = "ScriptGenerationError";
2198
+ }
2199
+ };
2200
+ var ScriptGenerator = class {
2201
+ ai;
2202
+ maxRetries;
2203
+ throwOnValidationFailure;
2204
+ constructor(ai, options = {}) {
2205
+ this.ai = ai;
2206
+ this.maxRetries = options.maxRetries ?? 2;
2207
+ this.throwOnValidationFailure = options.throwOnValidationFailure ?? false;
2208
+ }
2209
+ /**
2210
+ * Generate a script from a natural language request.
2211
+ *
2212
+ * @param request - The script generation request
2213
+ * @returns Generated script response
2214
+ * @throws ScriptGenerationError if throwOnValidationFailure is true and validation fails
2215
+ */
2216
+ async generate(request) {
2217
+ const prompt = buildScriptPrompt(request);
2218
+ let attempts = 0;
2219
+ let lastErrors = [];
2220
+ let currentPrompt = prompt;
2221
+ while (attempts <= this.maxRetries) {
2222
+ attempts++;
2223
+ try {
2224
+ const raw = await this.ai.generate(currentPrompt);
2225
+ const code = this.extractCode(raw);
2226
+ const validation = validateScriptAST(code);
2227
+ if (validation.valid) {
2228
+ return {
2229
+ code,
2230
+ explanation: this.generateExplanation(request, code),
2231
+ suggestedName: this.generateName(request.intent),
2232
+ suggestedTrigger: request.triggerType ?? this.inferTrigger(request),
2233
+ validated: true,
2234
+ attempts
2235
+ };
2236
+ }
2237
+ lastErrors = validation.errors;
2238
+ if (attempts <= this.maxRetries) {
2239
+ currentPrompt = buildRetryPrompt(prompt, validation.errors);
2240
+ }
2241
+ } catch (err) {
2242
+ if (attempts > this.maxRetries) {
2243
+ throw new ScriptGenerationError(
2244
+ `AI generation failed after ${attempts} attempts: ${err instanceof Error ? err.message : String(err)}`,
2245
+ lastErrors,
2246
+ attempts
2247
+ );
2248
+ }
2249
+ }
2250
+ }
2251
+ if (this.throwOnValidationFailure) {
2252
+ throw new ScriptGenerationError(
2253
+ `Script validation failed after ${attempts} attempts`,
2254
+ lastErrors,
2255
+ attempts
2256
+ );
2257
+ }
2258
+ const lastRaw = await this.ai.generate(currentPrompt);
2259
+ const lastCode = this.extractCode(lastRaw);
2260
+ return {
2261
+ code: lastCode,
2262
+ explanation: this.generateExplanation(request, lastCode),
2263
+ suggestedName: this.generateName(request.intent),
2264
+ suggestedTrigger: request.triggerType ?? this.inferTrigger(request),
2265
+ validated: false,
2266
+ attempts
2267
+ };
2268
+ }
2269
+ /**
2270
+ * Extract code from AI response (strips markdown fences, etc.)
2271
+ */
2272
+ extractCode(raw) {
2273
+ let text3 = raw.trim();
2274
+ const fencedMatch = text3.match(/```(?:javascript|js|typescript|ts)?\s*\n?([\s\S]*?)```/);
2275
+ if (fencedMatch) {
2276
+ text3 = fencedMatch[1].trim();
2277
+ }
2278
+ if (text3.startsWith("`") && text3.endsWith("`")) {
2279
+ text3 = text3.slice(1, -1).trim();
2280
+ }
2281
+ if (!text3.startsWith("(") && !text3.startsWith("function")) {
2282
+ const arrowMatch = text3.match(/(\([^)]*\)\s*=>\s*[\s\S]+)/);
2283
+ if (arrowMatch) {
2284
+ text3 = arrowMatch[1];
2285
+ }
2286
+ }
2287
+ return text3;
2288
+ }
2289
+ /**
2290
+ * Generate an explanation for the script
2291
+ */
2292
+ generateExplanation(request, _code) {
2293
+ const intent = request.intent.toLowerCase();
2294
+ if (intent.includes("calculat") || intent.includes("comput")) {
2295
+ return `Calculates ${intent.replace(/calculat\w*|comput\w*/i, "").trim()}`;
2296
+ }
2297
+ if (intent.includes("tag") || intent.includes("mark") || intent.includes("label")) {
2298
+ return `Automatically ${intent}`;
2299
+ }
2300
+ if (intent.includes("check") || intent.includes("validat") || intent.includes("verify")) {
2301
+ return `Validates that ${intent.replace(/check\w*|validat\w*|verify\w*/i, "").trim()}`;
2302
+ }
2303
+ return `Script that ${intent}`;
2304
+ }
2305
+ /**
2306
+ * Generate a name from the intent
2307
+ */
2308
+ generateName(intent) {
2309
+ const words = intent.replace(/[^a-zA-Z0-9\s]/g, "").split(/\s+/).filter((w) => w.length > 2).slice(0, 4);
2310
+ if (words.length === 0) {
2311
+ return "customScript";
2312
+ }
2313
+ return words.map((word, index) => {
2314
+ const lower = word.toLowerCase();
2315
+ if (index === 0) return lower;
2316
+ return lower.charAt(0).toUpperCase() + lower.slice(1);
2317
+ }).join("");
2318
+ }
2319
+ /**
2320
+ * Infer the best trigger type from the request
2321
+ */
2322
+ inferTrigger(request) {
2323
+ const intent = request.intent.toLowerCase();
2324
+ const outputType = request.outputType;
2325
+ if (outputType === "value") {
2326
+ return "onView";
2327
+ }
2328
+ if (outputType === "mutation") {
2329
+ if (intent.includes("when") || intent.includes("if") || intent.includes("auto") || intent.includes("automatic")) {
2330
+ return "onChange";
2331
+ }
2332
+ }
2333
+ return "manual";
2334
+ }
2335
+ };
2336
+ async function generateScript(provider, request) {
2337
+ const generator = new ScriptGenerator(provider);
2338
+ return generator.generate(request);
2339
+ }
2340
+
2341
+ // src/services/client.ts
2342
+ var SERVICE_IPC_CHANNELS = {
2343
+ START: "xnet:service:start",
2344
+ STOP: "xnet:service:stop",
2345
+ RESTART: "xnet:service:restart",
2346
+ STATUS: "xnet:service:status",
2347
+ LIST_ALL: "xnet:service:list-all",
2348
+ CALL: "xnet:service:call",
2349
+ STATUS_UPDATE: "xnet:service:status-update",
2350
+ OUTPUT: "xnet:service:output"
2351
+ };
2352
+ function getIPC() {
2353
+ if (typeof window === "undefined") return null;
2354
+ return window.xnetServices ?? null;
2355
+ }
2356
+ function createServiceClient() {
2357
+ const ipc = getIPC();
2358
+ if (!ipc) {
2359
+ return createMockServiceClient();
2360
+ }
2361
+ return {
2362
+ start: (definition) => ipc.invoke(SERVICE_IPC_CHANNELS.START, definition),
2363
+ stop: (serviceId) => ipc.invoke(SERVICE_IPC_CHANNELS.STOP, serviceId),
2364
+ restart: (serviceId) => ipc.invoke(SERVICE_IPC_CHANNELS.RESTART, serviceId),
2365
+ status: (serviceId) => ipc.invoke(SERVICE_IPC_CHANNELS.STATUS, serviceId),
2366
+ listAll: () => ipc.invoke(SERVICE_IPC_CHANNELS.LIST_ALL),
2367
+ call: (serviceId, method, path, body) => ipc.invoke(SERVICE_IPC_CHANNELS.CALL, serviceId, method, path, body),
2368
+ onStatusChange: (serviceId, callback) => {
2369
+ const handler = (event) => {
2370
+ if (event.serviceId === serviceId) {
2371
+ callback(event.status);
2372
+ }
2373
+ };
2374
+ ipc.on(SERVICE_IPC_CHANNELS.STATUS_UPDATE, handler);
2375
+ return () => ipc.off(SERVICE_IPC_CHANNELS.STATUS_UPDATE, handler);
2376
+ },
2377
+ onOutput: (serviceId, callback) => {
2378
+ const handler = (event) => {
2379
+ if (event.serviceId === serviceId) {
2380
+ callback(event);
2381
+ }
2382
+ };
2383
+ ipc.on(SERVICE_IPC_CHANNELS.OUTPUT, handler);
2384
+ return () => ipc.off(SERVICE_IPC_CHANNELS.OUTPUT, handler);
2385
+ }
2386
+ };
2387
+ }
2388
+ function createMockServiceClient() {
2389
+ const notAvailable = () => {
2390
+ throw new Error(
2391
+ "Service client is only available in Electron. Make sure you are running in the Electron renderer process."
2392
+ );
2393
+ };
2394
+ return {
2395
+ start: notAvailable,
2396
+ stop: notAvailable,
2397
+ restart: notAvailable,
2398
+ status: notAvailable,
2399
+ listAll: notAvailable,
2400
+ call: notAvailable,
2401
+ onStatusChange: () => () => {
2402
+ },
2403
+ onOutput: () => () => {
2404
+ }
2405
+ };
2406
+ }
2407
+ function isServiceClientAvailable() {
2408
+ return getIPC() !== null;
2409
+ }
2410
+
2411
+ // src/services/webhook-emitter.ts
2412
+ var WebhookEmitter = class {
2413
+ constructor(store) {
2414
+ this.store = store;
2415
+ }
2416
+ webhooks = /* @__PURE__ */ new Map();
2417
+ unsubscribe;
2418
+ deliveryHistory = [];
2419
+ maxHistorySize = 100;
2420
+ /**
2421
+ * Start listening for store changes.
2422
+ * Must be called before webhooks will be sent.
2423
+ */
2424
+ start() {
2425
+ if (this.unsubscribe) return;
2426
+ this.unsubscribe = this.store.subscribe((event) => {
2427
+ this.handleEvent(event);
2428
+ });
2429
+ }
2430
+ /**
2431
+ * Stop listening for store changes.
2432
+ */
2433
+ stop() {
2434
+ if (this.unsubscribe) {
2435
+ this.unsubscribe();
2436
+ this.unsubscribe = void 0;
2437
+ }
2438
+ }
2439
+ /**
2440
+ * Register a webhook configuration.
2441
+ * Returns a disposable to unregister.
2442
+ */
2443
+ register(config) {
2444
+ this.webhooks.set(config.id, { ...config, enabled: config.enabled ?? true });
2445
+ return {
2446
+ dispose: () => {
2447
+ this.webhooks.delete(config.id);
2448
+ }
2449
+ };
2450
+ }
2451
+ /**
2452
+ * Unregister a webhook by ID.
2453
+ */
2454
+ unregister(id) {
2455
+ return this.webhooks.delete(id);
2456
+ }
2457
+ /**
2458
+ * Get all registered webhooks.
2459
+ */
2460
+ getWebhooks() {
2461
+ return Array.from(this.webhooks.values());
2462
+ }
2463
+ /**
2464
+ * Get a webhook by ID.
2465
+ */
2466
+ getWebhook(id) {
2467
+ return this.webhooks.get(id);
2468
+ }
2469
+ /**
2470
+ * Update a webhook configuration.
2471
+ */
2472
+ updateWebhook(id, updates) {
2473
+ const existing = this.webhooks.get(id);
2474
+ if (!existing) return false;
2475
+ this.webhooks.set(id, { ...existing, ...updates, id });
2476
+ return true;
2477
+ }
2478
+ /**
2479
+ * Get recent delivery history.
2480
+ */
2481
+ getDeliveryHistory() {
2482
+ return [...this.deliveryHistory];
2483
+ }
2484
+ /**
2485
+ * Clear delivery history.
2486
+ */
2487
+ clearDeliveryHistory() {
2488
+ this.deliveryHistory = [];
2489
+ }
2490
+ // ─── Event Handling ──────────────────────────────────────────────────────────
2491
+ handleEvent(event) {
2492
+ const eventType = this.getEventType(event);
2493
+ if (!eventType) return;
2494
+ const payload = {
2495
+ type: eventType,
2496
+ timestamp: Date.now(),
2497
+ node: event.node,
2498
+ schema: event.node?.schemaId
2499
+ };
2500
+ for (const webhook of this.webhooks.values()) {
2501
+ if (!webhook.enabled) continue;
2502
+ if (!webhook.events.includes(eventType)) continue;
2503
+ if (webhook.schema && event.node?.schemaId !== webhook.schema) continue;
2504
+ this.send(webhook, payload).catch((err) => {
2505
+ console.error(`[WebhookEmitter] Failed to send to ${webhook.url}:`, err);
2506
+ });
2507
+ }
2508
+ }
2509
+ getEventType(event) {
2510
+ const changeType = event.change.type;
2511
+ if (changeType === "node-change") {
2512
+ if (event.node?.deleted) return "deleted";
2513
+ return "updated";
2514
+ }
2515
+ return null;
2516
+ }
2517
+ // ─── Delivery ────────────────────────────────────────────────────────────────
2518
+ async send(webhook, payload) {
2519
+ const maxRetries = webhook.retries ?? 3;
2520
+ let lastError = null;
2521
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
2522
+ try {
2523
+ const result = await this.deliverWebhook(webhook, payload);
2524
+ this.recordDelivery({
2525
+ webhookId: webhook.id,
2526
+ success: true,
2527
+ statusCode: result.status,
2528
+ attempts: attempt,
2529
+ deliveredAt: Date.now()
2530
+ });
2531
+ return;
2532
+ } catch (err) {
2533
+ lastError = err instanceof Error ? err : new Error(String(err));
2534
+ if (attempt < maxRetries) {
2535
+ await this.delay(1e3 * Math.pow(2, attempt - 1));
2536
+ }
2537
+ }
2538
+ }
2539
+ this.recordDelivery({
2540
+ webhookId: webhook.id,
2541
+ success: false,
2542
+ error: lastError?.message ?? "Unknown error",
2543
+ attempts: maxRetries
2544
+ });
2545
+ }
2546
+ async deliverWebhook(webhook, payload) {
2547
+ const body = JSON.stringify(payload);
2548
+ const headers = {
2549
+ "content-type": "application/json",
2550
+ "user-agent": "xNet-Webhook/1.0"
2551
+ };
2552
+ if (webhook.secret) {
2553
+ const signature = await this.sign(body, webhook.secret);
2554
+ headers["x-xnet-signature"] = signature;
2555
+ headers["x-xnet-signature-256"] = `sha256=${signature}`;
2556
+ }
2557
+ const response = await fetch(webhook.url, {
2558
+ method: "POST",
2559
+ headers,
2560
+ body,
2561
+ signal: AbortSignal.timeout(1e4)
2562
+ // 10 second timeout
2563
+ });
2564
+ if (!response.ok) {
2565
+ const text3 = await response.text().catch(() => "");
2566
+ throw new Error(`HTTP ${response.status}: ${text3.slice(0, 200)}`);
2567
+ }
2568
+ return { status: response.status };
2569
+ }
2570
+ async sign(body, secret) {
2571
+ const encoder = new TextEncoder();
2572
+ const key = await crypto.subtle.importKey(
2573
+ "raw",
2574
+ encoder.encode(secret),
2575
+ { name: "HMAC", hash: "SHA-256" },
2576
+ false,
2577
+ ["sign"]
2578
+ );
2579
+ const signature = await crypto.subtle.sign("HMAC", key, encoder.encode(body));
2580
+ return Array.from(new Uint8Array(signature)).map((b) => b.toString(16).padStart(2, "0")).join("");
2581
+ }
2582
+ recordDelivery(result) {
2583
+ this.deliveryHistory.push(result);
2584
+ if (this.deliveryHistory.length > this.maxHistorySize) {
2585
+ this.deliveryHistory.shift();
2586
+ }
2587
+ }
2588
+ delay(ms) {
2589
+ return new Promise((resolve) => setTimeout(resolve, ms));
2590
+ }
2591
+ };
2592
+ function createWebhookEmitter(store) {
2593
+ return new WebhookEmitter(store);
2594
+ }
2595
+ export {
2596
+ AIGenerationError,
2597
+ AnthropicProvider,
2598
+ ContributionRegistry,
2599
+ MiddlewareChain,
2600
+ OllamaProvider,
2601
+ OpenAIProvider,
2602
+ PluginError,
2603
+ PluginRegistry,
2604
+ PluginSchema,
2605
+ PluginValidationError,
2606
+ SERVICE_IPC_CHANNELS,
2607
+ ScriptError,
2608
+ ScriptGenerationError,
2609
+ ScriptGenerator,
2610
+ ScriptRunner,
2611
+ ScriptSandbox,
2612
+ ScriptSchema,
2613
+ ScriptTimeoutError,
2614
+ ScriptValidationError,
2615
+ ShortcutManager,
2616
+ TypedRegistry,
2617
+ WebhookEmitter,
2618
+ buildRetryPrompt,
2619
+ buildScriptPrompt,
2620
+ createAIProvider,
2621
+ createExtensionContext,
2622
+ createExtensionStorage,
2623
+ createScriptContext,
2624
+ createServiceClient,
2625
+ createWebhookEmitter,
2626
+ defineExtension,
2627
+ executeScript,
2628
+ generateScript,
2629
+ getPlatformCapabilities,
2630
+ getShortcutManager,
2631
+ installShortcutHandler,
2632
+ isOllamaAvailable,
2633
+ isScriptNode,
2634
+ isServiceClientAvailable,
2635
+ listOllamaModels,
2636
+ quickSafetyCheck,
2637
+ validateManifest,
2638
+ validateScript,
2639
+ validateScriptAST
2640
+ };