holosphere 2.0.0-alpha11 → 2.0.0-alpha13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/dist/{2019-D2OG2idw.js → 2019-CLMqIAfQ.js} +1722 -1668
  2. package/dist/{2019-D2OG2idw.js.map → 2019-CLMqIAfQ.js.map} +1 -1
  3. package/dist/2019-Cp3uYhyY.cjs +8 -0
  4. package/dist/{2019-EION3wKo.cjs.map → 2019-Cp3uYhyY.cjs.map} +1 -1
  5. package/dist/browser-D6cNVl0v.cjs +2 -0
  6. package/dist/{browser-Cq59Ij19.cjs.map → browser-D6cNVl0v.cjs.map} +1 -1
  7. package/dist/{browser-BSniCNqO.js → browser-nUQt1cnB.js} +2 -2
  8. package/dist/{browser-BSniCNqO.js.map → browser-nUQt1cnB.js.map} +1 -1
  9. package/dist/cjs/holosphere.cjs +1 -1
  10. package/dist/esm/holosphere.js +67 -50
  11. package/dist/{index-D-jZhliX.js → index-BN_uoxQK.js} +20324 -735
  12. package/dist/index-BN_uoxQK.js.map +1 -0
  13. package/dist/{index-Bl6rM1NW.js → index-CoAjtqsD.js} +2 -2
  14. package/dist/{index-Bl6rM1NW.js.map → index-CoAjtqsD.js.map} +1 -1
  15. package/dist/{index-Bwg3OzRM.cjs → index-Cp3tI53z.cjs} +3 -3
  16. package/dist/{index-Bwg3OzRM.cjs.map → index-Cp3tI53z.cjs.map} +1 -1
  17. package/dist/index-DJjGSwXG.cjs +13 -0
  18. package/dist/index-DJjGSwXG.cjs.map +1 -0
  19. package/dist/index-V8EHMYEY.cjs +29 -0
  20. package/dist/index-V8EHMYEY.cjs.map +1 -0
  21. package/dist/index-Z5TstN1e.js +11663 -0
  22. package/dist/index-Z5TstN1e.js.map +1 -0
  23. package/dist/indexeddb-storage-CZK5A7XH.cjs +2 -0
  24. package/dist/indexeddb-storage-CZK5A7XH.cjs.map +1 -0
  25. package/dist/{indexeddb-storage-5eiUNsHC.js → indexeddb-storage-bpA01pAU.js} +39 -2
  26. package/dist/indexeddb-storage-bpA01pAU.js.map +1 -0
  27. package/dist/{memory-storage-DMt36uZO.cjs → memory-storage-B1k8Jszd.cjs} +2 -2
  28. package/dist/{memory-storage-DMt36uZO.cjs.map → memory-storage-B1k8Jszd.cjs.map} +1 -1
  29. package/dist/{memory-storage-CI-gfmuG.js → memory-storage-BqhmytP_.js} +2 -2
  30. package/dist/{memory-storage-CI-gfmuG.js.map → memory-storage-BqhmytP_.js.map} +1 -1
  31. package/docs/FEDERATION.md +474 -0
  32. package/package.json +3 -1
  33. package/src/crypto/nostr-utils.js +7 -0
  34. package/src/crypto/secp256k1.js +104 -38
  35. package/src/federation/capabilities.js +162 -0
  36. package/src/federation/card-storage.js +376 -0
  37. package/src/federation/handshake.js +561 -9
  38. package/src/federation/hologram.js +194 -57
  39. package/src/federation/holon-registry.js +187 -0
  40. package/src/federation/index.js +68 -0
  41. package/src/federation/registry.js +164 -6
  42. package/src/federation/request-card.js +373 -0
  43. package/src/hierarchical/upcast.js +19 -3
  44. package/src/index.js +209 -75
  45. package/src/lib/federation-methods.js +527 -5
  46. package/src/storage/indexeddb-storage.js +41 -0
  47. package/src/storage/nostr-async.js +14 -5
  48. package/src/storage/nostr-client.js +471 -155
  49. package/src/storage/nostr-wrapper.js +6 -3
  50. package/dist/2019-EION3wKo.cjs +0 -8
  51. package/dist/_commonjsHelpers-C37NGDzP.cjs +0 -2
  52. package/dist/_commonjsHelpers-C37NGDzP.cjs.map +0 -1
  53. package/dist/_commonjsHelpers-CUmg6egw.js +0 -7
  54. package/dist/_commonjsHelpers-CUmg6egw.js.map +0 -1
  55. package/dist/browser-Cq59Ij19.cjs +0 -2
  56. package/dist/index-D-jZhliX.js.map +0 -1
  57. package/dist/index-Dc6Z8Aob.cjs +0 -18
  58. package/dist/index-Dc6Z8Aob.cjs.map +0 -1
  59. package/dist/indexeddb-storage-5eiUNsHC.js.map +0 -1
  60. package/dist/indexeddb-storage-FNFUVvTJ.cjs +0 -2
  61. package/dist/indexeddb-storage-FNFUVvTJ.cjs.map +0 -1
  62. package/dist/secp256k1-CEwJNcfV.js +0 -1890
  63. package/dist/secp256k1-CEwJNcfV.js.map +0 -1
  64. package/dist/secp256k1-CiEONUnj.cjs +0 -12
  65. package/dist/secp256k1-CiEONUnj.cjs.map +0 -1
