/ hooks / hypercore_context_hook.py
hypercore_context_hook.py
  1  #!/usr/bin/env python3
  2  """
  3  Hypercore Context Hook for Claude Code
  4  
  5  Injects cross-session awareness into Claude Code conversations.
  6  This runs as a Claude Code hook at the start of each user message.
  7  
  8  Install:
  9    Add to ~/.claude/settings.json:
 10    {
 11      "hooks": {
 12        "UserPromptSubmit": [
 13          {
 14            "command": "python3 /Users/rcerf/repos/Sovereign_OS/hooks/hypercore_context_hook.py"
 15          }
 16        ]
 17      }
 18    }
 19  
 20  What it does:
 21  1. Reads local FO-STATE.json (First Officer persistent state) - ALWAYS available
 22  2. Reads local Phoenix states from sessions directory - ALWAYS available
 23  3. Connects to Hypercore daemon at localhost:7777 - IF available
 24  4. Fetches cross-session attractors (hot topics) - IF Hypercore available
 25  5. Outputs awareness blocks for Claude
 26  
 27  The key insight: Local state is PRIMARY (always available), Hypercore is BONUS.
 28  This ensures cognitive continuity even when Hypercore daemon isn't running.
 29  """
 30  
 31  import json
 32  import sys
 33  import urllib.request
 34  import urllib.error
 35  from datetime import datetime
 36  from pathlib import Path
 37  
 38  DAEMON_URL = "http://localhost:7777"
 39  MAX_ATTRACTORS = 5
 40  MAX_PHOENIX_STATES = 3
 41  
 42  # Local paths (relative to hook location)
 43  HOOK_DIR = Path(__file__).parent
 44  SOVEREIGN_OS_ROOT = HOOK_DIR.parent
 45  SESSIONS_DIR = SOVEREIGN_OS_ROOT / "sessions"
 46  FO_STATE_PATH = SESSIONS_DIR / "FO-STATE.json"
 47  LIVE_COMPRESSION_PATH = SESSIONS_DIR / "LIVE-COMPRESSION.md"
 48  
 49  
 50  def fetch_json(path: str) -> dict:
 51      """Fetch JSON from daemon."""
 52      try:
 53          url = f"{DAEMON_URL}{path}"
 54          with urllib.request.urlopen(url, timeout=2) as response:
 55              return json.loads(response.read().decode('utf-8'))
 56      except:
 57          return {}
 58  
 59  
 60  # =============================================================================
 61  # LOCAL STATE READERS (ALWAYS AVAILABLE)
 62  # =============================================================================
 63  
 64  def get_local_fo_state() -> dict:
 65      """Read local First Officer state."""
 66      try:
 67          if FO_STATE_PATH.exists():
 68              return json.loads(FO_STATE_PATH.read_text())
 69      except:
 70          pass
 71      return {}
 72  
 73  
 74  def get_local_phoenix_state() -> str:
 75      """Read local LIVE-COMPRESSION.md and extract key info."""
 76      lines = []
 77  
 78      try:
 79          if LIVE_COMPRESSION_PATH.exists():
 80              content = LIVE_COMPRESSION_PATH.read_text()
 81  
 82              # Extract key metadata
 83              session_id = ""
 84              updated = ""
 85              focus = ""
 86              gravity_wells = []
 87  
 88              for line in content.split("\n"):
 89                  if "session_id::" in line or "session:" in line:
 90                      session_id = line.split("::")[-1].strip() if "::" in line else line.split(":")[-1].strip()
 91                  elif "updated::" in line:
 92                      updated = line.split("::", 1)[1].strip()
 93                  elif "**Primary work:**" in line:
 94                      focus = line.replace("**Primary work:**", "").strip()
 95                  elif "[[" in line and "]]" in line and "strength::" not in line:
 96                      import re
 97                      match = re.search(r'\[\[([^\]]+)\]\]', line)
 98                      if match:
 99                          gravity_wells.append(match.group(1))
