holosphere 1.1.19 → 2.0.0-alpha0

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 (146) hide show
  1. package/.env.example +36 -0
  2. package/.eslintrc.json +16 -0
  3. package/.prettierrc.json +7 -0
  4. package/README.md +476 -531
  5. package/bin/holosphere-activitypub.js +158 -0
  6. package/cleanup-test-data.js +204 -0
  7. package/examples/demo.html +1333 -0
  8. package/examples/example-bot.js +197 -0
  9. package/package.json +47 -87
  10. package/scripts/check-bundle-size.js +54 -0
  11. package/scripts/check-quest-ids.js +77 -0
  12. package/scripts/import-holons.js +578 -0
  13. package/scripts/publish-to-relay.js +101 -0
  14. package/scripts/read-example.js +186 -0
  15. package/scripts/relay-diagnostic.js +59 -0
  16. package/scripts/relay-example.js +179 -0
  17. package/scripts/resync-to-relay.js +245 -0
  18. package/scripts/revert-import.js +196 -0
  19. package/scripts/test-hybrid-mode.js +108 -0
  20. package/scripts/test-local-storage.js +63 -0
  21. package/scripts/test-nostr-direct.js +55 -0
  22. package/scripts/test-read-data.js +45 -0
  23. package/scripts/test-write-read.js +63 -0
  24. package/scripts/verify-import.js +95 -0
  25. package/scripts/verify-relay-data.js +139 -0
  26. package/src/ai/aggregation.js +319 -0
  27. package/src/ai/breakdown.js +511 -0
  28. package/src/ai/classifier.js +217 -0
  29. package/src/ai/council.js +228 -0
  30. package/src/ai/embeddings.js +279 -0
  31. package/src/ai/federation-ai.js +324 -0
  32. package/src/ai/h3-ai.js +955 -0
  33. package/src/ai/index.js +112 -0
  34. package/src/ai/json-ops.js +225 -0
  35. package/src/ai/llm-service.js +205 -0
  36. package/src/ai/nl-query.js +223 -0
  37. package/src/ai/relationships.js +353 -0
  38. package/src/ai/schema-extractor.js +218 -0
  39. package/src/ai/spatial.js +293 -0
  40. package/src/ai/tts.js +194 -0
  41. package/src/content/social-protocols.js +168 -0
  42. package/src/core/holosphere.js +273 -0
  43. package/src/crypto/secp256k1.js +259 -0
  44. package/src/federation/discovery.js +334 -0
  45. package/src/federation/hologram.js +1042 -0
  46. package/src/federation/registry.js +386 -0
  47. package/src/hierarchical/upcast.js +110 -0
  48. package/src/index.js +2669 -0
  49. package/src/schema/validator.js +91 -0
  50. package/src/spatial/h3-operations.js +110 -0
  51. package/src/storage/backend-factory.js +125 -0
  52. package/src/storage/backend-interface.js +142 -0
  53. package/src/storage/backends/activitypub/server.js +653 -0
  54. package/src/storage/backends/activitypub-backend.js +272 -0
  55. package/src/storage/backends/gundb-backend.js +233 -0
  56. package/src/storage/backends/nostr-backend.js +136 -0
  57. package/src/storage/filesystem-storage-browser.js +41 -0
  58. package/src/storage/filesystem-storage.js +138 -0
  59. package/src/storage/global-tables.js +81 -0
  60. package/src/storage/gun-async.js +281 -0
  61. package/src/storage/gun-wrapper.js +221 -0
  62. package/src/storage/indexeddb-storage.js +122 -0
  63. package/src/storage/key-storage-simple.js +76 -0
  64. package/src/storage/key-storage.js +136 -0
  65. package/src/storage/memory-storage.js +59 -0
  66. package/src/storage/migration.js +338 -0
  67. package/src/storage/nostr-async.js +811 -0
  68. package/src/storage/nostr-client.js +939 -0
  69. package/src/storage/nostr-wrapper.js +211 -0
  70. package/src/storage/outbox-queue.js +208 -0
  71. package/src/storage/persistent-storage.js +109 -0
  72. package/src/storage/sync-service.js +164 -0
  73. package/src/subscriptions/manager.js +142 -0
  74. package/test-ai-real-api.js +202 -0
  75. package/tests/unit/ai/aggregation.test.js +295 -0
  76. package/tests/unit/ai/breakdown.test.js +446 -0
  77. package/tests/unit/ai/classifier.test.js +294 -0
  78. package/tests/unit/ai/council.test.js +262 -0
  79. package/tests/unit/ai/embeddings.test.js +384 -0
  80. package/tests/unit/ai/federation-ai.test.js +344 -0
  81. package/tests/unit/ai/h3-ai.test.js +458 -0
  82. package/tests/unit/ai/index.test.js +304 -0
  83. package/tests/unit/ai/json-ops.test.js +307 -0
  84. package/tests/unit/ai/llm-service.test.js +390 -0
  85. package/tests/unit/ai/nl-query.test.js +383 -0
  86. package/tests/unit/ai/relationships.test.js +311 -0
  87. package/tests/unit/ai/schema-extractor.test.js +384 -0
  88. package/tests/unit/ai/spatial.test.js +279 -0
  89. package/tests/unit/ai/tts.test.js +279 -0
  90. package/tests/unit/content.test.js +332 -0
  91. package/tests/unit/contract/core.test.js +88 -0
  92. package/tests/unit/contract/crypto.test.js +198 -0
  93. package/tests/unit/contract/data.test.js +223 -0
  94. package/tests/unit/contract/federation.test.js +181 -0
  95. package/tests/unit/contract/hierarchical.test.js +113 -0
  96. package/tests/unit/contract/schema.test.js +114 -0
  97. package/tests/unit/contract/social.test.js +217 -0
  98. package/tests/unit/contract/spatial.test.js +110 -0
  99. package/tests/unit/contract/subscriptions.test.js +128 -0
  100. package/tests/unit/contract/utils.test.js +159 -0
  101. package/tests/unit/core.test.js +152 -0
  102. package/tests/unit/crypto.test.js +328 -0
  103. package/tests/unit/federation.test.js +234 -0
  104. package/tests/unit/gun-async.test.js +252 -0
  105. package/tests/unit/hierarchical.test.js +399 -0
  106. package/tests/unit/integration/scenario-01-geographic-storage.test.js +74 -0
  107. package/tests/unit/integration/scenario-02-federation.test.js +76 -0
  108. package/tests/unit/integration/scenario-03-subscriptions.test.js +102 -0
  109. package/tests/unit/integration/scenario-04-validation.test.js +129 -0
  110. package/tests/unit/integration/scenario-05-hierarchy.test.js +125 -0
  111. package/tests/unit/integration/scenario-06-social.test.js +135 -0
  112. package/tests/unit/integration/scenario-07-persistence.test.js +130 -0
  113. package/tests/unit/integration/scenario-08-authorization.test.js +161 -0
  114. package/tests/unit/integration/scenario-09-cross-dimensional.test.js +139 -0
  115. package/tests/unit/integration/scenario-10-cross-holosphere-capabilities.test.js +357 -0
  116. package/tests/unit/integration/scenario-11-cross-holosphere-federation.test.js +410 -0
  117. package/tests/unit/integration/scenario-12-capability-federated-read.test.js +719 -0
  118. package/tests/unit/performance/benchmark.test.js +85 -0
  119. package/tests/unit/schema.test.js +213 -0
  120. package/tests/unit/spatial.test.js +158 -0
  121. package/tests/unit/storage.test.js +195 -0
  122. package/tests/unit/subscriptions.test.js +328 -0
  123. package/tests/unit/test-data-permanence-debug.js +197 -0
  124. package/tests/unit/test-data-permanence.js +340 -0
  125. package/tests/unit/test-key-persistence-fixed.js +148 -0
  126. package/tests/unit/test-key-persistence.js +172 -0
  127. package/tests/unit/test-relay-permanence.js +376 -0
  128. package/tests/unit/test-second-node.js +95 -0
  129. package/tests/unit/test-simple-write.js +89 -0
  130. package/vite.config.js +49 -0
  131. package/vitest.config.js +20 -0
  132. package/FEDERATION.md +0 -213
  133. package/compute.js +0 -298
  134. package/content.js +0 -1022
  135. package/federation.js +0 -1234
  136. package/global.js +0 -736
  137. package/hexlib.js +0 -335
  138. package/hologram.js +0 -183
  139. package/holosphere-bundle.esm.js +0 -34549
  140. package/holosphere-bundle.js +0 -34580
  141. package/holosphere-bundle.min.js +0 -49
  142. package/holosphere.d.ts +0 -604
  143. package/holosphere.js +0 -739
  144. package/node.js +0 -246
  145. package/schema.js +0 -139
  146. package/utils.js +0 -302
