/ scripts / hypercore_daemon.js
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  })