100  
101              if session_id or updated or focus:
102                  lines.append(f"  - {session_id or 'current'} ({updated[:16] if updated else 'unknown'}): {', '.join(gravity_wells[:3]) or focus[:50]}")
103  
104      except:
105          pass
106  
107      return "\n".join(lines)
108  
109  
110  def get_local_gravity_wells() -> list:
111      """Extract gravity wells from local FO state."""
112      fo_state = get_local_fo_state()
113      wells = fo_state.get("gravity_wells", {})
114  
115      if wells:
116          sorted_wells = sorted(wells.items(), key=lambda x: -x[1])
117          return [{"concept": k, "strength": v} for k, v in sorted_wells[:10]]
118  
119      return []
120  
121  
122  def get_cross_session_context() -> str:
123      """Build cross-session awareness context."""
124      lines = []
125  
126      # Get attractors (hot topics across sessions)
127      attractors_data = fetch_json("/attractors")
128      attractors = attractors_data.get("attractors", [])
129  
130      # Filter to multi-session attractors
131      cross_session = [a for a in attractors if len(a.get("sessions", [])) >= 2]
132      multi_machine = [a for a in attractors if len(a.get("machines", [])) > 1]
133  
134      if cross_session or multi_machine:
135          lines.append("<cross-session-awareness>")
136          lines.append("Context from Hypercore P2P mesh:")
137          lines.append("")
138  
139          if cross_session:
140              lines.append("**Topics active across multiple sessions:**")
141              for a in cross_session[:MAX_ATTRACTORS]:
142                  topic = a.get("topic", "")
143                  sessions = len(a.get("sessions", []))
144                  lines.append(f"  - {topic} ({sessions} sessions)")
145              lines.append("")
146  
147          if multi_machine:
148              lines.append("**Topics spanning multiple machines:**")
149              for a in multi_machine[:MAX_ATTRACTORS]:
150                  topic = a.get("topic", "")
151                  machines = a.get("machines", [])
152                  lines.append(f"  - {topic} (on: {', '.join(machines)})")
153              lines.append("")
154  
155          lines.append("Consider how your conversation relates to these active threads.")
156          lines.append("</cross-session-awareness>")
157  
158      # Get recent Phoenix states
159      phoenix_data = fetch_json("/phoenix")
160      states = phoenix_data.get("states", [])
161  
162      if states:
163          # Sort by most recent
164          recent = sorted(states, key=lambda s: s.get("storedAt", ""), reverse=True)[:MAX_PHOENIX_STATES]
165  
166          if recent:
167              lines.append("")
168              lines.append("<recent-phoenix-states>")
169              lines.append("Recent cognitive checkpoints available for resurrection:")
170              for state in recent:
171                  session_id = state.get("sessionId", "unknown")
172                  stored_at = state.get("storedAt", "")[:16]
173                  wells = state.get("gravityWells", [])
174                  well_str = ", ".join(w.get("concept", "") if isinstance(w, dict) else str(w) for w in wells[:3])
175                  lines.append(f"  - {session_id} ({stored_at}): {well_str}")
176              lines.append("</recent-phoenix-states>")
177  
178      # Get daemon status
179      status = fetch_json("/status")
180      peers = status.get("peers", 0)
181      machine = status.get("machine", "unknown")
182  
183      if peers > 0:
184          lines.append("")
185          lines.append(f"<p2p-status>Connected to {peers} peer(s) via Hyperswarm</p2p-status>")
186  
187      return "\n".join(lines)
188  
189  
190  def register_session_start():
191      """Register this session start with Hypercore."""
192      try:
193          # Generate a session ID from timestamp
194          session_id = f"claude-code-{datetime.now().strftime('%Y%m%d-%H%M%S')}"
195  
196          data = json.dumps({
197              "type": "session_start",
198              "sessionId": session_id,
199              "source": "claude-code-hook"
200          }).encode('utf-8')
201  
202          req = urllib.request.Request(
203              f"{DAEMON_URL}/event",
204              data=data,
205              headers={"Content-Type": "application/json"},
206              method="POST"
207          )
208  
209          with urllib.request.urlopen(req, timeout=2):
210              pass
211  
212      except:
213          pass  # Silent fail - don't break the hook
214  
215  
216  def main():
217      """Main hook entry point."""
218      # Read hook input from stdin (if any)
219      # For UserPromptSubmit, we just output context
220  
221      # Register this session
222      register_session_start()
223  
224      lines = []
225  
226      # ALWAYS: Get local phoenix state (doesn't require Hypercore daemon)
227      local_phoenix = get_local_phoenix_state()
228      local_wells = get_local_gravity_wells()
229  
230      if local_phoenix or local_wells:
231          lines.append("<recent-phoenix-states>")
232          lines.append("Recent cognitive checkpoints available for resurrection:")
233          if local_phoenix:
234              lines.append(local_phoenix)
235  
236      # BONUS: Get Hypercore cross-session context (if daemon running)
237      hypercore_context = get_cross_session_context()
238      if hypercore_context:
239          lines.append(hypercore_context)
240  
241      # Close phoenix states block if we started it
242      if local_phoenix or local_wells:
243          lines.append("</recent-phoenix-states>")
244  
245      # Output combined context
246      if lines:
247          print("\n".join(lines))
248  
249  
250  if __name__ == "__main__":
251      main()