@@ -0,0 +1,578 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Import script for holosphere from GUN folder structure
5
+ *
6
+ * Folder structure expected:
7
+ * - 3-level: {appOrHolon}/{lens}/{id}.json
8
+ * - 4-level: {app}/{holon}/{lens}/{id}.json
9
+ *
10
+ * Usage:
11
+ * node scripts/import-holons.js [options]
12
+ *
13
+ * Options:
14
+ * --source <path> Source directory (default: ../gun/Holons)
15
+ * --subfolder <path> Subfolder within source (e.g., "Appname/holonid")
16
+ * --app <name> App name for holosphere (default: use folder name or 'imported')
17
+ * --dry-run Show what would be imported without actually importing
18
+ * --verbose Show detailed progress
19
+ * --levels <3|4> Folder depth: 3 or 4 (default: auto-detect)
20
+ */
21
+
22
+ import fs from 'fs/promises';
23
+ import path from 'path';
24
+ import { HoloSphere } from '../src/index.js';
25
+ import dotenv from 'dotenv';
26
+
27
+ // Load environment variables
28
+ dotenv.config();
29
+
30
+ // Parse command line arguments
31
+ const args = process.argv.slice(2);
32
+ const options = {
33
+ source: '../Holons',
34
+ subfolder: null,
35
+ app: 'Holons',
36
+ dryRun: false,
37
+ verbose: true,
38
+ levels: 'auto',
39
+ relays: [process.env.HOLOSPHERE_RELAY || 'wss://relay.holons.io'],
40
+ persistence: true,
41
+ // Retry and rate limiting options
42
+ maxRetries: 3,
43
+ retryDelay: 1000,
44
+ writeDelay: 100,
45
+ verifyAndResync: true
46
+ };
47
+
48
+ for (let i = 0; i < args.length; i++) {
49
+ switch (args[i]) {
50
+ case '--source':
51
+ options.source = args[++i];
52
+ break;
53
+ case '--subfolder':
54
+ options.subfolder = args[++i];
55
+ break;
56
+ case '--app':
57
+ options.app = args[++i];
58
+ break;
59
+ case '--dry-run':
60
+ options.dryRun = true;
61
+ break;
62
+ case '--verbose':
63
+ options.verbose = true;
64
+ break;
65
+ case '--relay':
66
+ options.relays = [args[++i]];
67
+ break;
68
+ case '--levels':
69
+ options.levels = args[++i];
70
+ break;
71
+ case '--help':
72
+ console.log(`
73
+ Import holons from GUN folder structure into holosphere
74
+
75
+ Usage: node scripts/import-holons.js [options]
76
+
77
+ Options:
78
+ --source <path> Source directory (default: ../Holons)
79
+ --subfolder <path> Subfolder within source (e.g., "Appname/holonid")
80
+ --app <name> App name for holosphere (default: use folder name or 'Holons')
81
+ --dry-run Show what would be imported without actually importing
82
+ --verbose Show detailed progress
83
+ --relay <url> Relay URL (default: from HOLOSPHERE_RELAY env or wss://relay.holons.io)
84
+ --levels <3|4> Folder depth: 3 or 4 (default: auto-detect)
85
+ --help Show this help message
86
+
87
+ Features:
88
+ - Automatic retry logic (up to 3 retries per item)
89
+ - Rate limiting to prevent overwhelming the relay (100ms between writes)
90
+ - Post-import verification and automatic resync of missing items
91
+ - All data is guaranteed to be on the relay when import completes
92
+
93
+ Examples:
94
+ # Import from default location
95
+ node scripts/import-holons.js
96
+
97
+ # Import with specific app name
98
+ node scripts/import-holons.js --app myapp
99
+
100
+ # Dry run to see what would be imported
101
+ node scripts/import-holons.js --dry-run --verbose
102
+
103
+ # Import from custom location
104
+ node scripts/import-holons.js --source /path/to/holons
105
+
106
+ # Import from specific subfolder (recommended for single holon)
107
+ node scripts/import-holons.js --subfolder 235114395
108
+ `);
109
+ process.exit(0);
110
+ }
111
+ }
112
+
113
+ // Statistics
114
+ const stats = {
115
+ apps: new Set(),
116
+ holons: new Set(),
117
+ lenses: new Set(),
118
+ items: 0,
119
+ errors: 0,
120
+ skipped: 0,
121
+ retries: 0,
122
+ resynced: 0
123
+ };
124
+
125
+ /**
126
+ * Sleep helper for delays
127
+ */
128
+ async function sleep(ms) {
129
+ return new Promise(resolve => setTimeout(resolve, ms));
130
+ }
131
+
132
+ /**
133
+ * Write with retry logic
134
+ */
135
+ async function writeWithRetry(hs, holonId, lensName, data, retries = 0) {
136
+ try {
137
+ if (holonId === 'global') {
138
+ await hs.writeGlobal(lensName, data);
139
+ } else {
140
+ await hs.write(holonId, lensName, data);
141
+ }
142
+ return true;
143
+ } catch (error) {
144
+ if (retries < options.maxRetries) {
145
+ stats.retries++;
146
+ if (options.verbose) {
147
+ console.log(` Retry ${retries + 1}/${options.maxRetries} for ${lensName}/${data.id}`);
148
+ }
149
+ await sleep(options.retryDelay);
150
+ return writeWithRetry(hs, holonId, lensName, data, retries + 1);
151
+ } else {
152
+ throw error;
153
+ }
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Check if a string is a valid H3 cell ID
159
+ */
160
+ function isH3Cell(str) {
161
+ return /^[0-9a-f]{15}$/i.test(str);
162
+ }
163
+
164
+ /**
165
+ * Recursively scan directory and import data
166
+ */
167
+ async function scanAndImport(dir, hs, depth = 0, context = {}) {
168
+ try {
169
+ const entries = await fs.readdir(dir, { withFileTypes: true });
170
+
171
+ for (const entry of entries) {
172
+ const fullPath = path.join(dir, entry.name);
173
+
174
+ if (entry.isDirectory()) {
175
+ // Skip special directories
176
+ if (entry.name.startsWith('.') || entry.name === '_holograms') {
177
+ continue;
178
+ }
179
+
180
+ // Update context based on depth
181
+ const newContext = { ...context };
182
+
183
+ if (depth === 0) {
184
+ // First level: if we have a holon from subfolder, treat this as lens
185
+ if (context.holon) {
186
+ newContext.lens = entry.name;
187
+ stats.lenses.add(entry.name);
188
+ } else {
189
+ // Otherwise, treat as app name or holon ID
190
+ newContext.appOrHolon = entry.name;
191
+ stats.apps.add(entry.name);
192
+ }
193
+ } else if (depth === 1) {
194
+ // Second level: could be holon (if 4-level) or lens (if 3-level)
195
+ // If we already have a lens, this is just a container folder - keep the lens
196
+ if (context.lens) {
197
+ // Don't change lens, just keep recursing
198
+ // This handles cases like quests/1002/ where 1002 is just a container
199
+ } else if (isH3Cell(entry.name)) {
200
+ // Auto-detect: if it's an H3 cell, treat as holon
201
+ newContext.holon = entry.name;
202
+ stats.holons.add(entry.name);
203
+ } else {
204
+ // Otherwise treat as lens
205
+ newContext.lens = entry.name;
206
+ stats.lenses.add(entry.name);
207
+ }
208
+ } else if (depth === 2) {
209
+ // Third level or deeper: if we have a lens, keep it (container folders)
210
+ // Otherwise set as lens (4-level structure)
211
+ if (!context.lens) {
212
+ newContext.lens = entry.name;
213
+ stats.lenses.add(entry.name);
214
+ }
215
+ }
216
+
217
+ // Recurse into subdirectory
218
+ await scanAndImport(fullPath, hs, depth + 1, newContext);
219
+
220
+ } else if (entry.isFile() && entry.name.endsWith('.json')) {
221
+ // Import JSON file
222
+ await importFile(fullPath, hs, context, entry.name);
223
+ }
224
+ }
225
+ } catch (error) {
226
+ console.error(`Error scanning directory ${dir}:`, error.message);
227
+ stats.errors++;
228
+ }
229
+ }
230
+
231
+ /**
232
+ * Import a single JSON file into holosphere
233
+ */
234
+ async function importFile(filePath, hs, context, filename) {
235
+ try {
236
+ // Read and parse JSON file
237
+ const content = await fs.readFile(filePath, 'utf-8');
238
+ let data = JSON.parse(content);
239
+
240
+ // Handle double-encoded JSON strings
241
+ if (typeof data === 'string') {
242
+ try {
243
+ data = JSON.parse(data);
244
+ } catch (e) {
245
+ // If it fails to parse again, keep it as a string value
246
+ // Wrap it in an object
247
+ data = { value: data };
248
+ }
249
+ }
250
+
251
+ // Handle non-object data
252
+ if (typeof data !== 'object' || data === null) {
253
+ data = { value: data };
254
+ }
255
+
256
+ // Extract ID from filename (remove .json extension)
257
+ const id = path.basename(filename, '.json');
258
+
259
+ // Ensure data has an id field
260
+ if (!data.id) {
261
+ data.id = id;
262
+ }
263
+
264
+ // Determine app, holon, and lens
265
+ let appName, holonId, lensName;
266
+
267
+ if (context.holon) {
268
+ // 4-level structure: app/holon/lens/id
269
+ appName = context.appOrHolon || options.app || 'imported';
270
+ holonId = context.holon;
271
+ lensName = context.lens;
272
+ } else if (context.lens) {
273
+ // 3-level structure: could be app/lens/id or holon/lens/id
274
+ // If the first level is an H3 cell, treat it as holon with default app
275
+ if (isH3Cell(context.appOrHolon)) {
276
+ appName = options.app || 'imported';
277
+ holonId = context.appOrHolon;
278
+ } else {
279
+ appName = context.appOrHolon || options.app || 'imported';
280
+ holonId = 'global'; // Use global for non-geographic data
281
+ }
282
+ lensName = context.lens;
283
+ } else if (context.appOrHolon) {
284
+ // 2-level structure: treat first level as lens, second as id
285
+ // This handles root-level files like /announcements/13.json
286
+ appName = options.app || 'imported';
287
+ holonId = 'global';
288
+ lensName = context.appOrHolon;
289
+ } else {
290
+ console.warn(`Skipping ${filePath}: unable to determine structure`);
291
+ stats.skipped++;
292
+ return;
293
+ }
294
+
295
+ if (!lensName) {
296
+ console.warn(`Skipping ${filePath}: no lens found`);
297
+ stats.skipped++;
298
+ return;
299
+ }
300
+
301
+ if (options.verbose) {
302
+ console.log(`Importing: app=${appName}, holon=${holonId}, lens=${lensName}, id=${id}`);
303
+ }
304
+
305
+ if (!options.dryRun) {
306
+ // Import into holosphere with retry logic
307
+ await writeWithRetry(hs, holonId, lensName, data);
308
+
309
+ // Rate limiting between writes
310
+ if (options.writeDelay > 0) {
311
+ await sleep(options.writeDelay);
312
+ }
313
+ }
314
+
315
+ stats.items++;
316
+
317
+ if (stats.items % 100 === 0) {
318
+ console.log(`Imported ${stats.items} items...`);
319
+ }
320
+
321
+ } catch (error) {
322
+ console.error(`Error importing ${filePath}:`, error.message);
323
+ stats.errors++;
324
+ }
325
+ }
326
+
327
+ /**
328
+ * Verify and resync data to relay
329
+ */
330
+ async function verifyAndResync(hs, initialContext) {
331
+ console.log('\n======================');
332
+ console.log('Verifying Relay Sync');
333
+ console.log('======================\n');
334
+
335
+ // Get the holon ID from context
336
+ const holonId = initialContext.holon;
337
+ if (!holonId) {
338
+ console.log('No holon ID found, skipping verification');
339
+ return;
340
+ }
341
+
342
+ console.log(`Checking holon: ${holonId}`);
343
+ console.log('Waiting for relay sync...\n');
344
+ await sleep(3000); // Wait for initial sync
345
+
346
+ // Initialize a relay-only client to check what's actually on the relay
347
+ const hsRelay = new HoloSphere({
348
+ appName: options.app || 'imported',
349
+ relays: options.relays,
350
+ persistence: false, // Don't use local storage
351
+ logLevel: 'ERROR',
352
+ privateKey: process.env.HOLOSPHERE_PRIVATE_KEY
353
+ });
354
+
355
+ await sleep(2000);
356
+
357
+ const lensesToCheck = Array.from(stats.lenses);
358
+ let totalMissing = 0;
359
+ const missingByLens = {};
360
+
361
+ console.log('Comparing local vs relay data:\n');
362
+ console.log('Lens'.padEnd(20) + 'Local'.padEnd(10) + 'Relay'.padEnd(10) + 'Missing');
363
+ console.log('-'.repeat(50));
364
+
365
+ for (const lens of lensesToCheck) {
366
+ try {
367
+ // Read from local (hs has both local and relay)
368
+ const localData = await hs.read(holonId, lens);
369
+ // Read from relay only
370
+ const relayData = await hsRelay.read(holonId, lens);
371
+
372
+ const localMap = new Map();
373
+ const relayMap = new Map();
374
+
375
+ // Build local map
376
+ if (Array.isArray(localData)) {
377
+ localData.forEach(item => {
378
+ if (item && item.id) localMap.set(item.id.toString(), item);
379
+ });
380
+ } else if (localData && typeof localData === 'object') {
381
+ Object.keys(localData).forEach(key => {
382
+ const item = localData[key];
383
+ if (item && item.id) localMap.set(item.id.toString(), item);
384
+ });
385
+ }
386
+
387
+ // Build relay map
388
+ if (Array.isArray(relayData)) {
389
+ relayData.forEach(item => {
390
+ if (item && item.id) relayMap.set(item.id.toString(), item);
391
+ });
392
+ } else if (relayData && typeof relayData === 'object') {
393
+ Object.keys(relayData).forEach(key => {
394
+ const item = relayData[key];
395
+ if (item && item.id) relayMap.set(item.id.toString(), item);
396
+ });
397
+ }
398
+
399
+ const missing = [];
400
+ for (const [id, item] of localMap) {
401
+ if (!relayMap.has(id)) {
402
+ missing.push(item);
403
+ }
404
+ }
405
+
406
+ const missingCount = missing.length;
407
+ totalMissing += missingCount;
408
+
409
+ if (missingCount > 0) {
410
+ missingByLens[lens] = missing;
411
+ }
412
+
413
+ console.log(
414
+ lens.padEnd(20) +
415
+ localMap.size.toString().padEnd(10) +
416
+ relayMap.size.toString().padEnd(10) +
417
+ missingCount.toString()
418
+ );
419
+ } catch (error) {
420
+ console.log(lens.padEnd(20) + 'Error: ' + error.message);
421
+ }
422
+ }
423
+
424
+ console.log('-'.repeat(50));
425
+ console.log(`Total missing from relay: ${totalMissing}`);
426
+
427
+ // Resync missing items
428
+ if (totalMissing > 0) {
429
+ console.log('\n======================');
430
+ console.log('Re-syncing Missing Items');
431
+ console.log('======================\n');
432
+
433
+ for (const [lens, missing] of Object.entries(missingByLens)) {
434
+ console.log(`\nSyncing ${missing.length} items for lens: ${lens}`);
435
+
436
+ for (const item of missing) {
437
+ try {
438
+ await writeWithRetry(hsRelay, holonId, lens, item);
439
+ stats.resynced++;
440
+ if (options.verbose) {
441
+ console.log(` ✓ ${lens}/${item.id}`);
442
+ }
443
+ await sleep(options.writeDelay);
444
+ } catch (error) {
445
+ console.log(` ✗ ${lens}/${item.id}: ${error.message}`);
446
+ }
447
+ }
448
+ }
449
+
450
+ console.log(`\n✓ Re-synced ${stats.resynced} items to relay`);
451
+ } else {
452
+ console.log('\n✓ All data is already on the relay!');
453
+ }
454
+ }
455
+
456
+ /**
457
+ * Main import function
458
+ */
459
+ async function main() {
460
+ console.log('Holosphere Import Tool');
461
+ console.log('======================\n');
462
+
463
+ // Resolve source path
464
+ let sourcePath = path.resolve(options.source);
465
+ let initialContext = {};
466
+ let startDepth = 0;
467
+
468
+ // Handle subfolder as holon ID
469
+ if (options.subfolder) {
470
+ sourcePath = path.join(sourcePath, options.subfolder);
471
+
472
+ // Extract holon ID from subfolder (last component of path)
473
+ const holonId = path.basename(options.subfolder);
474
+
475
+ // Pre-populate context with holon ID
476
+ // When holon is set, folders will be treated as lenses even at depth 0
477
+ initialContext = {
478
+ holon: holonId
479
+ };
480
+
481
+ stats.holons.add(holonId);
482
+
483
+ console.log(`Using subfolder as holon ID: ${holonId}`);
484
+ }
485
+
486
+ try {
487
+ await fs.access(sourcePath);
488
+ } catch (error) {
489
+ console.error(`Error: Source directory not found: ${sourcePath}`);
490
+ process.exit(1);
491
+ }
492
+
493
+ console.log(`Source: ${sourcePath}`);
494
+ if (options.subfolder) {
495
+ console.log(`Subfolder: ${options.subfolder}`);
496
+ console.log(`Holon ID: ${initialContext.holon}`);
497
+ }
498
+ console.log(`App: ${options.app || 'auto-detect'}`);
499
+ console.log(`Dry run: ${options.dryRun ? 'Yes' : 'No'}`);
500
+ console.log('');
501
+
502
+ // Initialize holosphere
503
+ let hs = null;
504
+ if (!options.dryRun) {
505
+ console.log('Initializing holosphere...');
506
+ hs = new HoloSphere({
507
+ appName: options.app || 'imported',
508
+ relays: options.relays,
509
+ persistence: options.persistence,
510
+ logLevel: options.verbose ? 'DEBUG' : 'INFO',
511
+ privateKey: process.env.HOLOSPHERE_PRIVATE_KEY
512
+ });
513
+ console.log('Holosphere initialized.\n');
514
+ }
515
+
516
+ // Start import
517
+ const startTime = Date.now();
518
+ console.log('Starting import...\n');
519
+
520
+ await scanAndImport(sourcePath, hs, startDepth, initialContext);
521
+
522
+ const duration = ((Date.now() - startTime) / 1000).toFixed(2);
523
+
524
+ // Print statistics
525
+ console.log('\n======================');
526
+ console.log('Import Complete');
527
+ console.log('======================');
528
+ console.log(`Duration: ${duration}s`);
529
+ console.log(`Apps: ${stats.apps.size}`);
530
+ console.log(`Holons: ${stats.holons.size}`);
531
+ console.log(`Lenses: ${stats.lenses.size}`);
532
+ console.log(`Items imported: ${stats.items}`);
533
+ console.log(`Errors: ${stats.errors}`);
534
+ console.log(`Skipped: ${stats.skipped}`);
535
+ if (stats.retries > 0) {
536
+ console.log(`Retries: ${stats.retries}`);
537
+ }
538
+
539
+ if (options.verbose) {
540
+ console.log('\nApps:', Array.from(stats.apps).join(', '));
541
+ console.log('Holons:', Array.from(stats.holons).join(', '));
542
+ console.log('Lenses:', Array.from(stats.lenses).join(', '));
543
+ }
544
+
545
+ if (!options.dryRun && hs) {
546
+ console.log('\nHolosphere metrics:', hs.metrics());
547
+
548
+ // Verify and resync if enabled
549
+ if (options.verifyAndResync && options.relays.length > 0) {
550
+ await verifyAndResync(hs, initialContext);
551
+ }
552
+ }
553
+
554
+ // Cleanup: Close connections and exit
555
+ if (!options.dryRun && hs) {
556
+ console.log('\nClosing connections...');
557
+ if (hs.client && hs.client.pool && hs.client.pool.close) {
558
+ try {
559
+ hs.client.pool.close(hs.client.relays);
560
+ } catch (e) {
561
+ // Ignore errors during cleanup
562
+ }
563
+ }
564
+ }
565
+
566
+ if (stats.resynced > 0) {
567
+ console.log(`\n✓ Import complete - ${stats.items} items imported, ${stats.resynced} re-synced to relay\n`);
568
+ } else {
569
+ console.log('\n✓ Import complete\n');
570
+ }
571
+ process.exit(0);
572
+ }
573
+
574
+ // Run main function
575
+ main().catch(error => {
576
+ console.error('Fatal error:', error);
577
+ process.exit(1);
578
+ });
@@ -0,0 +1,101 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Publish a sample of imported data to relay.holons.io
5
+ */
6
+
7
+ import { HoloSphere } from '../src/index.js';
8
+ import dotenv from 'dotenv';
9
+
10
+ // Load environment variables
11
+ dotenv.config();
12
+
13
+ async function main() {
14
+ console.log('Publishing Sample Data to relay.holons.io');
15
+ console.log('==========================================\n');
16
+
17
+ // First, read from local cache (no relays)
18
+ console.log('Step 1: Reading sample data from local cache...');
19
+ const hsLocal = new HoloSphere({
20
+ appName: 'imported',
21
+ relays: [],
22
+ persistence: true,
23
+ logLevel: 'INFO',
24
+ privateKey: process.env.HOLOSPHERE_PRIVATE_KEY
25
+ });
26
+
27
+ await new Promise(resolve => setTimeout(resolve, 1000));
28
+
29
+ const quests = await hsLocal.readGlobal('quests');
30
+ const questArray = Object.values(quests || {});
31
+
32
+ console.log(`✓ Found ${questArray.length} quests locally\n`);
33
+
34
+ if (questArray.length === 0) {
35
+ console.log('No quests found to publish.');
36
+ return;
37
+ }
38
+
39
+ // Take first 5 quests
40
+ const sampleQuests = questArray.slice(0, 5);
41
+ console.log(`Selected ${sampleQuests.length} quests to publish:\n`);
42
+
43
+ sampleQuests.forEach((q, idx) => {
44
+ console.log(`${idx + 1}. ${q.title || q.id} (${q.status || 'unknown'})`);
45
+ });
46
+
47
+ console.log('\n');
48
+
49
+ // Now publish to relay.holons.io
50
+ console.log('Step 2: Publishing to relay.holons.io...');
51
+ const hsRelay = new HoloSphere({
52
+ appName: 'imported',
53
+ relays: ['wss://relay.holons.io'],
54
+ persistence: true,
55
+ logLevel: 'INFO',
56
+ privateKey: process.env.HOLOSPHERE_PRIVATE_KEY
57
+ });
58
+
59
+ let successCount = 0;
60
+ for (const quest of sampleQuests) {
61
+ try {
62
+ const success = await hsRelay.writeGlobal('quests', quest);
63
+ if (success) {
64
+ console.log(`✓ Published: ${quest.title || quest.id}`);
65
+ successCount++;
66
+ } else {
67
+ console.log(`✗ Failed: ${quest.title || quest.id}`);
68
+ }
69
+ } catch (error) {
70
+ console.log(`✗ Error: ${quest.title || quest.id} - ${error.message}`);
71
+ }
72
+ }
73
+
74
+ console.log(`\n✓ Published ${successCount}/${sampleQuests.length} quests\n`);
75
+
76
+ // Wait for propagation
77
+ console.log('Step 3: Waiting for relay propagation...');
78
+ await new Promise(resolve => setTimeout(resolve, 3000));
79
+ console.log('✓ Wait complete\n');
80
+
81
+ // Read back from relay
82
+ console.log('Step 4: Reading back from relay.holons.io...');
83
+ const retrievedQuests = await hsRelay.readGlobal('quests');
84
+ const retrievedArray = Object.values(retrievedQuests || {});
85
+
86
+ console.log(`✓ Found ${retrievedArray.length} quests on relay\n`);
87
+
88
+ if (retrievedArray.length > 0) {
89
+ console.log('Retrieved quests:');
90
+ retrievedArray.forEach((q, idx) => {
91
+ console.log(`${idx + 1}. ${q.title || q.id}`);
92
+ console.log(` Status: ${q.status || 'unknown'}`);
93
+ console.log(` ID: ${q.id}`);
94
+ console.log('');
95
+ });
96
+ }
97
+
98
+ console.log('✓ Complete!');
99
+ }
100
+
101
+ main().catch(console.error);