package/src/index.js CHANGED
@@ -21,6 +21,11 @@ import * as schema from './schema/validator.js';
21
21
  import { ValidationError } from './schema/validator.js';
22
22
  import * as federation from './federation/hologram.js';
23
23
  import * as handshake from './federation/handshake.js';
24
+ import * as holonRegistry from './federation/holon-registry.js';
25
+ import * as registry from './federation/registry.js';
26
+ import * as capabilities from './federation/capabilities.js';
27
+ import * as requestCard from './federation/request-card.js';
28
+ import * as cardStorage from './federation/card-storage.js';
24
29
  import * as crypto from './crypto/secp256k1.js';
25
30
  import * as nostrUtils from './crypto/nostr-utils.js';
26
31
  import * as social from './content/social-protocols.js';
@@ -254,6 +259,51 @@ class HoloSphereBase extends HoloSphereCore {
254
259
  return spatial.isValidH3(holonId);
255
260
  }
256
261
 
262
+ // === Holon Registry Operations ===
263
+
264
+ /**
265
+ * Register a holon with a public key.
266
+ * Maps a holonId (H3, chatId, concept) to a keypair owner.
267
+ *
268
+ * @param {string} holonId - Holon identifier to register
269
+ * @param {string} publicKey - 64-char hex public key (owner)
270
+ * @param {Object} [options={}] - Registration options
271
+ * @param {string} [options.alias] - Human-readable name
272
+ * @returns {Promise<boolean>} Success indicator
273
+ */
274
+ async registerHolon(holonId, publicKey, options = {}) {
275
+ return holonRegistry.registerHolon(this.client, this.config.appName, holonId, publicKey, options);
276
+ }
277
+
278
+ /**
279
+ * Look up a holon's registration.
280
+ *
281
+ * @param {string} holonId - Holon identifier
282
+ * @returns {Promise<Object|null>} Registry entry { holonId, publicKey, alias } or null
283
+ */
284
+ async lookupHolon(holonId) {
285
+ return holonRegistry.lookupHolon(this.client, this.config.appName, holonId);
286
+ }
287
+
288
+ /**
289
+ * Unregister a holon.
290
+ *
291
+ * @param {string} holonId - Holon identifier
292
+ * @returns {Promise<boolean>} Success indicator
293
+ */
294
+ async unregisterHolon(holonId) {
295
+ return holonRegistry.unregisterHolon(this.client, this.config.appName, holonId);
296
+ }
297
+
298
+ /**
299
+ * List all registered holons.
300
+ *
301
+ * @returns {Promise<Object[]>} Array of registry entries
302
+ */
303
+ async getRegisteredHolons() {
304
+ return holonRegistry.listRegisteredHolons(this.client, this.config.appName);
305
+ }
306
+
257
307
  // === Data Operations ===
258
308
 
