hypercore_daemon.js
1 #!/usr/bin/env node 2 /** 3 * Hypercore Context Sync Daemon 4 * 5 * P2P replication of Claude Code session context across machines. 6 * Provides HTTP API for Python integration. 7 * 8 * Architecture: 9 * Hypercore (append-only): 10 * - context:sessions - All session events 11 * - context:attention - Attention/gaze events 12 * 13 * Hyperbee (key-value): 14 * - attractor:{topic} - Cross-session attractors 15 * - phoenix:{session_id} - Phoenix states for resurrection 16 * - session:{session_id} - Active session metadata 17 * - machine:{machine_id} - Machine presence/status 18 * 19 * Usage: 20 * node hypercore_daemon.js # Start daemon 21 * node hypercore_daemon.js --port 7777 # Custom port 22 * node hypercore_daemon.js --join <key> # Join existing swarm 23 * 24 * HTTP API: 25 * POST /event - Write session event 26 * POST /phoenix - Store Phoenix state 27 * GET /phoenix/:id - Retrieve Phoenix state 28 * GET /attractors - List cross-session attractors 29 * GET /status - Daemon status + peer count 30 * POST /topic - Register topic activity 31 */ 32 33 const Corestore = require('corestore') 34 const Hyperbee = require('hyperbee') 35 const Hyperswarm = require('hyperswarm') 36 const express = require('express') 37 const crypto = require('crypto') 38 const path = require('path') 39 const fs = require('fs') 40 const os = require('os') 41 42 // Configuration 43 const STORAGE_PATH = process.env.HYPERCORE_PATH || 44 path.join(os.homedir(), '.cerf', 'hypercore-context') 45 const DEFAULT_PORT = 7777 46 const SWARM_TOPIC = 'cerf-sovereign-context-v1' 47 48 class HypercoreDaemon { 49 constructor(options = {}) { 50 this.port = options.port || DEFAULT_PORT 51 this.joinKey = options.joinKey || null 52 this.machineId = options.machineId || os.hostname() 53 54 this.store = null 55 this.sessionsCore = null 56 this.attentionCore = null 57 this.db = null 58 this.swarm = null 59 this.app = null 60 this.server = null 61 62 this.peers = new Set() 63 this.startedAt = new Date() 64 this.eventCount = 0 65 } 66 67 async start() { 68 console.log('=== Hypercore Context Daemon ===') 69 console.log(`Machine: ${this.machineId}`) 70 console.log(`Storage: ${STORAGE_PATH}`) 71 console.log(`Port: ${this.port}`) 72 console.log('') 73 74 // Ensure storage directory 75 fs.mkdirSync(STORAGE_PATH, { recursive: true }) 76 77 // Initialize Corestore 78 this.store = new Corestore(STORAGE_PATH) 79 await this.store.ready() 80 81 // Initialize cores 82 this.sessionsCore = this.store.get({ name: 'context:sessions' }) 83 this.attentionCore = this.store.get({ name: 'context:attention' }) 84 await Promise.all([this.sessionsCore.ready(), this.attentionCore.ready()]) 85 86 // Initialize Hyperbee for indexed data 87 const indexCore = this.store.get({ name: 'context:index' }) 88 await indexCore.ready() 89 this.db = new Hyperbee(indexCore, { 90 keyEncoding: 'utf-8', 91 valueEncoding: 'json' 92 }) 93 await this.db.ready() 94 95 // Register this machine 96 await this.db.put(`machine:${this.machineId}`, { 97 machineId: this.machineId, 98 startedAt: this.startedAt.toISOString(), 99 lastSeen: new Date().toISOString(), 100 platform: os.platform(), 101 arch: os.arch() 102 }) 103 104 console.log('Cores initialized:') 105 console.log(` Sessions: ${this.sessionsCore.key.toString('hex').slice(0, 16)}...`) 106 console.log(` Attention: ${this.attentionCore.key.toString('hex').slice(0, 16)}...`) 107 console.log(` Index: ${this.db.core.key.toString('hex').slice(0, 16)}...`) 108 console.log('') 109 110 // Start Hyperswarm for P2P 111 await this.startSwarm() 112 113 // Start HTTP server 114 await this.startHTTP() 115 116 // Heartbeat 117 setInterval(() => this.heartbeat(), 30000) 118 119 console.log('') 120 console.log('Daemon running. Press Ctrl+C to stop.') 121 return this 122 } 123 124 async startSwarm() { 125 this.swarm = new Hyperswarm() 126 127 // Handle new connections 128 this.swarm.on('connection', (conn, info) => { 129 const peerId = info.publicKey.toString('hex').slice(0, 8) 130 console.log(`[Swarm] Peer connected: ${peerId}`) 131 this.peers.add(peerId) 132 133 // Replicate all cores 134 this.store.replicate(conn) 135 136 conn.on('close', () => { 137 console.log(`[Swarm] Peer disconnected: ${peerId}`) 138 this.peers.delete(peerId) 139 }) 140 141 conn.on('error', (err) => { 142 console.log(`[Swarm] Peer error: ${err.message}`) 143 }) 144 }) 145 146 // Join the swarm 147 const topic = this.joinKey 148 ? Buffer.from(this.joinKey, 'hex') 149 : crypto.createHash('sha256').update(SWARM_TOPIC).digest() 150 151 const discovery = this.swarm.join(topic, { server: true, client: true }) 152 await discovery.flushed() 153 154 console.log('Hyperswarm active:') 155 console.log(` Topic: ${topic.toString('hex').slice(0, 16)}...`) 156 console.log(` Discovery key for other machines:`) 157 console.log(` ${topic.toString('hex')}`) 158 } 159 160 async startHTTP() { 161 this.app = express() 162 this.app.use(express.json({ limit: '10mb' })) 163 164 // CORS for local development 165 this.app.use((req, res, next) => { 166 res.header('Access-Control-Allow-Origin', '*') 167 res.header('Access-Control-Allow-Headers', 'Content-Type') 168 next() 169 }) 170 171 // Health check 172 this.app.get('/health', (req, res) => { 173 res.json({ status: 'ok', machine: this.machineId }) 174 }) 175 176 // Status endpoint 177 this.app.get('/status', async (req, res) => { 178 const attractorCount = await this.countKeys('attractor:') 179 const phoenixCount = await this.countKeys('phoenix:') 180 const sessionCount = await this.countKeys('session:') 181 182 res.json({ 183 machine: this.machineId, 184 uptime: Math.floor((Date.now() - this.startedAt.getTime()) / 1000), 185 peers: this.peers.size, 186 peerIds: Array.from(this.peers), 187 cores: { 188 sessions: { 189 key: this.sessionsCore.key.toString('hex'), 190 length: this.sessionsCore.length 191 }, 192 attention: { 193 key: this.attentionCore.key.toString('hex'), 194 length: this.attentionCore.length 195 }, 196 index: { 197 key: this.db.core.key.toString('hex'), 198 length: this.db.core.length 199 } 200 }, 201 counts: { 202 attractors: attractorCount, 203 phoenixStates: phoenixCount, 204 sessions: sessionCount, 205 eventsProcessed: this.eventCount 206 } 207 }) 208 }) 209 210 // Write session event 211 this.app.post('/event', async (req, res) => { 212 try { 213 const event = { 214 ts: new Date().toISOString(), 215 machine: this.machineId, 216 ...req.body 217 } 218 219 await this.sessionsCore.append(Buffer.from(JSON.stringify(event))) 220 this.eventCount++ 221 222 // Update attractors if topics provided 223 if (event.topics && Array.isArray(event.topics)) { 224 for (const topic of event.topics) { 225 await this.updateAttractor(topic, event.sessionId) 226 } 227 } 228 229 res.json({ 230 success: true, 231 index: this.sessionsCore.length - 1, 232 eventCount: this.eventCount 233 }) 234 } catch (err) { 235 res.status(500).json({ error: err.message }) 236 } 237 }) 238 239 // Write attention event (gaze, focus, etc.) 240 this.app.post('/attention', async (req, res) => { 241 try { 242 const event = { 243 ts: new Date().toISOString(), 244 machine: this.machineId, 245 ...req.body 246 } 247 248 await this.attentionCore.append(Buffer.from(JSON.stringify(event))) 249 250 res.json({ 251 success: true, 252 index: this.attentionCore.length - 1 253 }) 254 } catch (err) { 255 res.status(500).json({ error: err.message }) 256 } 257 }) 258 259 // Register topic activity 260 this.app.post('/topic', async (req, res) => { 261 try { 262 const { topic, sessionId, strength } = req.body 263 if (!topic) { 264 return res.status(400).json({ error: 'topic required' }) 265 } 266 267 const result = await this.updateAttractor(topic, sessionId, strength) 268 res.json(result) 269 } catch (err) { 270 res.status(500).json({ error: err.message }) 271 } 272 }) 273 274 // Get attractors 275 this.app.get('/attractors', async (req, res) => { 276 try { 277 const attractors = [] 278 for await (const entry of this.db.createReadStream({ 279 gte: 'attractor:', 280 lt: 'attractor:~' 281 })) { 282 attractors.push(entry.value) 283 } 284 285 // Sort by count descending 286 attractors.sort((a, b) => b.count - a.count) 287 288 res.json({ attractors }) 289 } catch (err) { 290 res.status(500).json({ error: err.message }) 291 } 292 }) 293 294 // Store Phoenix state 295 this.app.post('/phoenix', async (req, res) => { 296 try { 297 const { sessionId, state } = req.body 298 if (!sessionId || !state) { 299 return res.status(400).json({ error: 'sessionId and state required' }) 300 } 301 302 const key = `phoenix:${sessionId}` 303 const record = { 304 sessionId, 305 storedAt: new Date().toISOString(), 306 storedBy: this.machineId, 307 state 308 } 309 310 await this.db.put(key, record) 311 312 // Also append to sessions core for audit trail 313 await this.sessionsCore.append(Buffer.from(JSON.stringify({ 314 ts: new Date().toISOString(), 315 type: 'phoenix_stored', 316 sessionId, 317 machine: this.machineId 318 }))) 319 320 res.json({ success: true, key }) 321 } catch (err) { 322 res.status(500).json({ error: err.message }) 323 } 324 }) 325 326 // Get Phoenix state 327 this.app.get('/phoenix/:sessionId', async (req, res) => { 328 try { 329 const key = `phoenix:${req.params.sessionId}` 330 const entry = await this.db.get(key) 331 332 if (!entry) { 333 return res.status(404).json({ error: 'Phoenix state not found' }) 334 } 335 336 res.json(entry.value) 337 } catch (err) { 338 res.status(500).json({ error: err.message }) 339 } 340 }) 341 342 // List all Phoenix states 343 this.app.get('/phoenix', async (req, res) => { 344 try { 345 const states = [] 346 for await (const entry of this.db.createReadStream({ 347 gte: 'phoenix:', 348 lt: 'phoenix:~' 349 })) { 350 states.push({ 351 sessionId: entry.value.sessionId, 352 storedAt: entry.value.storedAt, 353 storedBy: entry.value.storedBy, 354 // Don't include full state in list view 355 gravityWells: entry.value.state?.gravity_wells?.slice(0, 3) 356 }) 357 } 358 359 res.json({ states }) 360 } catch (err) { 361 res.status(500).json({ error: err.message }) 362 } 363 }) 364 365 // Get recent events 366 this.app.get('/events', async (req, res) => { 367 try { 368 const limit = Math.min(parseInt(req.query.limit) || 20, 100) 369 const events = [] 370 const start = Math.max(0, this.sessionsCore.length - limit) 371 372 for (let i = start; i < this.sessionsCore.length; i++) { 373 const block = await this.sessionsCore.get(i) 374 events.push(JSON.parse(block.toString())) 375 } 376 377 res.json({ events, total: this.sessionsCore.length }) 378 } catch (err) { 379 res.status(500).json({ error: err.message }) 380 } 381 }) 382 383 // Get active sessions across machines 384 this.app.get('/sessions', async (req, res) => { 385 try { 386 const sessions = [] 387 for await (const entry of this.db.createReadStream({ 388 gte: 'session:', 389 lt: 'session:~' 390 })) { 391 sessions.push(entry.value) 392 } 393 394 res.json({ sessions }) 395 } catch (err) { 396 res.status(500).json({ error: err.message }) 397 } 398 }) 399 400 // Register/update session 401 this.app.post('/session', async (req, res) => { 402 try { 403 const { sessionId, topics, altitude, status } = req.body 404 if (!sessionId) { 405 return res.status(400).json({ error: 'sessionId required' }) 406 } 407 408 const key = `session:${sessionId}` 409 const existing = await this.db.get(key) 410 411 const record = { 412 sessionId, 413 machine: this.machineId, 414 topics: topics || existing?.value?.topics || [], 415 altitude: altitude || existing?.value?.altitude || 'tactical', 416 status: status || 'active', 417 startedAt: existing?.value?.startedAt || new Date().toISOString(), 418 lastUpdate: new Date().toISOString() 419 } 420 421 await this.db.put(key, record) 422 423 // Update membrane topics 424 if (topics && Array.isArray(topics)) { 425 for (const topic of topics) { 426 await this.updateAttractor(topic, sessionId) 427 } 428 } 429 430 res.json({ success: true, session: record }) 431 } catch (err) { 432 res.status(500).json({ error: err.message }) 433 } 434 }) 435 436 // Get machines 437 this.app.get('/machines', async (req, res) => { 438 try { 439 const machines = [] 440 for await (const entry of this.db.createReadStream({ 441 gte: 'machine:', 442 lt: 'machine:~' 443 })) { 444 machines.push(entry.value) 445 } 446 447 res.json({ machines }) 448 } catch (err) { 449 res.status(500).json({ error: err.message }) 450 } 451 }) 452 453 // Replication keys for manual sync 454 this.app.get('/keys', (req, res) => { 455 res.json({ 456 sessions: this.sessionsCore.key.toString('hex'), 457 attention: this.attentionCore.key.toString('hex'), 458 index: this.db.core.key.toString('hex'), 459 swarmTopic: crypto.createHash('sha256').update(SWARM_TOPIC).digest().toString('hex') 460 }) 461 }) 462 463 // Start server 464 return new Promise((resolve) => { 465 this.server = this.app.listen(this.port, () => { 466 console.log(`HTTP API listening on port ${this.port}`) 467 console.log(` Status: http://localhost:${this.port}/status`) 468 console.log(` Attractors: http://localhost:${this.port}/attractors`) 469 console.log(` Phoenix: http://localhost:${this.port}/phoenix`) 470 resolve() 471 }) 472 }) 473 } 474 475 async updateAttractor(topic, sessionId, strength = 1.0) { 476 const key = `attractor:${topic.toLowerCase()}` 477 const existing = await this.db.get(key) 478 479 const sessions = existing?.value?.sessions || [] 480 if (sessionId && !sessions.includes(sessionId)) { 481 sessions.push(sessionId) 482 } 483 484 const record = { 485 topic: topic, 486 count: (existing?.value?.count || 0) + 1, 487 strength: Math.min(1.0, (existing?.value?.strength || 0) + strength * 0.1), 488 sessions: sessions.slice(-20), // Keep last 20 sessions 489 lastSeen: new Date().toISOString(), 490 machines: [...new Set([...(existing?.value?.machines || []), this.machineId])] 491 } 492 493 await this.db.put(key, record) 494 return record 495 } 496 497 async countKeys(prefix) { 498 let count = 0 499 for await (const _ of this.db.createReadStream({ 500 gte: prefix, 501 lt: prefix + '~' 502 })) { 503 count++ 504 } 505 return count 506 } 507 508 async heartbeat() { 509 // Update machine last seen 510 await this.db.put(`machine:${this.machineId}`, { 511 machineId: this.machineId, 512 startedAt: this.startedAt.toISOString(), 513 lastSeen: new Date().toISOString(), 514 platform: os.platform(), 515 arch: os.arch(), 516 peers: this.peers.size, 517 eventCount: this.eventCount 518 }) 519 } 520 521 async stop() { 522 console.log('\nShutting down...') 523 524 if (this.server) { 525 this.server.close() 526 } 527 528 if (this.swarm) { 529 await this.swarm.destroy() 530 } 531 532 if (this.store) { 533 await this.store.close() 534 } 535 536 console.log('Daemon stopped.') 537 } 538 } 539 540 // CLI 541 async function main() { 542 const args = process.argv.slice(2) 543 544 let port = DEFAULT_PORT 545 let joinKey = null 546 547 for (let i = 0; i < args.length; i++) { 548 if (args[i] === '--port' && args[i + 1]) { 549 port = parseInt(args[i + 1]) 550 i++ 551 } else if (args[i] === '--join' && args[i + 1]) { 552 joinKey = args[i + 1] 553 i++ 554 } else if (args[i] === '--help' || args[i] === '-h') { 555 console.log(` 556 Hypercore Context Sync Daemon 557 558 Usage: 559 node hypercore_daemon.js [options] 560 561 Options: 562 --port <port> HTTP API port (default: ${DEFAULT_PORT}) 563 --join <key> Join existing swarm with discovery key 564 --help Show this help 565 566 HTTP API Endpoints: 567 GET /status Daemon status + peer count 568 GET /keys Replication keys for manual sync 569 GET /attractors List cross-session attractors 570 GET /events Recent session events 571 GET /sessions Active sessions across machines 572 GET /machines Connected machines 573 GET /phoenix List Phoenix states 574 GET /phoenix/:id Get specific Phoenix state 575 POST /event Write session event 576 POST /attention Write attention event 577 POST /topic Register topic activity 578 POST /session Register/update session 579 POST /phoenix Store Phoenix state 580 581 Example: 582 # Start on MacBook 583 node hypercore_daemon.js --port 7777 584 585 # Start on Mac Mini, join MacBook's swarm 586 node hypercore_daemon.js --port 7777 --join <discovery_key> 587 `) 588 process.exit(0) 589 } 590 } 591 592 const daemon = new HypercoreDaemon({ port, joinKey }) 593 594 process.on('SIGINT', async () => { 595 await daemon.stop() 596 process.exit(0) 597 }) 598 599 process.on('SIGTERM', async () => { 600 await daemon.stop() 601 process.exit(0) 602 }) 603 604 await daemon.start() 605 } 606 607 main().catch(err => { 608 console.error('Fatal error:', err) 609 process.exit(1) 610 })