sovereign-mesh.js
1 #!/usr/bin/env node 2 /** 3 * Sovereign Mesh - Unified P2P Context Sync 4 * 5 * The operator is the pilot. The mnemonic is their license number. 6 * 7 * ONE daemon that does everything: 8 * - HTTP relay (for iOS Shortcuts) 9 * - Nostr broadcast (encrypted public channel) 10 * - Hyperswarm mesh (peer-to-peer sync) 11 * 12 * All keys derive from the operator's mnemonic. Any machine with the 13 * mnemonic joins the operator's mesh automatically. 14 * 15 * Usage: 16 * node sovereign-mesh.js --mnemonic "amber-falcon" # Join with mnemonic 17 * node sovereign-mesh.js --mnemonic "amber-falcon" --http # With HTTP relay 18 * node sovereign-mesh.js --name "mac-mini" # Set instance name 19 * node sovereign-mesh.js init # Generate new mnemonic 20 */ 21 22 import Hyperswarm from 'hyperswarm' 23 import crypto from 'crypto' 24 import http from 'http' 25 import fs from 'fs' 26 import path from 'path' 27 import os from 'os' 28 import { finalizeEvent } from 'nostr-tools' 29 import { Relay } from 'nostr-tools/relay' 30 import * as bip39 from 'bip39' 31 import { pbkdf2 } from '@noble/hashes/pbkdf2.js' 32 import { sha256 } from '@noble/hashes/sha2.js' 33 import { getMnemonicFromVault, vaultExists } from './sovereign-vault.js' 34 35 // ═══════════════════════════════════════════════════════════════════════════ 36 // CONFIGURATION 37 // ═══════════════════════════════════════════════════════════════════════════ 38 const HTTP_PORT = 7778 // 7777 used by hypercore_daemon 39 const CONFIG_DIR = path.join(os.homedir(), '.sovereign') 40 const MNEMONIC_FILE = path.join(CONFIG_DIR, 'operator-mnemonic.json') 41 const STATE_FILE = path.join(CONFIG_DIR, 'mesh-state.json') // Persistence file 42 43 const RELAYS = ['wss://relay.damus.io', 'wss://nos.lol', 'wss://nostr.wine'] 44 const EVENT_KIND = 30078 45 46 // ═══════════════════════════════════════════════════════════════════════════ 47 // BIP-39 MNEMONIC & KEY DERIVATION 48 // Proper cryptographic security using industry standards 49 // ═══════════════════════════════════════════════════════════════════════════ 50 51 // Security parameters 52 const MNEMONIC_WORDS = 4 // 4 words = 44 bits entropy (17.6 trillion combinations) 53 const PBKDF2_ITERATIONS = 2048 // Standard BIP-39 iteration count 54 const ENTROPY_BITS = MNEMONIC_WORDS * 11 // 11 bits per BIP-39 word 55 56 /** 57 * Generate a new operator mnemonic using BIP-39. 58 * 59 * 4 words provides 44 bits of entropy: 60 * - 2048^4 = 17.6 trillion combinations 61 * - Brute force at 1M/sec = 200+ days 62 * - Human-speakable and memorable 63 * 64 * For higher security, use 6 words (66 bits) or 12 words (128 bits). 65 */ 66 function generateMnemonic(wordCount = MNEMONIC_WORDS) { 67 // Generate full BIP-39 mnemonic and take first N words 68 // This ensures proper entropy and checksum handling 69 const fullMnemonic = bip39.generateMnemonic() 70 const words = fullMnemonic.split(' ').slice(0, wordCount) 71 return words.join('-') 72 } 73 74 /** 75 * Validate a mnemonic against BIP-39 wordlist. 76 */ 77 function validateMnemonic(mnemonic) { 78 const words = mnemonic.replace(/-/g, ' ').split(' ') 79 const wordlist = bip39.wordlists.english 80 return words.every(word => wordlist.includes(word.toLowerCase())) 81 } 82 83 /** 84 * Derive all keys from operator mnemonic using PBKDF2. 85 * 86 * PBKDF2-HMAC-SHA256 with 2048 iterations provides: 87 * - Slow key derivation (hard to brute force) 88 * - Deterministic output (same mnemonic = same keys) 89 * - Industry standard (BIP-39 compatible) 90 */ 91 function deriveKeysFromMnemonic(mnemonic) { 92 const normalized = mnemonic.toLowerCase().trim().replace(/\s+/g, ' ') 93 const mnemonicBytes = new TextEncoder().encode(normalized) 94 95 // Derive master seed using PBKDF2 (BIP-39 standard) 96 const masterSeed = pbkdf2(sha256, mnemonicBytes, 97 new TextEncoder().encode('sovereign-mesh'), 98 { c: PBKDF2_ITERATIONS, dkLen: 64 } 99 ) 100 101 // Derive individual keys from master seed 102 const sovereignKey = pbkdf2(sha256, masterSeed, 103 new TextEncoder().encode('sovereign'), 104 { c: 1, dkLen: 32 } 105 ) 106 107 const meshTopic = pbkdf2(sha256, masterSeed, 108 new TextEncoder().encode('mesh'), 109 { c: 1, dkLen: 32 } 110 ) 111 112 const nostrSecretKey = pbkdf2(sha256, masterSeed, 113 new TextEncoder().encode('nostr'), 114 { c: 1, dkLen: 32 } 115 ) 116 117 // Identifier for tagging (not security-critical) 118 const identifier = Buffer.from(sha256(mnemonicBytes)).toString('hex').slice(0, 16) 119 120 return { 121 sovereignKey: Buffer.from(sovereignKey), 122 meshTopic: Buffer.from(meshTopic), 123 nostrSecretKey: Buffer.from(nostrSecretKey), 124 identifier, 125 mnemonic: normalized.replace(/\s+/g, '-') 126 } 127 } 128 129 async function loadOrCreateMnemonic() { 130 fs.mkdirSync(CONFIG_DIR, { recursive: true }) 131 132 let vaultError = null 133 134 // Priority 1: Check P2P vault (Autopass) 135 if (vaultExists()) { 136 try { 137 const vaultMnemonic = await getMnemonicFromVault(true) // throwOnError=true 138 if (vaultMnemonic) { 139 console.log('[mesh] Mnemonic loaded from vault') 140 return vaultMnemonic 141 } 142 } catch (err) { 143 vaultError = err.message 144 if (err.message.includes('locked')) { 145 console.log(`[vault] Warning: Vault is locked by another process`) 146 } else { 147 console.log(`[vault] Warning: Could not read vault (${err.message})`) 148 } 149 } 150 } 151 152 // Priority 2: Check local config file (JSON) 153 // This is the fallback when vault is locked or unavailable 154 if (fs.existsSync(MNEMONIC_FILE)) { 155 try { 156 const data = JSON.parse(fs.readFileSync(MNEMONIC_FILE, 'utf8')) 157 if (data.mnemonic) { 158 if (vaultError) { 159 console.log('[mesh] Mnemonic loaded from JSON (vault unavailable)') 160 } else { 161 console.log('[mesh] Mnemonic loaded from local config') 162 } 163 return data.mnemonic 164 } 165 } catch (err) { 166 console.log(`[mesh] Warning: Could not read mnemonic file: ${err.message}`) 167 } 168 } 169 170 // Priority 3: Check for legacy key files 171 // Only use legacy if neither vault nor JSON has a mnemonic 172 const KEY_FILE = path.join(CONFIG_DIR, 'broadcast-key.json') 173 if (fs.existsSync(KEY_FILE)) { 174 console.log('[mesh] Warning: Using legacy keys (no mnemonic found)') 175 console.log('[mesh] Run "node sovereign-vault.js init" to migrate to mnemonic') 176 return null 177 } 178 179 return null 180 } 181 182 // ═══════════════════════════════════════════════════════════════════════════ 183 // ENCRYPTION 184 // ═══════════════════════════════════════════════════════════════════════════ 185 function encrypt(plaintext, key) { 186 const iv = crypto.randomBytes(12) 187 const cipher = crypto.createCipheriv('aes-256-gcm', key, iv) 188 let encrypted = cipher.update(plaintext, 'utf8', 'base64') 189 encrypted += cipher.final('base64') 190 const authTag = cipher.getAuthTag() 191 return `${iv.toString('base64')}:${authTag.toString('base64')}:${encrypted}` 192 } 193 194 function decrypt(ciphertext, key) { 195 const parts = ciphertext.split(':') 196 if (parts.length !== 3) throw new Error('Invalid ciphertext') 197 const iv = Buffer.from(parts[0], 'base64') 198 const authTag = Buffer.from(parts[1], 'base64') 199 const encrypted = parts[2] 200 const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv) 201 decipher.setAuthTag(authTag) 202 let decrypted = decipher.update(encrypted, 'base64', 'utf8') 203 decrypted += decipher.final('utf8') 204 return decrypted 205 } 206 207 // ═══════════════════════════════════════════════════════════════════════════ 208 // SOVEREIGN MESH NODE 209 // ═══════════════════════════════════════════════════════════════════════════ 210 class SovereignMesh { 211 constructor(options = {}) { 212 this.name = options.name || os.hostname() 213 this.enableHttp = options.http || false 214 this.mnemonic = options.mnemonic || null 215 216 this.swarm = null 217 this.peers = new Map() 218 this.httpServer = null 219 220 // Keys (derived from mnemonic or loaded from files) 221 this.nostrConnections = [] 222 this.sovereignKey = null 223 this.nostrSecretKey = null 224 this.nostrPublicKey = null 225 this.identifier = null 226 this.meshTopic = null 227 228 // Uptime tracking 229 this.startTime = Date.now() 230 231 // Deduplication 232 this.seen = new Set() 233 this.SEEN_TTL = 60000 234 235 // Context storage for bootstrap (silence-led context buffer) 236 this.foStates = new Map() // nodeId -> FO state from each instance 237 this.recentMessages = [] // Last N messages for context 238 this.MAX_RECENT_MESSAGES = 50 // Keep last 50 messages 239 this.FO_STATE_TTL = 300000 // FO states expire after 5 minutes 240 241 // Aha Moments - high-importance insights (importance >= 0.7) 242 // These persist longer and get priority in bootstrap 243 this.ahaMoments = [] // Recent aha moments across all instances 244 this.MAX_AHA_MOMENTS = 20 // Keep last 20 aha moments 245 this.AHA_MOMENT_TTL = 3600000 // Aha moments persist for 1 hour 246 247 // Convergence detection - when instances work on similar topics 248 this.convergenceAlerts = [] // Active convergence alerts 249 this.MAX_CONVERGENCE_ALERTS = 10 250 this.CONVERGENCE_TTL = 1800000 // 30 minutes 251 252 // Principle candidates - new principles propagated across mesh 253 this.principleCandidates = [] // Active principle candidates 254 this.MAX_PRINCIPLE_CANDIDATES = 20 255 this.PRINCIPLE_CANDIDATE_TTL = 86400000 // 24 hours (candidates need time to be tested) 256 257 // Confirmed principles - ready to be written to CLAUDE.md 258 this.confirmedPrinciples = [] // Principles that passed testing 259 this.MAX_CONFIRMED_PRINCIPLES = 50 260 this.CONFIRMED_PRINCIPLE_TTL = 604800000 // 7 days (keep for a week) 261 262 // Confirmation thresholds 263 this.CONFIRM_MIN_VOTES = 5 // Minimum votes to confirm 264 this.CONFIRM_SUPPORT_RATIO = 0.8 // 80% support to confirm 265 this.CONFIRM_TESTING_HOURS = 24 // Hours in testing before auto-confirm at 70% 266 267 // Persistence 268 this.STATE_SAVE_INTERVAL = 30000 // Save state every 30 seconds 269 this.lastStateSave = 0 270 } 271 272 // ═══════════════════════════════════════════════════════════════════════════ 273 // STATE PERSISTENCE 274 // ═══════════════════════════════════════════════════════════════════════════ 275 276 /** 277 * Save mesh state to disk for persistence across restarts. 278 */ 279 saveState() { 280 const state = { 281 savedAt: new Date().toISOString(), 282 version: 1, 283 284 // Principle candidates (most important to persist) 285 principleCandidates: this.principleCandidates, 286 287 // Confirmed principles 288 confirmedPrinciples: this.confirmedPrinciples, 289 290 // Aha moments (persist recent ones) 291 ahaMoments: this.ahaMoments, 292 293 // Convergence alerts (short-lived, but useful) 294 convergenceAlerts: this.convergenceAlerts, 295 296 // Recent messages (limited persistence) 297 recentMessages: this.recentMessages.slice(-20), 298 299 // FO states (short-lived, but helps bootstrap) 300 foStates: Array.from(this.foStates.entries()) 301 } 302 303 try { 304 fs.writeFileSync(STATE_FILE, JSON.stringify(state, null, 2)) 305 console.log(`[mesh] 💾 State saved (${this.principleCandidates.length} candidates, ${this.confirmedPrinciples.length} confirmed)`) 306 } catch (err) { 307 console.log(`[mesh] ⚠ Failed to save state: ${err.message}`) 308 } 309 } 310 311 /** 312 * Load mesh state from disk on startup. 313 */ 314 loadState() { 315 if (!fs.existsSync(STATE_FILE)) { 316 console.log('[mesh] No saved state found, starting fresh') 317 return false 318 } 319 320 try { 321 const data = JSON.parse(fs.readFileSync(STATE_FILE, 'utf8')) 322 const savedAt = new Date(data.savedAt) 323 const ageMinutes = (Date.now() - savedAt.getTime()) / 60000 324 325 console.log(`[mesh] 📂 Loading saved state from ${savedAt.toISOString()} (${ageMinutes.toFixed(0)} min ago)`) 326 327 // Restore principle candidates 328 if (data.principleCandidates) { 329 this.principleCandidates = data.principleCandidates 330 console.log(`[mesh] └─ ${this.principleCandidates.length} principle candidates`) 331 } 332 333 // Restore confirmed principles 334 if (data.confirmedPrinciples) { 335 this.confirmedPrinciples = data.confirmedPrinciples 336 console.log(`[mesh] └─ ${this.confirmedPrinciples.length} confirmed principles`) 337 } 338 339 // Restore aha moments (filter expired) 340 if (data.ahaMoments) { 341 const now = Date.now() 342 this.ahaMoments = data.ahaMoments.filter(m => 343 now - m.receivedAt < this.AHA_MOMENT_TTL 344 ) 345 console.log(`[mesh] └─ ${this.ahaMoments.length} aha moments (${data.ahaMoments.length - this.ahaMoments.length} expired)`) 346 } 347 348 // Restore convergence alerts (filter expired) 349 if (data.convergenceAlerts) { 350 const now = Date.now() 351 this.convergenceAlerts = data.convergenceAlerts.filter(a => 352 now - a.detectedAt < this.CONVERGENCE_TTL 353 ) 354 console.log(`[mesh] └─ ${this.convergenceAlerts.length} convergence alerts`) 355 } 356 357 // Restore recent messages 358 if (data.recentMessages) { 359 this.recentMessages = data.recentMessages 360 console.log(`[mesh] └─ ${this.recentMessages.length} recent messages`) 361 } 362 363 // Restore FO states (filter expired) 364 if (data.foStates) { 365 const now = Date.now() 366 for (const [nodeId, state] of data.foStates) { 367 if (now - state.receivedAt < this.FO_STATE_TTL) { 368 this.foStates.set(nodeId, state) 369 } 370 } 371 console.log(`[mesh] └─ ${this.foStates.size} FO states`) 372 } 373 374 return true 375 } catch (err) { 376 console.log(`[mesh] ⚠ Failed to load state: ${err.message}`) 377 return false 378 } 379 } 380 381 /** 382 * Periodic state save check. 383 */ 384 checkStateSave() { 385 const now = Date.now() 386 if (now - this.lastStateSave > this.STATE_SAVE_INTERVAL) { 387 this.saveState() 388 this.lastStateSave = now 389 } 390 } 391 392 async start() { 393 console.log('╔══════════════════════════════════════════════════════════╗') 394 console.log('║ SOVEREIGN MESH NODE ║') 395 console.log('║ The operator is the pilot. ║') 396 console.log('╚══════════════════════════════════════════════════════════╝') 397 console.log() 398 console.log(`[mesh] Instance: ${this.name}`) 399 400 // Load saved state from disk (persistence across restarts) 401 this.loadState() 402 403 // Load or derive keys 404 await this.loadKeys() 405 406 if (this.meshTopic) { 407 console.log(`[mesh] Topic: ${this.meshTopic.toString('hex').slice(0, 16)}...`) 408 } 409 410 // Start Hyperswarm 411 await this.startSwarm() 412 413 // Start Nostr listener 414 await this.startNostr() 415 416 // Start HTTP relay if requested 417 if (this.enableHttp) { 418 await this.startHttp() 419 } 420 421 // Start periodic state save (every 30 seconds) 422 this.stateSaveInterval = setInterval(() => { 423 this.checkStateSave() 424 }, 10000) // Check every 10s, actual save every 30s 425 426 console.log() 427 console.log('────────────────────────────────────────────────────────────') 428 console.log('Mesh node active. State persisted every 30s. Ctrl+C to stop.') 429 console.log('────────────────────────────────────────────────────────────') 430 } 431 432 async loadKeys() { 433 // If mnemonic provided, derive all keys from it 434 if (this.mnemonic) { 435 const derived = deriveKeysFromMnemonic(this.mnemonic) 436 this.sovereignKey = derived.sovereignKey 437 this.meshTopic = derived.meshTopic 438 this.nostrSecretKey = derived.nostrSecretKey 439 this.identifier = derived.identifier 440 441 // Mask mnemonic for display (first letter...last letter) 442 const firstLetter = derived.mnemonic[0] 443 const lastLetter = derived.mnemonic[derived.mnemonic.length - 1] 444 console.log(`[mesh] Operator: ${firstLetter}****${lastLetter}`) 445 console.log(`[mesh] Sovereign ID: ${this.identifier}`) 446 return 447 } 448 449 // Try to load mnemonic from vault or file 450 const savedMnemonic = await loadOrCreateMnemonic() 451 if (savedMnemonic) { 452 this.mnemonic = savedMnemonic 453 const derived = deriveKeysFromMnemonic(savedMnemonic) 454 this.sovereignKey = derived.sovereignKey 455 this.meshTopic = derived.meshTopic 456 this.nostrSecretKey = derived.nostrSecretKey 457 this.identifier = derived.identifier 458 459 // Mask mnemonic for display (first letter...last letter) 460 const firstLetter = derived.mnemonic[0] 461 const lastLetter = derived.mnemonic[derived.mnemonic.length - 1] 462 console.log(`[mesh] Operator: ${firstLetter}****${lastLetter}`) 463 console.log(`[mesh] Sovereign ID: ${this.identifier}`) 464 return 465 } 466 467 // Legacy: try to load from old key files 468 const KEY_FILE = path.join(CONFIG_DIR, 'broadcast-key.json') 469 const NOSTR_KEY_FILE = path.join(CONFIG_DIR, 'nostr-identity.json') 470 471 if (fs.existsSync(KEY_FILE) && fs.existsSync(NOSTR_KEY_FILE)) { 472 const keyData = JSON.parse(fs.readFileSync(KEY_FILE, 'utf8')) 473 this.sovereignKey = Buffer.from(keyData.key, 'hex') 474 this.identifier = keyData.identifier 475 476 const nostrData = JSON.parse(fs.readFileSync(NOSTR_KEY_FILE, 'utf8')) 477 this.nostrSecretKey = Buffer.from(nostrData.secretKey, 'hex') 478 479 // Use legacy topic 480 this.meshTopic = crypto.createHash('sha256').update('sovereign-os-cerf-2026').digest() 481 482 console.log(`[mesh] Legacy mode - Sovereign ID: ${this.identifier}`) 483 return 484 } 485 486 console.log('[mesh] No operator mnemonic provided') 487 console.log('[mesh] Run with --mnemonic "your-words" or use "init" to generate') 488 console.log('[mesh] Running in receive-only mode') 489 490 // Fallback topic for receive-only 491 this.meshTopic = crypto.createHash('sha256').update('sovereign-os-public').digest() 492 } 493 494 async startSwarm() { 495 this.swarm = new Hyperswarm() 496 497 this.swarm.on('connection', (conn, info) => { 498 const peerId = info.publicKey.toString('hex').slice(0, 8) 499 console.log(`[mesh] ✓ Peer connected: ${peerId}`) 500 this.peers.set(peerId, conn) 501 502 conn.on('data', (data) => this.onPeerMessage(peerId, data)) 503 conn.on('close', () => { 504 console.log(`[mesh] ✗ Peer disconnected: ${peerId}`) 505 this.peers.delete(peerId) 506 }) 507 conn.on('error', () => {}) 508 }) 509 510 const discovery = this.swarm.join(this.meshTopic, { client: true, server: true }) 511 await discovery.flushed() 512 console.log(`[mesh] Joined swarm, discovering peers...`) 513 } 514 515 async startNostr() { 516 if (!this.sovereignKey) return 517 518 console.log('[nostr] Connecting to relays...') 519 for (const url of RELAYS) { 520 try { 521 const relay = await Relay.connect(url) 522 this.nostrConnections.push(relay) 523 524 relay.subscribe([ 525 { kinds: [EVENT_KIND], authors: [this.nostrPublicKey], '#d': [this.identifier] } 526 ], { 527 onevent: (event) => this.onNostrEvent(event), 528 oneose: () => {} 529 }) 530 console.log(`[nostr] ✓ ${url}`) 531 } catch { 532 console.log(`[nostr] ✗ ${url}`) 533 } 534 } 535 } 536 537 async startHttp() { 538 this.httpServer = http.createServer((req, res) => this.handleHttp(req, res)) 539 this.httpServer.listen(HTTP_PORT, '0.0.0.0', () => { 540 console.log(`[http] Relay listening on :${HTTP_PORT}`) 541 console.log(`[http] iOS Shortcut: https://YOUR-TAILSCALE-HOST/publish`) 542 }) 543 } 544 545 // Handle incoming peer message 546 onPeerMessage(peerId, data) { 547 try { 548 const msg = JSON.parse(data.toString()) 549 const hash = crypto.createHash('sha256').update(data).digest('hex').slice(0, 16) 550 551 if (this.seen.has(hash)) return 552 this.seen.add(hash) 553 setTimeout(() => this.seen.delete(hash), this.SEEN_TTL) 554 555 console.log(`[mesh] ← ${msg.from || peerId}: ${msg.content?.slice(0, 50) || msg.type}`) 556 557 // Handle different message types 558 if (msg.type === 'fo-state' && msg.nodeId) { 559 // FO state updates 560 this.foStates.set(msg.nodeId, { 561 ...msg, 562 receivedAt: Date.now() 563 }) 564 } else if (msg.type === 'aha_moment' || (msg.payload && msg.payload.importance >= 0.7)) { 565 // Aha moments - high-importance insights get priority storage 566 this.storeAhaMoment(msg) 567 console.log(`[mesh] 🎯 Aha moment received from ${msg.from || peerId}`) 568 } else if (msg.type === 'principle_candidate') { 569 // Principle candidate - new principle propagation 570 this.storePrincipleCandidate(msg) 571 console.log(`[mesh] 📜 Principle candidate received: ${msg.name} from ${msg.from || peerId}`) 572 } else if (msg.type === 'principle_vote') { 573 // Vote on a principle candidate 574 this.votePrincipleCandidate(msg.name, msg.vote, msg.from, msg.reason) 575 console.log(`[mesh] 🗳️ Vote received: ${msg.vote} on ${msg.name} from ${msg.from || peerId}`) 576 } else { 577 // Store regular messages for context 578 this.storeMessage(msg) 579 } 580 581 // Forward to other peers (but not back to sender) 582 this.broadcast(msg, peerId) 583 } catch {} 584 } 585 586 // Handle incoming Nostr event 587 onNostrEvent(event) { 588 if (!this.sovereignKey) return 589 try { 590 const decrypted = decrypt(event.content, this.sovereignKey) 591 const msg = JSON.parse(decrypted) 592 593 const hash = crypto.createHash('sha256').update(decrypted).digest('hex').slice(0, 16) 594 if (this.seen.has(hash)) return 595 this.seen.add(hash) 596 setTimeout(() => this.seen.delete(hash), this.SEEN_TTL) 597 598 console.log(`[nostr] ← ${msg.source || 'phone'}: ${msg.content?.slice(0, 50) || msg.type}`) 599 600 // Handle different message types 601 if (msg.type === 'fo-state' && msg.nodeId) { 602 // FO state updates 603 this.foStates.set(msg.nodeId, { 604 ...msg, 605 receivedAt: Date.now() 606 }) 607 } else if (msg.type === 'aha_moment' || (msg.payload && msg.payload.importance >= 0.7)) { 608 // Aha moments - high-importance insights get priority storage 609 this.storeAhaMoment(msg) 610 console.log(`[nostr] 🎯 Aha moment received`) 611 } else if (msg.type === 'principle_candidate') { 612 // Principle candidate - new principle propagation 613 this.storePrincipleCandidate(msg) 614 console.log(`[nostr] 📜 Principle candidate received: ${msg.name}`) 615 } else if (msg.type === 'principle_vote') { 616 // Vote on a principle candidate 617 this.votePrincipleCandidate(msg.name, msg.vote, msg.from, msg.reason) 618 console.log(`[nostr] 🗳️ Vote received: ${msg.vote} on ${msg.name}`) 619 } else { 620 // Store regular messages for context 621 this.storeMessage(msg) 622 } 623 624 // Forward to all Hyperswarm peers 625 this.broadcast({ ...msg, via: 'nostr' }) 626 } catch {} 627 } 628 629 // Handle HTTP request 630 async handleHttp(req, res) { 631 res.setHeader('Access-Control-Allow-Origin', '*') 632 res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS') 633 634 if (req.method === 'OPTIONS') { 635 res.writeHead(204) 636 res.end() 637 return 638 } 639 640 const url = new URL(req.url, `http://${req.headers.host}`) 641 642 // Root endpoint - simple ping 643 if (url.pathname === '/') { 644 res.writeHead(200, { 'Content-Type': 'application/json' }) 645 res.end(JSON.stringify({ status: 'ok', node: this.name, peers: this.peers.size })) 646 return 647 } 648 649 // Health endpoint - comprehensive status (moved to end for proper implementation) 650 651 // ═══════════════════════════════════════════════════════════════════════════ 652 // CONTEXT ENDPOINTS (silence-led bootstrap) 653 // ═══════════════════════════════════════════════════════════════════════════ 654 655 // GET /context - Returns mesh state for Claude bootstrap 656 if (url.pathname === '/context' && req.method === 'GET') { 657 res.writeHead(200, { 'Content-Type': 'application/json' }) 658 res.end(JSON.stringify(this.getBootstrapContext())) 659 return 660 } 661 662 // POST /fo-state - First Officer publishes its state 663 if (url.pathname === '/fo-state' && req.method === 'POST') { 664 const body = await this.readBody(req) 665 try { 666 const state = JSON.parse(body) 667 const nodeId = state.nodeId || 'unknown' 668 669 // Detect convergence BEFORE storing (to compare with existing states) 670 this.detectConvergence(state) 671 672 // Store locally 673 this.foStates.set(nodeId, { 674 ...state, 675 receivedAt: Date.now() 676 }) 677 678 // Broadcast to mesh peers 679 this.broadcast({ 680 type: 'fo-state', 681 from: nodeId, 682 ...state 683 }) 684 685 console.log(`[http] ← FO state from ${nodeId}`) 686 687 res.writeHead(200, { 'Content-Type': 'application/json' }) 688 res.end(JSON.stringify({ ok: true, stored: nodeId })) 689 } catch (e) { 690 res.writeHead(400, { 'Content-Type': 'application/json' }) 691 res.end(JSON.stringify({ error: 'Invalid JSON' })) 692 } 693 return 694 } 695 696 // POST /aha-moment - High-importance insight broadcast 697 if (url.pathname === '/aha-moment' && req.method === 'POST') { 698 const body = await this.readBody(req) 699 try { 700 const data = JSON.parse(body) 701 const msg = { 702 type: 'aha_moment', 703 from: data.from || this.name, 704 content: data.content, 705 axioms: data.axioms || [], 706 importance: data.importance || 0.7, 707 energy_state: data.energy_state || 'kinetic', 708 session_id: data.session_id, 709 timestamp: new Date().toISOString() 710 } 711 712 console.log(`[http] 🎯 Aha moment from ${msg.from}: ${msg.content?.slice(0, 50)}`) 713 714 // Store as aha moment (priority storage) 715 this.storeAhaMoment(msg) 716 717 // Broadcast to mesh peers 718 this.broadcast(msg) 719 720 // Publish to Nostr 721 await this.publishNostr(msg) 722 723 res.writeHead(200, { 'Content-Type': 'application/json' }) 724 res.end(JSON.stringify({ ok: true, stored: true, peers: this.peers.size })) 725 } catch (e) { 726 res.writeHead(400, { 'Content-Type': 'application/json' }) 727 res.end(JSON.stringify({ error: 'Invalid JSON' })) 728 } 729 return 730 } 731 732 // POST /principle-candidate - New principle propagation 733 if (url.pathname === '/principle-candidate' && req.method === 'POST') { 734 const body = await this.readBody(req) 735 try { 736 const data = JSON.parse(body) 737 const msg = { 738 type: 'principle_candidate', 739 from: data.from || this.name, 740 name: data.name, // Short name (e.g., "A5-Ergodic-Asymmetry") 741 statement: data.statement, // The principle statement 742 rationale: data.rationale, // Why this principle matters 743 evidence: data.evidence || [], // Examples that support it 744 axiom_connections: data.axiom_connections || [], // Which axioms it relates to 745 divergence_score: data.divergence_score || 0.7, // D score that triggered it 746 proposed_by: data.proposed_by, // Session/instance that discovered it 747 status: 'candidate', // candidate -> testing -> confirmed/rejected 748 votes: { support: 0, reject: 0 }, // Cross-instance voting 749 timestamp: new Date().toISOString() 750 } 751 752 console.log(`[http] 📜 Principle candidate from ${msg.from}: ${msg.name}`) 753 754 // Store as principle candidate 755 this.storePrincipleCandidate(msg) 756 757 // Broadcast to mesh peers 758 this.broadcast(msg) 759 760 // Publish to Nostr (principles are important enough for wider broadcast) 761 await this.publishNostr(msg) 762 763 res.writeHead(200, { 'Content-Type': 'application/json' }) 764 res.end(JSON.stringify({ ok: true, stored: true, name: msg.name, peers: this.peers.size })) 765 } catch (e) { 766 res.writeHead(400, { 'Content-Type': 'application/json' }) 767 res.end(JSON.stringify({ error: 'Invalid JSON' })) 768 } 769 return 770 } 771 772 // POST /principle-vote - Vote on a principle candidate 773 if (url.pathname === '/principle-vote' && req.method === 'POST') { 774 const body = await this.readBody(req) 775 try { 776 const data = JSON.parse(body) 777 const { name, vote, from, reason } = data // vote: 'support' or 'reject' 778 779 if (!name || !vote || !['support', 'reject'].includes(vote)) { 780 res.writeHead(400, { 'Content-Type': 'application/json' }) 781 res.end(JSON.stringify({ error: 'Invalid vote (need name and vote: support/reject)' })) 782 return 783 } 784 785 const updated = this.votePrincipleCandidate(name, vote, from || this.name, reason) 786 787 if (updated) { 788 // Broadcast vote to other instances 789 this.broadcast({ 790 type: 'principle_vote', 791 name, 792 vote, 793 from: from || this.name, 794 reason, 795 timestamp: new Date().toISOString() 796 }) 797 798 console.log(`[http] 🗳️ Vote on ${name}: ${vote} from ${from || this.name}`) 799 res.writeHead(200, { 'Content-Type': 'application/json' }) 800 res.end(JSON.stringify({ ok: true, name, vote, updated: updated })) 801 } else { 802 res.writeHead(404, { 'Content-Type': 'application/json' }) 803 res.end(JSON.stringify({ error: 'Principle candidate not found' })) 804 } 805 } catch (e) { 806 res.writeHead(400, { 'Content-Type': 'application/json' }) 807 res.end(JSON.stringify({ error: 'Invalid JSON' })) 808 } 809 return 810 } 811 812 // GET /confirmed-principles - Get principles ready for CLAUDE.md 813 if (url.pathname === '/confirmed-principles' && req.method === 'GET') { 814 // Also check for time-based confirmations 815 this.checkTimeBasedConfirmations() 816 817 const unwritten = this.getUnwrittenConfirmedPrinciples() 818 res.writeHead(200, { 'Content-Type': 'application/json' }) 819 res.end(JSON.stringify({ 820 confirmed: unwritten, 821 count: unwritten.length, 822 total_confirmed: this.confirmedPrinciples.length 823 })) 824 return 825 } 826 827 // POST /mark-principle-written - Mark a principle as written to CLAUDE.md 828 if (url.pathname === '/mark-principle-written' && req.method === 'POST') { 829 const body = await this.readBody(req) 830 try { 831 const data = JSON.parse(body) 832 const { name } = data 833 834 if (!name) { 835 res.writeHead(400, { 'Content-Type': 'application/json' }) 836 res.end(JSON.stringify({ error: 'Need principle name' })) 837 return 838 } 839 840 this.markPrincipleWritten(name) 841 842 res.writeHead(200, { 'Content-Type': 'application/json' }) 843 res.end(JSON.stringify({ ok: true, name, marked: true })) 844 } catch (e) { 845 res.writeHead(400, { 'Content-Type': 'application/json' }) 846 res.end(JSON.stringify({ error: 'Invalid JSON' })) 847 } 848 return 849 } 850 851 // GET /graph-items - Returns mesh items formatted for knowledge graph ingestion 852 // Used by graph_feeder.py to create nodes from mesh data 853 if (url.pathname === '/graph-items' && req.method === 'GET') { 854 const items = this.getGraphItems() 855 res.writeHead(200, { 'Content-Type': 'application/json' }) 856 res.end(JSON.stringify(items)) 857 return 858 } 859 860 // POST /query - Cross-instance contextual queries 861 // Allows Claude instances to ask specific questions to the mesh 862 if (url.pathname === '/query' && req.method === 'POST') { 863 const body = await this.readBody(req) 864 try { 865 const query = JSON.parse(body) 866 const result = this.handleQuery(query) 867 res.writeHead(200, { 'Content-Type': 'application/json' }) 868 res.end(JSON.stringify(result)) 869 } catch (e) { 870 res.writeHead(400, { 'Content-Type': 'application/json' }) 871 res.end(JSON.stringify({ error: 'Invalid query', message: e.message })) 872 } 873 return 874 } 875 876 if (url.pathname === '/publish' && req.method === 'POST') { 877 const body = await this.readBody(req) 878 const msg = { 879 type: 'message', 880 from: 'phone', 881 content: body.trim(), 882 timestamp: new Date().toISOString() 883 } 884 885 console.log(`[http] ← phone: ${body.slice(0, 50)}`) 886 887 // Store for context 888 this.storeMessage(msg) 889 890 // Broadcast to mesh 891 this.broadcast(msg) 892 893 // Publish to Nostr 894 await this.publishNostr(msg) 895 896 res.writeHead(200, { 'Content-Type': 'application/json' }) 897 res.end(JSON.stringify({ ok: true, peers: this.peers.size })) 898 return 899 } 900 901 // GET /health - Comprehensive mesh health status 902 if (url.pathname === '/health' && req.method === 'GET') { 903 const health = this.getHealthStatus() 904 res.writeHead(200, { 'Content-Type': 'application/json' }) 905 res.end(JSON.stringify(health)) 906 return 907 } 908 909 // POST /broadcast - JSON broadcast to all mesh peers 910 // Used by: Phoenix mesh sync, attention daemon, graph sink 911 if (url.pathname === '/broadcast' && req.method === 'POST') { 912 const body = await this.readBody(req) 913 try { 914 const data = JSON.parse(body) 915 const msgType = data.type || 'broadcast' 916 917 const msg = { 918 type: msgType, 919 from: data.from || this.name, 920 payload: data.payload || data, 921 timestamp: new Date().toISOString() 922 } 923 924 console.log(`[http] broadcast: ${msgType} from ${msg.from}`) 925 926 // Store for context (Phoenix states, graph deltas, etc.) 927 this.storeMessage(msg) 928 929 // Broadcast to mesh peers 930 this.broadcast(msg) 931 932 // Handle specific message types 933 if (msgType === 'phoenix_state') { 934 this.handlePhoenixState(msg) 935 } else if (msgType === 'graph_delta') { 936 this.handleGraphDelta(msg) 937 } else if (msgType === 'major_resonance') { 938 this.handleMajorResonance(msg) 939 } 940 941 res.writeHead(200, { 'Content-Type': 'application/json' }) 942 res.end(JSON.stringify({ ok: true, type: msgType, peers: this.peers.size })) 943 } catch (e) { 944 console.error('[http] broadcast error:', e.message) 945 res.writeHead(400, { 'Content-Type': 'application/json' }) 946 res.end(JSON.stringify({ error: 'Invalid JSON', message: e.message })) 947 } 948 return 949 } 950 951 res.writeHead(404) 952 res.end('Not found') 953 } 954 955 readBody(req) { 956 return new Promise((resolve) => { 957 let body = '' 958 req.on('data', c => body += c) 959 req.on('end', () => resolve(body)) 960 }) 961 } 962 963 // Broadcast to all peers except excluded 964 broadcast(msg, excludePeer = null) { 965 const data = JSON.stringify({ ...msg, from: msg.from || this.name }) 966 for (const [peerId, conn] of this.peers) { 967 if (peerId !== excludePeer) { 968 try { conn.write(data) } catch {} 969 } 970 } 971 } 972 973 // Publish to Nostr 974 async publishNostr(msg) { 975 if (!this.sovereignKey || !this.nostrSecretKey) return 976 977 const encrypted = encrypt(JSON.stringify(msg), this.sovereignKey) 978 const event = finalizeEvent({ 979 kind: EVENT_KIND, 980 created_at: Math.floor(Date.now() / 1000), 981 tags: [['d', this.identifier]], 982 content: encrypted 983 }, this.nostrSecretKey) 984 985 for (const relay of this.nostrConnections) { 986 try { await relay.publish(event) } catch {} 987 } 988 } 989 990 // ═══════════════════════════════════════════════════════════════════════════ 991 // CONTEXT METHODS (for silence-led bootstrap) 992 // ═══════════════════════════════════════════════════════════════════════════ 993 994 /** 995 * Store a message for context retrieval. 996 * Called on every incoming message (peer, nostr, http). 997 */ 998 storeMessage(msg) { 999 this.recentMessages.push({ 1000 ...msg, 1001 receivedAt: Date.now() 1002 }) 1003 // Trim to max size 1004 while (this.recentMessages.length > this.MAX_RECENT_MESSAGES) { 1005 this.recentMessages.shift() 1006 } 1007 } 1008 1009 /** 1010 * Store an aha moment (high-importance insight). 1011 * These persist longer and get priority in bootstrap. 1012 */ 1013 storeAhaMoment(msg) { 1014 const payload = msg.payload || msg 1015 this.ahaMoments.push({ 1016 from: msg.from || 'unknown', 1017 content: payload.content || msg.content, 1018 type: payload.type || msg.type, 1019 axioms: payload.axioms || [], 1020 importance: payload.importance || 0.7, 1021 energy_state: payload.energy_state || 'kinetic', 1022 session_id: payload.session_id, 1023 receivedAt: Date.now() 1024 }) 1025 // Trim to max size 1026 while (this.ahaMoments.length > this.MAX_AHA_MOMENTS) { 1027 this.ahaMoments.shift() 1028 } 1029 } 1030 1031 /** 1032 * Get aha moments, filtering out expired ones. 1033 */ 1034 getAhaMoments(limit = 5) { 1035 const now = Date.now() 1036 // Filter expired and return most recent 1037 this.ahaMoments = this.ahaMoments.filter(m => now - m.receivedAt < this.AHA_MOMENT_TTL) 1038 return this.ahaMoments.slice(-limit).reverse() // Most recent first 1039 } 1040 1041 /** 1042 * Detect convergence when FO state is updated. 1043 * If multiple instances share gravity wells, generate alert. 1044 */ 1045 detectConvergence(newState) { 1046 const nodeId = newState.nodeId 1047 const newWells = newState.gravityWells || [] 1048 if (newWells.length === 0) return 1049 1050 const now = Date.now() 1051 1052 // Compare with other FO states 1053 for (const [otherId, otherState] of this.foStates) { 1054 if (otherId === nodeId) continue 1055 if (now - otherState.receivedAt > this.FO_STATE_TTL) continue 1056 1057 const otherWells = otherState.gravityWells || [] 1058 if (otherWells.length === 0) continue 1059 1060 // Find overlapping topics 1061 const overlap = newWells.filter(w => otherWells.includes(w)) 1062 1063 if (overlap.length >= 2) { 1064 // Significant convergence detected! 1065 const alertKey = [nodeId, otherId].sort().join(':') 1066 1067 // Check if we already have this alert 1068 const existingIndex = this.convergenceAlerts.findIndex( 1069 a => a.key === alertKey 1070 ) 1071 1072 const alert = { 1073 key: alertKey, 1074 instances: [nodeId, otherId], 1075 sharedTopics: overlap, 1076 strength: overlap.length / Math.max(newWells.length, otherWells.length), 1077 detectedAt: now 1078 } 1079 1080 if (existingIndex >= 0) { 1081 // Update existing alert 1082 this.convergenceAlerts[existingIndex] = alert 1083 } else { 1084 // New convergence detected 1085 this.convergenceAlerts.push(alert) 1086 console.log(`[mesh] 🔄 CONVERGENCE: ${nodeId} ↔ ${otherId} on [${overlap.join(', ')}]`) 1087 1088 // Trim to max 1089 while (this.convergenceAlerts.length > this.MAX_CONVERGENCE_ALERTS) { 1090 this.convergenceAlerts.shift() 1091 } 1092 } 1093 } 1094 } 1095 } 1096 1097 /** 1098 * Get active convergence alerts. 1099 */ 1100 getConvergenceAlerts() { 1101 const now = Date.now() 1102 this.convergenceAlerts = this.convergenceAlerts.filter( 1103 a => now - a.detectedAt < this.CONVERGENCE_TTL 1104 ) 1105 return this.convergenceAlerts 1106 } 1107 1108 /** 1109 * Store a principle candidate for cross-instance propagation. 1110 */ 1111 storePrincipleCandidate(msg) { 1112 const payload = msg.payload || msg 1113 1114 // Check if we already have this principle (by name) 1115 const existingIndex = this.principleCandidates.findIndex( 1116 p => p.name === payload.name 1117 ) 1118 1119 const candidate = { 1120 name: payload.name, 1121 statement: payload.statement, 1122 rationale: payload.rationale, 1123 evidence: payload.evidence || [], 1124 axiom_connections: payload.axiom_connections || [], 1125 divergence_score: payload.divergence_score || 0.7, 1126 proposed_by: payload.proposed_by || payload.from, 1127 from: payload.from || 'unknown', 1128 status: payload.status || 'candidate', 1129 votes: payload.votes || { support: 0, reject: 0 }, 1130 voters: payload.voters || [], // Track who voted to prevent double-voting 1131 receivedAt: Date.now(), 1132 timestamp: payload.timestamp || new Date().toISOString() 1133 } 1134 1135 if (existingIndex >= 0) { 1136 // Update existing - preserve votes but update other fields 1137 const existing = this.principleCandidates[existingIndex] 1138 candidate.votes = existing.votes 1139 candidate.voters = existing.voters 1140 this.principleCandidates[existingIndex] = candidate 1141 } else { 1142 // New principle candidate 1143 this.principleCandidates.push(candidate) 1144 1145 // Trim to max 1146 while (this.principleCandidates.length > this.MAX_PRINCIPLE_CANDIDATES) { 1147 this.principleCandidates.shift() 1148 } 1149 } 1150 } 1151 1152 /** 1153 * Vote on a principle candidate. 1154 * Returns the updated candidate or null if not found. 1155 */ 1156 votePrincipleCandidate(name, vote, from, reason) { 1157 const index = this.principleCandidates.findIndex(p => p.name === name) 1158 if (index < 0) return null 1159 1160 const candidate = this.principleCandidates[index] 1161 1162 // Prevent double-voting from same source 1163 if (candidate.voters.includes(from)) { 1164 console.log(`[mesh] Vote ignored: ${from} already voted on ${name}`) 1165 return candidate 1166 } 1167 1168 // Record vote 1169 candidate.voters.push(from) 1170 if (vote === 'support') { 1171 candidate.votes.support++ 1172 } else { 1173 candidate.votes.reject++ 1174 } 1175 1176 // Check if principle should transition status 1177 const totalVotes = candidate.votes.support + candidate.votes.reject 1178 const supportRatio = candidate.votes.support / totalVotes 1179 1180 if (totalVotes >= 3) { // Minimum 3 votes to change status 1181 if (supportRatio >= 0.7 && candidate.status === 'candidate') { 1182 candidate.status = 'testing' 1183 candidate.testingStartedAt = Date.now() 1184 console.log(`[mesh] 📜 ${name} → TESTING (${(supportRatio * 100).toFixed(0)}% support)`) 1185 } else if (supportRatio < 0.3) { 1186 candidate.status = 'rejected' 1187 console.log(`[mesh] 📜 ${name} → REJECTED (${(supportRatio * 100).toFixed(0)}% support)`) 1188 } 1189 } 1190 1191 // Check if principle should be CONFIRMED 1192 // Condition: 5+ votes with 80% support while in testing 1193 if (candidate.status === 'testing' && 1194 totalVotes >= this.CONFIRM_MIN_VOTES && 1195 supportRatio >= this.CONFIRM_SUPPORT_RATIO) { 1196 this.confirmPrinciple(candidate) 1197 } 1198 1199 this.principleCandidates[index] = candidate 1200 return candidate 1201 } 1202 1203 /** 1204 * Confirm a principle - move from testing to confirmed. 1205 */ 1206 confirmPrinciple(candidate) { 1207 const name = candidate.name 1208 1209 // Check if already confirmed 1210 if (this.confirmedPrinciples.find(p => p.name === name)) { 1211 return 1212 } 1213 1214 const confirmed = { 1215 ...candidate, 1216 status: 'confirmed', 1217 confirmedAt: Date.now(), 1218 writtenToClaude: false // Track if written to CLAUDE.md 1219 } 1220 1221 this.confirmedPrinciples.push(confirmed) 1222 console.log(`[mesh] ✅ ${name} → CONFIRMED!`) 1223 console.log(`[mesh] Ready to be written to CLAUDE.md`) 1224 1225 // Broadcast confirmation to mesh 1226 this.broadcast({ 1227 type: 'principle_confirmed', 1228 name: name, 1229 principle: confirmed, 1230 timestamp: new Date().toISOString() 1231 }) 1232 1233 // Trim confirmed list if needed 1234 while (this.confirmedPrinciples.length > this.MAX_CONFIRMED_PRINCIPLES) { 1235 this.confirmedPrinciples.shift() 1236 } 1237 1238 // Remove from candidates (or mark as confirmed there too) 1239 const index = this.principleCandidates.findIndex(p => p.name === name) 1240 if (index >= 0) { 1241 this.principleCandidates[index].status = 'confirmed' 1242 } 1243 } 1244 1245 /** 1246 * Check for time-based confirmations. 1247 * Principles in testing for 24+ hours with 70%+ support get confirmed. 1248 */ 1249 checkTimeBasedConfirmations() { 1250 const now = Date.now() 1251 const testingThreshold = this.CONFIRM_TESTING_HOURS * 60 * 60 * 1000 1252 1253 for (const candidate of this.principleCandidates) { 1254 if (candidate.status !== 'testing') continue 1255 if (!candidate.testingStartedAt) continue 1256 1257 const timeInTesting = now - candidate.testingStartedAt 1258 if (timeInTesting < testingThreshold) continue 1259 1260 const totalVotes = candidate.votes.support + candidate.votes.reject 1261 if (totalVotes < 3) continue // Need at least 3 votes 1262 1263 const supportRatio = candidate.votes.support / totalVotes 1264 if (supportRatio >= 0.7) { 1265 console.log(`[mesh] ⏰ Time-based confirmation: ${candidate.name}`) 1266 console.log(`[mesh] ${timeInTesting / 3600000}h in testing, ${(supportRatio * 100).toFixed(0)}% support`) 1267 this.confirmPrinciple(candidate) 1268 } 1269 } 1270 } 1271 1272 /** 1273 * Get confirmed principles that haven't been written to CLAUDE.md yet. 1274 */ 1275 getUnwrittenConfirmedPrinciples() { 1276 return this.confirmedPrinciples.filter(p => !p.writtenToClaude) 1277 } 1278 1279 /** 1280 * Mark a principle as written to CLAUDE.md. 1281 */ 1282 markPrincipleWritten(name) { 1283 const principle = this.confirmedPrinciples.find(p => p.name === name) 1284 if (principle) { 1285 principle.writtenToClaude = true 1286 principle.writtenAt = Date.now() 1287 console.log(`[mesh] ✍️ ${name} marked as written to CLAUDE.md`) 1288 } 1289 } 1290 1291 /** 1292 * Get principle candidates, filtering out expired ones. 1293 */ 1294 getPrincipleCandidates(limit = 5) { 1295 const now = Date.now() 1296 // Filter expired 1297 this.principleCandidates = this.principleCandidates.filter( 1298 p => now - p.receivedAt < this.PRINCIPLE_CANDIDATE_TTL 1299 ) 1300 // Return most recent first, prioritize 'testing' status 1301 return this.principleCandidates 1302 .sort((a, b) => { 1303 // Testing > candidate > rejected 1304 const statusOrder = { testing: 0, candidate: 1, rejected: 2 } 1305 const statusDiff = (statusOrder[a.status] || 1) - (statusOrder[b.status] || 1) 1306 if (statusDiff !== 0) return statusDiff 1307 return b.receivedAt - a.receivedAt 1308 }) 1309 .slice(0, limit) 1310 } 1311 1312 /** 1313 * Get recent messages for context bootstrap. 1314 */ 1315 getRecentMessages(limit = 10) { 1316 return this.recentMessages 1317 .slice(-limit) 1318 .map(m => ({ 1319 from: m.from, 1320 content: m.content, 1321 type: m.type, 1322 timestamp: m.timestamp 1323 })) 1324 } 1325 1326 /** 1327 * Get recent insights from all First Officer states. 1328 */ 1329 getRecentInsights() { 1330 const insights = [] 1331 const now = Date.now() 1332 1333 for (const [nodeId, state] of this.foStates) { 1334 // Skip expired states 1335 if (now - state.receivedAt > this.FO_STATE_TTL) { 1336 this.foStates.delete(nodeId) 1337 continue 1338 } 1339 1340 // Collect insights from this FO 1341 if (state.recentInsights) { 1342 for (const insight of state.recentInsights) { 1343 insights.push({ 1344 from: nodeId, 1345 ...insight 1346 }) 1347 } 1348 } 1349 } 1350 1351 return insights.slice(-10) // Last 10 insights across all instances 1352 } 1353 1354 /** 1355 * Detect hot topics - topics mentioned by multiple instances. 1356 */ 1357 detectHotTopics() { 1358 const topicCounts = new Map() 1359 const now = Date.now() 1360 1361 // Count topics from FO states 1362 for (const [nodeId, state] of this.foStates) { 1363 if (now - state.receivedAt > this.FO_STATE_TTL) continue 1364 1365 if (state.gravityWells) { 1366 for (const topic of state.gravityWells) { 1367 const count = topicCounts.get(topic) || 0 1368 topicCounts.set(topic, count + 1) 1369 } 1370 } 1371 } 1372 1373 // Also count from recent messages 1374 for (const msg of this.recentMessages.slice(-20)) { 1375 if (msg.content) { 1376 // Simple topic extraction: look for key terms 1377 const words = msg.content.toLowerCase().split(/\s+/) 1378 for (const word of words) { 1379 if (word.length > 4) { // Skip short words 1380 const count = topicCounts.get(word) || 0 1381 topicCounts.set(word, count + 0.5) // Weight messages less 1382 } 1383 } 1384 } 1385 } 1386 1387 // Return topics mentioned by 2+ sources 1388 return Array.from(topicCounts.entries()) 1389 .filter(([_, count]) => count >= 2) 1390 .sort((a, b) => b[1] - a[1]) 1391 .slice(0, 5) 1392 .map(([topic]) => topic) 1393 } 1394 1395 /** 1396 * Get full context for bootstrap. 1397 */ 1398 getBootstrapContext() { 1399 return { 1400 // Network state 1401 peers: Array.from(this.peers.keys()), 1402 peerCount: this.peers.size, 1403 1404 // AHA MOMENTS - Priority placement (high-importance insights) 1405 ahaMoments: this.getAhaMoments(5), 1406 1407 // CONVERGENCE ALERTS - Instances working on similar topics 1408 convergenceAlerts: this.getConvergenceAlerts(), 1409 1410 // PRINCIPLE CANDIDATES - New principles being tested across mesh 1411 principleCandidates: this.getPrincipleCandidates(5), 1412 1413 // CONFIRMED PRINCIPLES - Ready to be written to CLAUDE.md 1414 confirmedPrinciples: this.getUnwrittenConfirmedPrinciples(), 1415 1416 // Recent messages 1417 recentMessages: this.getRecentMessages(10), 1418 1419 // Cross-instance insights 1420 insights: this.getRecentInsights(), 1421 1422 // Hot topics 1423 hotTopics: this.detectHotTopics(), 1424 1425 // All FO states (for full context) 1426 foStates: Array.from(this.foStates.entries()).map(([nodeId, state]) => ({ 1427 nodeId, 1428 sessionId: state.sessionId, 1429 gravityWells: state.gravityWells, 1430 axiomActivity: state.axiomActivity, 1431 energyState: state.energyState, 1432 timestamp: state.timestamp 1433 })), 1434 1435 // My identity 1436 nodeId: this.name, 1437 meshTopic: this.meshTopic?.toString('hex').slice(0, 8), 1438 1439 timestamp: new Date().toISOString() 1440 } 1441 } 1442 1443 /** 1444 * Get mesh items formatted for knowledge graph ingestion. 1445 * Used by graph_feeder.py to create nodes from mesh data. 1446 */ 1447 getGraphItems() { 1448 const items = { 1449 nodes: [], 1450 edges: [], 1451 timestamp: new Date().toISOString() 1452 } 1453 1454 // 1. Aha moments → insight nodes (high importance) 1455 for (const aha of this.ahaMoments) { 1456 const nodeId = `MESH_AHA_${this.hashString(aha.content + aha.timestamp)}` 1457 items.nodes.push({ 1458 id: nodeId, 1459 node_type: 'insight', 1460 label: aha.content?.slice(0, 30) + '...', 1461 content: aha.content, 1462 axioms: aha.axioms || [], 1463 importance: aha.importance || 0.7, 1464 source: 'mesh_aha', 1465 created_at: aha.timestamp, 1466 metadata: { 1467 from: aha.from, 1468 energy_state: aha.energy_state, 1469 session_id: aha.session_id 1470 } 1471 }) 1472 1473 // Create edges to axioms 1474 for (const axiom of (aha.axioms || [])) { 1475 items.edges.push({ 1476 source_id: nodeId, 1477 target_id: axiom, 1478 edge_type: 'relates_to', 1479 strength: aha.importance || 0.7 1480 }) 1481 } 1482 } 1483 1484 // 2. Confirmed principles → derived_principle nodes (very high importance) 1485 for (const principle of this.confirmedPrinciples) { 1486 const nodeId = `MESH_PRINCIPLE_${this.hashString(principle.name)}` 1487 items.nodes.push({ 1488 id: nodeId, 1489 node_type: 'derived_principle', 1490 label: principle.name, 1491 content: principle.statement, 1492 axioms: principle.axiom_connections || [], 1493 importance: 0.9, // Confirmed principles are high value 1494 source: 'mesh_principle', 1495 created_at: principle.confirmedAt ? new Date(principle.confirmedAt).toISOString() : principle.timestamp, 1496 metadata: { 1497 proposed_by: principle.proposed_by, 1498 votes: principle.votes, 1499 status: 'confirmed', 1500 written_to_claude: principle.writtenToClaude 1501 } 1502 }) 1503 1504 // Create edges to axioms 1505 for (const axiom of (principle.axiom_connections || [])) { 1506 items.edges.push({ 1507 source_id: nodeId, 1508 target_id: axiom, 1509 edge_type: 'derived_from', 1510 strength: 0.9 1511 }) 1512 } 1513 } 1514 1515 // 3. Principle candidates in testing → candidate_principle nodes 1516 const testingPrinciples = this.principleCandidates.filter(p => p.status === 'testing') 1517 for (const principle of testingPrinciples) { 1518 const nodeId = `MESH_CANDIDATE_${this.hashString(principle.name)}` 1519 items.nodes.push({ 1520 id: nodeId, 1521 node_type: 'principle_candidate', 1522 label: `[TESTING] ${principle.name}`, 1523 content: principle.statement, 1524 axioms: principle.axiom_connections || [], 1525 importance: 0.75, // Testing principles are valuable but not confirmed 1526 source: 'mesh_candidate', 1527 created_at: principle.timestamp, 1528 metadata: { 1529 proposed_by: principle.proposed_by, 1530 votes: principle.votes, 1531 status: 'testing', 1532 divergence_score: principle.divergence_score 1533 } 1534 }) 1535 1536 // Create edges to axioms 1537 for (const axiom of (principle.axiom_connections || [])) { 1538 items.edges.push({ 1539 source_id: nodeId, 1540 target_id: axiom, 1541 edge_type: 'relates_to', 1542 strength: 0.75 1543 }) 1544 } 1545 } 1546 1547 // 4. Convergence alerts → concept nodes (shared topics) 1548 for (const alert of this.convergenceAlerts) { 1549 for (const topic of (alert.sharedTopics || [])) { 1550 const nodeId = `MESH_TOPIC_${this.hashString(topic)}` 1551 // Only add if not already present 1552 if (!items.nodes.find(n => n.id === nodeId)) { 1553 items.nodes.push({ 1554 id: nodeId, 1555 node_type: 'concept', 1556 label: topic, 1557 content: `Cross-instance topic: ${topic} (strength=${alert.strength})`, 1558 axioms: [], 1559 importance: 0.6, 1560 source: 'mesh_convergence', 1561 created_at: new Date(alert.detectedAt).toISOString(), 1562 metadata: { 1563 instances: alert.instances, 1564 strength: alert.strength 1565 } 1566 }) 1567 } 1568 } 1569 } 1570 1571 return items 1572 } 1573 1574 /** 1575 * Get comprehensive mesh health status. 1576 */ 1577 getHealthStatus() { 1578 const now = Date.now() 1579 const uptime = now - this.startTime 1580 1581 // Calculate health score (0-100) 1582 let score = 100 1583 const issues = [] 1584 1585 // Peer connectivity check 1586 if (this.peers.size === 0) { 1587 score -= 20 1588 issues.push({ severity: 'warning', message: 'No mesh peers connected' }) 1589 } 1590 1591 // Nostr relay check 1592 const connectedRelays = this.nostrRelays?.filter(r => r.connected)?.length || 0 1593 if (connectedRelays === 0) { 1594 score -= 15 1595 issues.push({ severity: 'warning', message: 'No Nostr relays connected' }) 1596 } else if (connectedRelays < 2) { 1597 score -= 5 1598 issues.push({ severity: 'info', message: `Only ${connectedRelays} Nostr relay connected` }) 1599 } 1600 1601 // Message activity check (last 5 minutes) 1602 const recentMessages = this.recentMessages.filter(m => 1603 now - new Date(m.timestamp).getTime() < 5 * 60 * 1000 1604 ) 1605 if (recentMessages.length === 0 && uptime > 5 * 60 * 1000) { 1606 score -= 10 1607 issues.push({ severity: 'info', message: 'No message activity in last 5 minutes' }) 1608 } 1609 1610 // State persistence check 1611 const lastSaveAge = now - this.lastStateSave 1612 if (lastSaveAge > 2 * this.STATE_SAVE_INTERVAL) { 1613 score -= 10 1614 issues.push({ severity: 'warning', message: 'State not saved recently' }) 1615 } 1616 1617 // Determine overall status 1618 let status = 'healthy' 1619 if (score < 70) status = 'degraded' 1620 if (score < 50) status = 'unhealthy' 1621 1622 return { 1623 status, 1624 score, 1625 uptime_seconds: Math.floor(uptime / 1000), 1626 uptime_human: this.formatUptime(uptime), 1627 1628 network: { 1629 peers: this.peers.size, 1630 peer_ids: Array.from(this.peers.keys()).slice(0, 10), 1631 nostr_relays: connectedRelays, 1632 mesh_topic: this.meshTopic?.toString('hex').slice(0, 16) 1633 }, 1634 1635 storage: { 1636 aha_moments: this.ahaMoments.length, 1637 principle_candidates: this.principleCandidates.length, 1638 confirmed_principles: this.confirmedPrinciples.length, 1639 convergence_alerts: this.convergenceAlerts.length, 1640 recent_messages: this.recentMessages.length, 1641 fo_states: this.foStates.size, 1642 last_state_save: new Date(this.lastStateSave).toISOString() 1643 }, 1644 1645 activity: { 1646 messages_last_5min: recentMessages.length, 1647 messages_last_hour: this.recentMessages.filter(m => 1648 now - new Date(m.timestamp).getTime() < 60 * 60 * 1000 1649 ).length 1650 }, 1651 1652 issues, 1653 timestamp: new Date().toISOString() 1654 } 1655 } 1656 1657 /** 1658 * Format uptime as human-readable string. 1659 */ 1660 formatUptime(ms) { 1661 const seconds = Math.floor(ms / 1000) 1662 const minutes = Math.floor(seconds / 60) 1663 const hours = Math.floor(minutes / 60) 1664 const days = Math.floor(hours / 24) 1665 1666 if (days > 0) return `${days}d ${hours % 24}h ${minutes % 60}m` 1667 if (hours > 0) return `${hours}h ${minutes % 60}m` 1668 if (minutes > 0) return `${minutes}m ${seconds % 60}s` 1669 return `${seconds}s` 1670 } 1671 1672 /** 1673 * Handle incoming Phoenix state from mesh. 1674 * 1675 * Stores for cross-instance resurrection availability. 1676 */ 1677 handlePhoenixState(msg) { 1678 try { 1679 const payload = msg.payload || {} 1680 const from = msg.from || 'unknown' 1681 1682 // Don't store our own states (we have them locally) 1683 if (from === this.name) return 1684 1685 // Store in phoenix-received list 1686 if (!this.phoenixReceived) this.phoenixReceived = [] 1687 1688 this.phoenixReceived.push({ 1689 ...payload, 1690 receivedFrom: from, 1691 receivedAt: new Date().toISOString() 1692 }) 1693 1694 // Trim to max 20 states 1695 while (this.phoenixReceived.length > 20) { 1696 this.phoenixReceived.shift() 1697 } 1698 1699 console.log(`[mesh] Phoenix state received from ${from}: ${payload.session_id || 'unknown'}`) 1700 } catch (e) { 1701 console.error('[mesh] Phoenix state error:', e.message) 1702 } 1703 } 1704 1705 /** 1706 * Handle incoming graph delta from mesh. 1707 * 1708 * Can be used to merge remote graph changes into local graph. 1709 */ 1710 handleGraphDelta(msg) { 1711 try { 1712 const payload = msg.payload || {} 1713 const delta = payload.delta || payload 1714 const from = msg.from || 'unknown' 1715 1716 // Don't process our own deltas 1717 if (from === this.name) return 1718 1719 // Store for later processing (graph feeder can merge these) 1720 if (!this.graphDeltas) this.graphDeltas = [] 1721 1722 this.graphDeltas.push({ 1723 nodes: delta.nodes || [], 1724 edges: delta.edges || [], 1725 receivedFrom: from, 1726 receivedAt: new Date().toISOString() 1727 }) 1728 1729 // Trim to max 50 deltas 1730 while (this.graphDeltas.length > 50) { 1731 this.graphDeltas.shift() 1732 } 1733 1734 const nodeCount = (delta.nodes || []).length 1735 const edgeCount = (delta.edges || []).length 1736 if (nodeCount > 0 || edgeCount > 0) { 1737 console.log(`[mesh] Graph delta from ${from}: +${nodeCount} nodes, +${edgeCount} edges`) 1738 } 1739 } catch (e) { 1740 console.error('[mesh] Graph delta error:', e.message) 1741 } 1742 } 1743 1744 /** 1745 * Handle major resonance from mesh. 1746 * 1747 * Major resonances are high-energy insights that should be surfaced 1748 * across all Claude sessions. Calibrated for 2-5 per day. 1749 */ 1750 handleMajorResonance(msg) { 1751 try { 1752 const payload = msg.payload || {} 1753 const from = msg.from || 'unknown' 1754 1755 // Don't process our own resonances 1756 if (from === this.name) return 1757 1758 // Store for display to all Claude sessions 1759 if (!this.majorResonances) this.majorResonances = [] 1760 1761 const resonance = { 1762 time: payload.time || new Date().toISOString(), 1763 type: payload.resonance_type || 'unknown', 1764 preview: payload.preview || '', 1765 score: payload.score || 0, 1766 reason: payload.reason || '', 1767 from: from, 1768 receivedAt: new Date().toISOString() 1769 } 1770 1771 this.majorResonances.push(resonance) 1772 1773 // Keep last 20 major resonances 1774 while (this.majorResonances.length > 20) { 1775 this.majorResonances.shift() 1776 } 1777 1778 // Log prominently - this is important 1779 console.log('') 1780 console.log(`[mesh] ════════════════════════════════════════════════════════`) 1781 console.log(`[mesh] ◆ MAJOR RESONANCE from ${from}`) 1782 console.log(`[mesh] Type: ${resonance.type} | Score: ${(resonance.score * 100).toFixed(0)}%`) 1783 console.log(`[mesh] ${resonance.preview}`) 1784 console.log(`[mesh] ════════════════════════════════════════════════════════`) 1785 console.log('') 1786 } catch (e) { 1787 console.error('[mesh] Major resonance error:', e.message) 1788 } 1789 } 1790 1791 /** 1792 * Handle cross-instance queries. 1793 * 1794 * Query types: 1795 * - topic_search: Find items mentioning specific topics 1796 * - instance_context: Get current focus of a specific instance 1797 * - insight_search: Search insights by keyword 1798 * - principle_status: Get status of a specific principle candidate 1799 * - recent_activity: Get recent mesh activity 1800 */ 1801 handleQuery(query) { 1802 const { type, ...params } = query 1803 1804 switch (type) { 1805 case 'topic_search': 1806 return this.queryTopicSearch(params) 1807 case 'instance_context': 1808 return this.queryInstanceContext(params) 1809 case 'insight_search': 1810 return this.queryInsightSearch(params) 1811 case 'principle_status': 1812 return this.queryPrincipleStatus(params) 1813 case 'recent_activity': 1814 return this.queryRecentActivity(params) 1815 default: 1816 return { error: `Unknown query type: ${type}`, supported: ['topic_search', 'instance_context', 'insight_search', 'principle_status', 'recent_activity'] } 1817 } 1818 } 1819 1820 /** 1821 * Search for mesh items by topic keywords. 1822 */ 1823 queryTopicSearch({ topics, limit = 10 }) { 1824 if (!topics || !Array.isArray(topics)) { 1825 return { error: 'topics must be an array of keywords' } 1826 } 1827 1828 const results = { 1829 query: { type: 'topic_search', topics }, 1830 aha_moments: [], 1831 principles: [], 1832 fo_states: [], 1833 messages: [], 1834 timestamp: new Date().toISOString() 1835 } 1836 1837 const topicLower = topics.map(t => t.toLowerCase()) 1838 1839 // Search aha moments 1840 for (const aha of this.ahaMoments) { 1841 const content = (aha.content || '').toLowerCase() 1842 if (topicLower.some(t => content.includes(t))) { 1843 results.aha_moments.push({ 1844 content: aha.content, 1845 from: aha.from, 1846 axioms: aha.axioms, 1847 importance: aha.importance, 1848 timestamp: aha.timestamp 1849 }) 1850 } 1851 } 1852 1853 // Search principle candidates 1854 for (const p of this.principleCandidates) { 1855 const text = `${p.name} ${p.statement} ${p.rationale || ''}`.toLowerCase() 1856 if (topicLower.some(t => text.includes(t))) { 1857 results.principles.push({ 1858 name: p.name, 1859 statement: p.statement, 1860 status: p.status, 1861 votes: p.votes, 1862 proposed_by: p.proposed_by 1863 }) 1864 } 1865 } 1866 1867 // Search FO states for gravity wells 1868 for (const [nodeId, state] of this.foStates) { 1869 const wells = state.gravityWells || [] 1870 if (wells.some(w => topicLower.some(t => w.toLowerCase().includes(t)))) { 1871 results.fo_states.push({ 1872 nodeId, 1873 gravityWells: wells, 1874 axiomActivity: state.axiomActivity, 1875 timestamp: state.timestamp 1876 }) 1877 } 1878 } 1879 1880 // Search recent messages 1881 for (const msg of this.recentMessages.slice(-50)) { 1882 const content = (msg.content || '').toLowerCase() 1883 if (topicLower.some(t => content.includes(t))) { 1884 results.messages.push({ 1885 content: msg.content?.slice(0, 200), 1886 from: msg.from, 1887 timestamp: msg.timestamp 1888 }) 1889 } 1890 } 1891 1892 // Limit results 1893 results.aha_moments = results.aha_moments.slice(0, limit) 1894 results.principles = results.principles.slice(0, limit) 1895 results.fo_states = results.fo_states.slice(0, limit) 1896 results.messages = results.messages.slice(0, limit) 1897 1898 results.total = results.aha_moments.length + results.principles.length + 1899 results.fo_states.length + results.messages.length 1900 1901 return results 1902 } 1903 1904 /** 1905 * Get context from a specific instance. 1906 */ 1907 queryInstanceContext({ instance }) { 1908 if (!instance) { 1909 // Return all instance contexts 1910 const contexts = [] 1911 for (const [nodeId, state] of this.foStates) { 1912 contexts.push({ 1913 nodeId, 1914 sessionId: state.sessionId, 1915 gravityWells: state.gravityWells, 1916 axiomActivity: state.axiomActivity, 1917 energyState: state.energyState, 1918 timestamp: state.timestamp 1919 }) 1920 } 1921 return { 1922 query: { type: 'instance_context', instance: 'all' }, 1923 instances: contexts, 1924 total: contexts.length, 1925 timestamp: new Date().toISOString() 1926 } 1927 } 1928 1929 // Find specific instance (fuzzy match) 1930 for (const [nodeId, state] of this.foStates) { 1931 if (nodeId.toLowerCase().includes(instance.toLowerCase())) { 1932 // Get aha moments from this instance 1933 const instanceAhas = this.ahaMoments.filter(a => 1934 a.from?.toLowerCase().includes(instance.toLowerCase()) 1935 ) 1936 1937 return { 1938 query: { type: 'instance_context', instance }, 1939 found: true, 1940 nodeId, 1941 context: { 1942 sessionId: state.sessionId, 1943 gravityWells: state.gravityWells, 1944 axiomActivity: state.axiomActivity, 1945 energyState: state.energyState, 1946 recentInsights: state.recentInsights, 1947 timestamp: state.timestamp 1948 }, 1949 aha_moments: instanceAhas.map(a => ({ 1950 content: a.content, 1951 axioms: a.axioms, 1952 importance: a.importance, 1953 timestamp: a.timestamp 1954 })), 1955 timestamp: new Date().toISOString() 1956 } 1957 } 1958 } 1959 1960 return { 1961 query: { type: 'instance_context', instance }, 1962 found: false, 1963 available_instances: Array.from(this.foStates.keys()), 1964 timestamp: new Date().toISOString() 1965 } 1966 } 1967 1968 /** 1969 * Search insights (aha moments) by keyword. 1970 */ 1971 queryInsightSearch({ keyword, axiom, minImportance = 0 }) { 1972 let results = [...this.ahaMoments] 1973 1974 // Filter by keyword 1975 if (keyword) { 1976 const kw = keyword.toLowerCase() 1977 results = results.filter(a => (a.content || '').toLowerCase().includes(kw)) 1978 } 1979 1980 // Filter by axiom 1981 if (axiom) { 1982 results = results.filter(a => (a.axioms || []).includes(axiom)) 1983 } 1984 1985 // Filter by importance 1986 if (minImportance > 0) { 1987 results = results.filter(a => (a.importance || 0) >= minImportance) 1988 } 1989 1990 return { 1991 query: { type: 'insight_search', keyword, axiom, minImportance }, 1992 insights: results.map(a => ({ 1993 content: a.content, 1994 from: a.from, 1995 axioms: a.axioms, 1996 importance: a.importance, 1997 energy_state: a.energy_state, 1998 timestamp: a.timestamp 1999 })), 2000 total: results.length, 2001 timestamp: new Date().toISOString() 2002 } 2003 } 2004 2005 /** 2006 * Get status of principle candidates. 2007 */ 2008 queryPrincipleStatus({ name, status }) { 2009 let results = [...this.principleCandidates, ...this.confirmedPrinciples] 2010 2011 // Filter by name 2012 if (name) { 2013 const nameLower = name.toLowerCase() 2014 results = results.filter(p => p.name.toLowerCase().includes(nameLower)) 2015 } 2016 2017 // Filter by status 2018 if (status) { 2019 results = results.filter(p => p.status === status) 2020 } 2021 2022 return { 2023 query: { type: 'principle_status', name, status }, 2024 principles: results.map(p => ({ 2025 name: p.name, 2026 statement: p.statement, 2027 status: p.status, 2028 votes: p.votes, 2029 axiom_connections: p.axiom_connections, 2030 proposed_by: p.proposed_by, 2031 writtenToClaude: p.writtenToClaude, 2032 timestamp: p.timestamp 2033 })), 2034 total: results.length, 2035 timestamp: new Date().toISOString() 2036 } 2037 } 2038 2039 /** 2040 * Get recent mesh activity. 2041 */ 2042 queryRecentActivity({ limit = 20, type: activityType }) { 2043 const activity = { 2044 query: { type: 'recent_activity', limit, activityType }, 2045 items: [], 2046 timestamp: new Date().toISOString() 2047 } 2048 2049 // Collect all recent items with timestamps 2050 const items = [] 2051 2052 // Aha moments 2053 if (!activityType || activityType === 'aha') { 2054 for (const a of this.ahaMoments) { 2055 items.push({ 2056 type: 'aha_moment', 2057 content: a.content?.slice(0, 100), 2058 from: a.from, 2059 axioms: a.axioms, 2060 timestamp: a.timestamp || new Date(a.receivedAt).toISOString() 2061 }) 2062 } 2063 } 2064 2065 // Principle candidates 2066 if (!activityType || activityType === 'principle') { 2067 for (const p of [...this.principleCandidates, ...this.confirmedPrinciples]) { 2068 items.push({ 2069 type: 'principle', 2070 content: `${p.name}: ${p.statement?.slice(0, 80)}`, 2071 status: p.status, 2072 votes: p.votes, 2073 timestamp: p.timestamp 2074 }) 2075 } 2076 } 2077 2078 // Convergence alerts 2079 if (!activityType || activityType === 'convergence') { 2080 for (const c of this.convergenceAlerts) { 2081 items.push({ 2082 type: 'convergence', 2083 content: `${c.instances.join(' ↔ ')}: ${c.sharedTopics.join(', ')}`, 2084 strength: c.strength, 2085 timestamp: new Date(c.detectedAt).toISOString() 2086 }) 2087 } 2088 } 2089 2090 // Recent messages 2091 if (!activityType || activityType === 'message') { 2092 for (const m of this.recentMessages.slice(-20)) { 2093 items.push({ 2094 type: 'message', 2095 content: m.content?.slice(0, 100), 2096 from: m.from, 2097 timestamp: m.timestamp 2098 }) 2099 } 2100 } 2101 2102 // Sort by timestamp (newest first) and limit 2103 items.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp)) 2104 activity.items = items.slice(0, limit) 2105 activity.total = items.length 2106 2107 return activity 2108 } 2109 2110 /** 2111 * Simple hash function for generating node IDs. 2112 */ 2113 hashString(str) { 2114 let hash = 0 2115 for (let i = 0; i < str.length; i++) { 2116 const char = str.charCodeAt(i) 2117 hash = ((hash << 5) - hash) + char 2118 hash = hash & hash // Convert to 32-bit integer 2119 } 2120 return Math.abs(hash).toString(16).slice(0, 12) 2121 } 2122 2123 async stop() { 2124 console.log('\n[mesh] Shutting down...') 2125 2126 // Stop periodic save interval 2127 if (this.stateSaveInterval) { 2128 clearInterval(this.stateSaveInterval) 2129 } 2130 2131 // Save state before exit 2132 console.log('[mesh] Saving state before exit...') 2133 this.saveState() 2134 2135 if (this.httpServer) this.httpServer.close() 2136 for (const relay of this.nostrConnections) relay.close() 2137 if (this.swarm) await this.swarm.destroy() 2138 process.exit(0) 2139 } 2140 } 2141 2142 // ═══════════════════════════════════════════════════════════════════════════ 2143 // CLI 2144 // ═══════════════════════════════════════════════════════════════════════════ 2145 const args = process.argv.slice(2) 2146 const options = { 2147 name: os.hostname(), 2148 http: false, 2149 mnemonic: null 2150 } 2151 2152 // Handle 'init' command first 2153 if (args[0] === 'init') { 2154 const mnemonic = generateMnemonic() 2155 fs.mkdirSync(CONFIG_DIR, { recursive: true }) 2156 fs.writeFileSync(MNEMONIC_FILE, JSON.stringify({ 2157 mnemonic, 2158 created: new Date().toISOString(), 2159 security: { 2160 words: MNEMONIC_WORDS, 2161 entropy_bits: MNEMONIC_WORDS * 11, 2162 pbkdf2_iterations: PBKDF2_ITERATIONS 2163 } 2164 }, null, 2)) 2165 2166 console.log(` 2167 ╔══════════════════════════════════════════════════════════════════════════╗ 2168 ║ YOUR OPERATOR MNEMONIC ║ 2169 ║ (BIP-39 / ${MNEMONIC_WORDS} words / ${MNEMONIC_WORDS * 11}-bit entropy) ║ 2170 ╚══════════════════════════════════════════════════════════════════════════╝ 2171 2172 ${mnemonic} 2173 2174 This is your sovereign identity. Memorize it. 2175 2176 Security: 2177 • ${MNEMONIC_WORDS} BIP-39 words = ${Math.pow(2048, MNEMONIC_WORDS).toExponential(2)} combinations 2178 • PBKDF2-HMAC-SHA256 with ${PBKDF2_ITERATIONS} iterations 2179 • Keys derived: sovereign (AES-256), mesh topic, Nostr identity 2180 2181 Any machine with this mnemonic joins YOUR mesh automatically. 2182 You are the pilot. This is your license number. 2183 2184 Saved to: ${MNEMONIC_FILE} 2185 2186 Next steps: 2187 # Start mesh on this machine 2188 node sovereign-mesh.js --http --name "${os.hostname()}" 2189 2190 # Join from another machine 2191 node sovereign-mesh.js --mnemonic "${mnemonic}" --name "other-machine" 2192 `) 2193 process.exit(0) 2194 } 2195 2196 for (let i = 0; i < args.length; i++) { 2197 if (args[i] === '--name' || args[i] === '-n') options.name = args[++i] 2198 if (args[i] === '--http' || args[i] === '-h') options.http = true 2199 if (args[i] === '--mnemonic' || args[i] === '-m') options.mnemonic = args[++i] 2200 if (args[i] === '--help') { 2201 console.log(` 2202 Sovereign Mesh - Unified P2P Context Sync 2203 2204 The operator is the pilot. The mnemonic is their license number. 2205 2206 Usage: 2207 node sovereign-mesh.js [command] [options] 2208 2209 Commands: 2210 init Generate a new operator mnemonic and save it 2211 2212 Options: 2213 --mnemonic, -m <words> Your 4-word operator mnemonic (e.g., "apple-river-stone-falcon") 2214 --name, -n <name> Instance name (default: hostname) 2215 --http, -h Enable HTTP relay for iOS Shortcuts 2216 --help Show this help 2217 2218 Examples: 2219 # First time: Generate your operator mnemonic 2220 node sovereign-mesh.js init 2221 2222 # Start mesh with your mnemonic (with HTTP relay for iOS) 2223 node sovereign-mesh.js --mnemonic "apple-river-stone-falcon" --http --name "macbook" 2224 2225 # Join from another machine 2226 node sovereign-mesh.js --mnemonic "apple-river-stone-falcon" --name "mac-mini" 2227 2228 # If mnemonic already saved, just run: 2229 node sovereign-mesh.js --http 2230 2231 Hierarchy: 2232 [operator].[instance].[session] 2233 apple-river-stone-falcon.macbook.quiet-mountain-silver-dawn 2234 2235 Security (BIP-39 standard): 2236 - 4 words = 44 bits entropy (17.6 trillion combinations) 2237 - PBKDF2-HMAC-SHA256 key derivation (2048 iterations) 2238 - Keys derived: sovereign (AES-256-GCM), mesh topic, Nostr identity 2239 `) 2240 process.exit(0) 2241 } 2242 } 2243 2244 const mesh = new SovereignMesh(options) 2245 process.on('SIGINT', () => mesh.stop()) 2246 process.on('SIGTERM', () => mesh.stop()) 2247 mesh.start().catch(console.error)