259
309
  /**
@@ -271,6 +321,7 @@ class HoloSphereBase extends HoloSphereCore {
271
321
  * @param {boolean} [options.autoPropagate=true] - Whether to propagate to federated holons
272
322
  * @param {Object} [options.propagationOptions] - Options for propagation
273
323
  * @param {boolean} [options.blocking=false] - If true, wait for relay confirmation before returning
324
+ * @param {string} [options.signingKey] - Private key to sign with (hex format). If not provided, uses holosphere's default key.
274
325
  * @returns {Promise<boolean>} True if write succeeded (or queued for optimistic writes)
275
326
  * @throws {ValidationError} If holonId, lensName, or data is invalid
276
327
  * @throws {AuthorizationError} If capability token is invalid
@@ -377,7 +428,8 @@ class HoloSphereBase extends HoloSphereCore {
377
428
  }
378
429
 
379
430
  // Regular write to relay
380
- await storage.write(this.client, path, data);
431
+ const writeOptions = options.signingKey ? { signingKey: options.signingKey } : {};
432
+ await storage.write(this.client, path, data, writeOptions);
381
433
 
382
434
  const endTime = Date.now();
383
435
  const duration = endTime - startTime;
@@ -486,6 +538,31 @@ class HoloSphereBase extends HoloSphereCore {
486
538
  return true;
487
539
  }
488
540
 
541
+ /**
542
+ * Resolve holonId to public key.
543
+ * If holonId is already a 64-char hex pubkey, returns it directly.
544
+ * Otherwise, looks up the holon registry.
545
+ *
546
+ * @private
547
+ * @param {string} holonId - Holon identifier
548
+ * @returns {Promise<string|null>} Public key or null if not found
549
+ */
550
+ async _resolveHolonToPubkey(holonId) {
551
+ return holonRegistry.resolveHolonToPubkey(this.client, this.config.appName, holonId);
552
+ }
553
+
554
+ /**
555
+ * Check if we have a stored capability for a given author and scope.
556
+ *
557
+ * @private
558
+ * @param {string} authorPubKey - Author's public key
559
+ * @param {Object} scope - Scope to check (holonId, lensName, dataId)
560
+ * @returns {Promise<Object|null>} Capability entry or null
561
+ */
562
+ async _getCapabilityForAuthor(authorPubKey, scope) {
563
+ return registry.getCapabilityForAuthor(this.client, this.config.appName, authorPubKey, scope);
564
+ }
565
+
489
566
  /**
490
567
  * Recursively resolves holograms (references) to their source data.
491
568
  * Handles circular reference detection and local overrides.
@@ -613,12 +690,98 @@ class HoloSphereBase extends HoloSphereCore {
613
690
  throw new ValidationError('ValidationError: lensName must be a non-empty string');
614
691
  }
615
692
 
693
+ // OPTIMIZATION: Check local caches first before any network/registry operations
694
+ // This ensures data we just wrote can be read back immediately (important for capability-based writes)
695
+ if (dataId) {
696
+ const earlyPath = storage.buildPath(this.config.appName, holonId, lensName, dataId);
697
+
698
+ // Check delete cache first - if deleted, return null immediately
699
+ if (this._deleteCache.has(earlyPath)) {
700
+ this._log('DEBUG', '🗑️ EARLY CACHE: Deleted item', { path: earlyPath });
701
+ return null;
702
+ }
703
+
704
+ // Check write cache - if this instance wrote data, return it immediately
705
+ const cached = this._writeCache.get(earlyPath);
706
+ if (cached) {
707
+ const cacheAge = Date.now() - cached.timestamp;
708
+ this._log('DEBUG', '⚡ EARLY CACHE HIT: Write cache', {
709
+ path: earlyPath,
710
+ cacheAge: `${cacheAge}ms`
711
+ });
712
+ // Still resolve holograms if needed
713
+ const { resolveHolograms = true } = options;
714
+ if (resolveHolograms && cached.data) {
715
+ return this._resolveHolograms(cached.data);
716
+ }
717
+ return cached.data;
718
+ }
719
+ }
720
+
721
+ // Resolve holonId to public key (if registered)
722
+ const targetPubkey = await this._resolveHolonToPubkey(holonId);
723
+
724
+ // Determine if reading another author's data
725
+ // If holonId can't be resolved, treat as own data (backwards compatible for H3 holons)
726
+ const isOtherAuthor = targetPubkey && targetPubkey !== this.client.publicKey;
727
+ let readOptions = {};
728
+
729
+ // Explicit capability token takes precedence
616
730
  const capToken = options.capabilityToken || options.capability;
617
731
  if (capToken) {
618
732
  const authorized = await this.verifyCapability(capToken, 'read', { holonId, lensName, dataId });
619
733
  if (!authorized) {
620
734
  throw new AuthorizationError('AuthorizationError: Invalid capability token for read operation', 'read');
621
735
  }
736
+ // Use target author for reading
737
+ if (isOtherAuthor) {
738
+ readOptions.authors = [targetPubkey];
739
+ }
740
+ } else if (isOtherAuthor) {
741
+ // Auto-check capability for other author's data
742
+ this._log('DEBUG', '🔍 Looking up capability for federated author', {
743
+ holonId,
744
+ lensName,
745
+ dataId: dataId || '*',
746
+ targetPubkey: targetPubkey?.slice(0, 12) + '...',
747
+ myPubkey: this.client?.publicKey?.slice(0, 12) + '...'
748
+ });
749
+ const capability = await this._getCapabilityForAuthor(targetPubkey, { holonId, lensName, dataId });
750
+ if (!capability) {
751
+ this._log('WARN', '❌ No capability found for federated author - returning empty', {
752
+ holonId,
753
+ lensName,
754
+ targetPubkey: targetPubkey?.slice(0, 12) + '...'
755
+ });
756
+ return dataId ? null : [];
757
+ }
758
+ this._log('DEBUG', '✅ Capability found for federated author', {
759
+ holonId,
760
+ lensName,
761
+ targetPubkey: targetPubkey?.slice(0, 12) + '...'
762
+ });
763
+ readOptions.authors = [targetPubkey];
764
+ }
765
+
766
+ // Include federated authors to see holograms written by federation partners
767
+ // This enables visibility of holograms created during federation
768
+ if (!isOtherAuthor) {
769
+ try {
770
+ const federatedAuthors = await registry.getFederatedAuthorsForScope(
771
+ this.client,
772
+ this.config.appName,
773
+ { holonId, lensName, dataId },
774
+ 'read'
775
+ );
776
+ if (federatedAuthors.length > 0) {
777
+ const partnerPubkeys = federatedAuthors.map(f => f.pubKey);
778
+ // Include self + all federated partners
779
+ readOptions.authors = [this.client.publicKey, ...partnerPubkeys];
780
+ this._log('DEBUG', 'Including federated authors', { count: partnerPubkeys.length });
781
+ }
782
+ } catch (err) {
783
+ this._log('WARN', 'Failed to get federated authors', { error: err.message });
784
+ }
622
785
  }
623
786
 
624
787
  const startTime = Date.now();
@@ -637,6 +800,8 @@ class HoloSphereBase extends HoloSphereCore {
637
800
  result = null;
638
801
  } else {
639
802
  // Check write cache for optimistic reads
803
+ // Always check write cache first - if this instance wrote data, it should be able to read it back
804
+ // regardless of holon ownership (important for capability-based writes)
640
805
  const cached = this._writeCache.get(path);
641
806
  if (cached) {
642
807
  const cacheAge = Date.now() - cached.timestamp;
@@ -648,8 +813,8 @@ class HoloSphereBase extends HoloSphereCore {
648
813
  });
649
814
  result = cached.data;
650
815
  } else {
651
- this._log('DEBUG', '📖 CACHE MISS: Reading from storage', { path });
652
- result = await storage.read(this.client, path);
816
+ this._log('DEBUG', '📖 CACHE MISS: Reading from storage', { path, authors: readOptions.authors });
817
+ result = await storage.read(this.client, path, readOptions);
653
818
  this._log('DEBUG', '💾 STORAGE READ', {
654
819
  path,
655
820
  source: 'storage',
@@ -660,11 +825,11 @@ class HoloSphereBase extends HoloSphereCore {
660
825
  }
661
826
  } else {
662
827
  const path = storage.buildPath(this.config.appName, holonId, lensName);
663
- this._log('DEBUG', 'readAll', { holonId, lensName, path });
828
+ this._log('DEBUG', 'readAll', { holonId, lensName, path, authors: readOptions.authors });
664
829
 
665
830
  // For readAll, merge cached writes with storage results and filter deleted
666
- const storageResult = await storage.readAll(this.client, path);
667
- result = this._mergeWithWriteCache(storageResult, path);
831
+ const storageResult = await storage.readAll(this.client, path, readOptions);
832
+ result = isOtherAuthor ? storageResult : this._mergeWithWriteCache(storageResult, path);
668
833
  this._log('DEBUG', 'readAll result', { count: Array.isArray(result) ? result.length : 0 });
669
834
  }
670
835
 
@@ -1179,56 +1344,11 @@ class HoloSphereBase extends HoloSphereCore {
1179
1344
  }
1180
1345
 
1181
1346
  // === Federation Operations ===
1182
-
1183
- /**
1184
- * Sets up federation between two holons for a specific lens.
1185
- * Federation enables data sharing and synchronization between holons.
1186
- *
1187
- * @param {string} sourceHolon - Source holon H3 cell ID
1188
- * @param {string} targetHolon - Target holon H3 cell ID
1189
- * @param {string} lensName - Name of the lens to federate
1190
- * @param {Object} [options={}] - Federation options
1191
- * @param {string} [options.direction='outbound'] - Direction: 'inbound', 'outbound', or 'bidirectional'
1192
- * @param {string} [options.mode='reference'] - Mode: 'reference' (hologram) or 'copy'
1193
- * @param {Function} [options.filter] - Filter function to select which data to federate
1194
- * @returns {Promise<boolean>} True if federation was set up successfully
1195
- * @throws {Error} If trying to federate a holon with itself or invalid direction
1196
- */
1197
- async federate(sourceHolon, targetHolon, lensName, options = {}) {
1198
- const { direction = 'outbound', mode = 'reference', filter = null } = options;
1199
-
1200
- // Validation
1201
- if (sourceHolon === targetHolon) {
1202
- throw new Error('Cannot federate a holon with itself');
1203
- }
1204
- if (!['inbound', 'outbound', 'bidirectional'].includes(direction)) {
1205
- throw new Error(`Invalid direction: ${direction}. Must be 'inbound', 'outbound', or 'bidirectional'`);
1206
- }
1207
-
1208
- // Store federation config
1209
- await federation.setupFederation(
1210
- this.client,
1211
- this.config.appName,
1212
- sourceHolon,
1213
- targetHolon,
1214
- lensName,
1215
- options
1216
- );
1217
-
1218
- // Actually propagate existing data based on direction
1219
- if (direction === 'outbound' || direction === 'bidirectional') {
1220
- await this._propagateExistingData(sourceHolon, targetHolon, lensName, { mode, filter });
1221
- }
1222
- if (direction === 'inbound' || direction === 'bidirectional') {
1223
- await this._propagateExistingData(targetHolon, sourceHolon, lensName, { mode, filter });
1224
- }
1225
-
1226
- this._metrics.federations = (this._metrics.federations || 0) + 1;
1227
- return true;
1228
- }
1347
+ // Note: federate() and unfederate() methods are provided by withFederationMethods mixin
1229
1348
 
1230
1349
  /**
1231
1350
  * Propagates existing data from one holon to another.
1351
+ * UNIFIED MODEL: Passes sourceAuthorPubKey for capability-based federation.
1232
1352
  *
1233
1353
  * @private
1234
1354
  * @param {string} fromHolon - Source holon H3 cell ID
@@ -1237,10 +1357,11 @@ class HoloSphereBase extends HoloSphereCore {
1237
1357
  * @param {Object} [options={}] - Propagation options
1238
1358
  * @param {string} [options.mode='reference'] - Mode: 'reference' or 'copy'
1239
1359
  * @param {Function} [options.filter] - Filter function to select data
1360
+ * @param {string} [options.sourceAuthorPubKey] - Source author's public key (defaults to client.publicKey)
1240
1361
  * @returns {Promise<void>}
1241
1362
  */
1242
1363
  async _propagateExistingData(fromHolon, toHolon, lensName, options = {}) {
1243
- const { mode = 'reference', filter = null } = options;
1364
+ const { mode = 'reference', filter = null, sourceAuthorPubKey } = options;
1244
1365
  const existingData = await this.read(fromHolon, lensName, null, { resolveHolograms: false });
1245
1366
  if (!existingData) return;
1246
1367
 
@@ -1250,6 +1371,7 @@ class HoloSphereBase extends HoloSphereCore {
1250
1371
  if (item.hologram === true) continue;
1251
1372
  // Apply filter if provided
1252
1373
  if (filter && !filter(item)) continue;
1374
+ // UNIFIED MODEL: Pass sourceAuthorPubKey for capability-based federation
1253
1375
  await federation.propagateData(
1254
1376
  this.client,
1255
1377
  this.config.appName,
@@ -1257,7 +1379,10 @@ class HoloSphereBase extends HoloSphereCore {
1257
1379
  fromHolon,
1258
1380
  toHolon,
1259
1381
  lensName,
1260
- mode
1382
+ mode,
1383
+ {
1384
+ sourceAuthorPubKey: sourceAuthorPubKey || this.client.publicKey,
1385
+ }
1261
1386
  );
1262
1387
  }
1263
1388
  }
@@ -1282,25 +1407,6 @@ class HoloSphereBase extends HoloSphereCore {
1282
1407
  return this._resolveHolograms(data);
1283
1408
  }
1284
1409
 
1285
- /**
1286
- * Removes federation between two holons for a specific lens.
1287
- *
1288
- * @param {string} sourceHolon - Source holon H3 cell ID
1289
- * @param {string} targetHolon - Target holon H3 cell ID
1290
- * @param {string} lensName - Name of the lens
1291
- * @returns {Promise<boolean>} Always returns true (idempotent operation)
1292
- */
1293
- async unfederate(sourceHolon, targetHolon, lensName) {
1294
- // Remove federation config for this relationship - idempotent
1295
- const configPath = storage.buildPath(this.config.appName, sourceHolon, lensName, '_federation');
1296
- try {
1297
- await storage.deleteData(this.client, configPath);
1298
- } catch (e) {
1299
- // Ignore errors - already unfederated or doesn't exist
1300
- }
1301
- return true;
1302
- }
1303
-
1304
1410
  /**
1305
1411
  * Updates local override values on a hologram.
1306
1412
  *
@@ -1412,6 +1518,7 @@ class HoloSphereBase extends HoloSphereCore {
1412
1518
  async propagateData(data, sourceHolon, targetHolon, lensName, options = {}) {
1413
1519
  // Extract mode from options, default to 'reference' for hologram creation
1414
1520
  const mode = options.mode || 'reference';
1521
+ // UNIFIED MODEL: Pass sourceAuthorPubKey for capability-based federation
1415
1522
  return federation.propagateData(
1416
1523
  this.client,
1417
1524
  this.config.appName,
@@ -1419,7 +1526,11 @@ class HoloSphereBase extends HoloSphereCore {
1419
1526
  sourceHolon,
1420
1527
  targetHolon,
1421
1528
  lensName,
1422
- mode
1529
+ mode,
1530
+ {
1531
+ sourceAuthorPubKey: options.sourceAuthorPubKey || this.client.publicKey,
1532
+ capability: options.capability,
1533
+ }
1423
1534
  );
1424
1535
  }
1425
1536
 
@@ -2267,10 +2378,33 @@ export {
2267
2378
  // Re-export types and utilities
2268
2379
  export { spatial, storage, schema, federation, handshake, crypto, nostrUtils, social, subscriptions, hierarchical };
2269
2380
 
2381
+ // Re-export federation submodules
2382
+ export { capabilities, requestCard, cardStorage, holonRegistry, registry };
2383
+
2270
2384
  // Re-export specific utilities used in tests
2271
2385
  export { matchScope } from './crypto/secp256k1.js';
2272
2386
  export { createHologram } from './federation/hologram.js';
2273
2387
 
2388
+ // Re-export federation card functions
2389
+ export {
2390
+ createFederationCard,
2391
+ getVisibleLenses,
2392
+ getLensConfigForHandshake,
2393
+ toggleLens,
2394
+ toggleCardExpansion,
2395
+ dismissCard,
2396
+ } from './federation/request-card.js';
2397
+
2398
+ // Re-export card storage functions
2399
+ export {
2400
+ saveCard,
2401
+ getCard,
2402
+ getDisplayableCards,
2403
+ dismissRequest,
2404
+ markResponseProcessed,
2405
+ isResponseProcessed,
2406
+ } from './federation/card-storage.js';
2407
+
2274
2408
  // Export AI factory function
2275
2409
  export { createAIServices } from './ai/index.js';
2276
2410