Skip to content

Commit fb506dd

Browse files
authored
Merge pull request #179 from thestumonkey/merge-mycelia-memories
Merge mycelia memories
2 parents 1356e79 + 8e45aae commit fb506dd

73 files changed

Lines changed: 7815 additions & 1754 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitmodules

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[submodule "extras/mycelia"]
2+
path = extras/mycelia
3+
url = https://github.com/mycelia-tech/mycelia

Makefile

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export $(shell sed 's/=.*//' config.env | grep -v '^\s*$$' | grep -v '^\s*\#')
1919
SCRIPTS_DIR := scripts
2020
K8S_SCRIPTS_DIR := $(SCRIPTS_DIR)/k8s
2121

22-
.PHONY: help menu setup-k8s setup-infrastructure setup-rbac setup-storage-pvc config config-docker config-k8s config-all clean deploy deploy-docker deploy-k8s deploy-k8s-full deploy-infrastructure deploy-apps check-infrastructure check-apps build-backend up-backend down-backend k8s-status k8s-cleanup k8s-purge audio-manage test-robot test-robot-integration test-robot-unit test-robot-endpoints test-robot-specific test-robot-clean
22+
.PHONY: help menu setup-k8s setup-infrastructure setup-rbac setup-storage-pvc config config-docker config-k8s config-all clean deploy deploy-docker deploy-k8s deploy-k8s-full deploy-infrastructure deploy-apps check-infrastructure check-apps build-backend up-backend down-backend k8s-status k8s-cleanup k8s-purge audio-manage mycelia-sync-status mycelia-sync-all mycelia-sync-user mycelia-check-orphans mycelia-reassign-orphans test-robot test-robot-integration test-robot-unit test-robot-endpoints test-robot-specific test-robot-clean
2323

2424
# Default target
2525
.DEFAULT_GOAL := menu
@@ -57,6 +57,13 @@ menu: ## Show interactive menu (default)
5757
@echo " check-apps 🔍 Check application services"
5858
@echo " clean 🧹 Clean up generated files"
5959
@echo
60+
@echo "🔄 Mycelia Sync:"
61+
@echo " mycelia-sync-status 📊 Show Mycelia OAuth sync status"
62+
@echo " mycelia-sync-all 🔄 Sync all Friend-Lite users to Mycelia"
63+
@echo " mycelia-sync-user 👤 Sync specific user (EMAIL=user@example.com)"
64+
@echo " mycelia-check-orphans 🔍 Find orphaned Mycelia objects"
65+
@echo " mycelia-reassign-orphans ♻️ Reassign orphans (EMAIL=admin@example.com)"
66+
@echo
6067
@echo "Current configuration:"
6168
@echo " DOMAIN: $(DOMAIN)"
6269
@echo " DEPLOYMENT_MODE: $(DEPLOYMENT_MODE)"
@@ -101,6 +108,13 @@ help: ## Show detailed help for all targets
101108
@echo "🎵 AUDIO MANAGEMENT:"
102109
@echo " audio-manage Interactive audio file management"
103110
@echo
111+
@echo "🔄 MYCELIA SYNC:"
112+
@echo " mycelia-sync-status Show Mycelia OAuth sync status for all users"
113+
@echo " mycelia-sync-all Sync all Friend-Lite users to Mycelia OAuth"
114+
@echo " mycelia-sync-user Sync specific user (EMAIL=user@example.com)"
115+
@echo " mycelia-check-orphans Find Mycelia objects without Friend-Lite owner"
116+
@echo " mycelia-reassign-orphans Reassign orphaned objects (EMAIL=admin@example.com)"
117+
@echo
104118
@echo "🧪 ROBOT FRAMEWORK TESTING:"
105119
@echo " test-robot Run all Robot Framework tests"
106120
@echo " test-robot-integration Run integration tests only"
@@ -333,6 +347,42 @@ audio-manage: ## Interactive audio file management
333347
@echo "🎵 Starting audio file management..."
334348
@$(SCRIPTS_DIR)/manage-audio-files.sh
335349

350+
# ========================================
351+
# MYCELIA SYNC
352+
# ========================================
353+
354+
mycelia-sync-status: ## Show Mycelia OAuth sync status for all users
355+
@echo "📊 Checking Mycelia OAuth sync status..."
356+
@cd backends/advanced && uv run python scripts/sync_friendlite_mycelia.py --status
357+
358+
mycelia-sync-all: ## Sync all Friend-Lite users to Mycelia OAuth
359+
@echo "🔄 Syncing all Friend-Lite users to Mycelia OAuth..."
360+
@echo "⚠️ This will create OAuth credentials for users without them"
361+
@read -p "Continue? (y/N): " confirm && [ "$$confirm" = "y" ] || exit 1
362+
@cd backends/advanced && uv run python scripts/sync_friendlite_mycelia.py --sync-all
363+
364+
mycelia-sync-user: ## Sync specific user to Mycelia OAuth (usage: make mycelia-sync-user EMAIL=user@example.com)
365+
@echo "👤 Syncing specific user to Mycelia OAuth..."
366+
@if [ -z "$(EMAIL)" ]; then \
367+
echo "❌ EMAIL parameter is required. Usage: make mycelia-sync-user EMAIL=user@example.com"; \
368+
exit 1; \
369+
fi
370+
@cd backends/advanced && uv run python scripts/sync_friendlite_mycelia.py --email $(EMAIL)
371+
372+
mycelia-check-orphans: ## Find Mycelia objects without Friend-Lite owner
373+
@echo "🔍 Checking for orphaned Mycelia objects..."
374+
@cd backends/advanced && uv run python scripts/sync_friendlite_mycelia.py --check-orphans
375+
376+
mycelia-reassign-orphans: ## Reassign orphaned objects to user (usage: make mycelia-reassign-orphans EMAIL=admin@example.com)
377+
@echo "♻️ Reassigning orphaned Mycelia objects..."
378+
@if [ -z "$(EMAIL)" ]; then \
379+
echo "❌ EMAIL parameter is required. Usage: make mycelia-reassign-orphans EMAIL=admin@example.com"; \
380+
exit 1; \
381+
fi
382+
@echo "⚠️ This will reassign all orphaned objects to: $(EMAIL)"
383+
@read -p "Continue? (y/N): " confirm && [ "$$confirm" = "y" ] || exit 1
384+
@cd backends/advanced && uv run python scripts/sync_friendlite_mycelia.py --reassign-orphans --target-email $(EMAIL)
385+
336386
# ========================================
337387
# TESTING TARGETS
338388
# ========================================

README-K8S.md

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -266,15 +266,20 @@ friend-lite/
266266

267267
1. **Clone Repository**
268268
```bash
269-
# Clone Friend-Lite repository
270-
git clone https://github.com/yourusername/friend-lite.git
269+
# Clone Friend-Lite repository with submodules
270+
git clone --recursive https://github.com/yourusername/friend-lite.git
271271
cd friend-lite
272-
272+
273+
# If you already cloned without --recursive, initialize submodules:
274+
# git submodule update --init --recursive
275+
273276
# Verify template files are present
274277
ls -la skaffold.env.template
275278
ls -la backends/advanced/.env.template
276279
```
277280

281+
> **Note:** The `--recursive` flag downloads the optional Mycelia submodule (an alternative memory backend with timeline visualization). Most deployments use the default Friend-Lite memory system and don't need Mycelia.
282+
278283
2. **Install Required Tools**
279284

280285
**kubectl** (required for Skaffold and Helm):

backends/advanced/.env.template

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,8 +99,8 @@ QDRANT_BASE_URL=qdrant
9999
# MEMORY PROVIDER CONFIGURATION
100100
# ========================================
101101

102-
# Memory Provider: "friend_lite" (default) or "openmemory_mcp"
103-
#
102+
# Memory Provider: "friend_lite" (default), "openmemory_mcp", or "mycelia"
103+
#
104104
# Friend-Lite (default): In-house memory system with full control
105105
# - Custom LLM-powered extraction with individual fact storage
106106
# - Smart deduplication and memory updates (ADD/UPDATE/DELETE)
@@ -113,6 +113,13 @@ QDRANT_BASE_URL=qdrant
113113
# - Web UI at http://localhost:8765
114114
# - Requires external server setup
115115
#
116+
# Mycelia: Full-featured personal memory timeline
117+
# - Voice, screenshots, and text capture
118+
# - Timeline UI with waveform playback
119+
# - Conversation extraction and semantic search
120+
# - OAuth federation for cross-instance sharing
121+
# - Requires Mycelia server setup (extras/mycelia)
122+
#
116123
# See MEMORY_PROVIDERS.md for detailed comparison
117124
MEMORY_PROVIDER=friend_lite
118125

@@ -128,6 +135,19 @@ MEMORY_PROVIDER=friend_lite
128135
# OPENMEMORY_USER_ID=openmemory
129136
# OPENMEMORY_TIMEOUT=30
130137

138+
# ----------------------------------------
139+
# Mycelia Configuration
140+
# (Only needed if MEMORY_PROVIDER=mycelia)
141+
# ----------------------------------------
142+
# First start Mycelia:
143+
# cd extras/mycelia && docker compose up -d redis mongo mongo-search
144+
# cd extras/mycelia/backend && deno task dev
145+
#
146+
# IMPORTANT: JWT_SECRET in Mycelia backend/.env must match AUTH_SECRET_KEY above
147+
# MYCELIA_URL=http://host.docker.internal:5173
148+
# MYCELIA_DB=mycelia # Database name (use mycelia_test for test environment)
149+
# MYCELIA_TIMEOUT=30
150+
131151
# ========================================
132152
# OPTIONAL FEATURES
133153
# ========================================

backends/advanced/docker-compose-test.yml

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ services:
3838
- MEMORY_PROVIDER=${MEMORY_PROVIDER:-friend_lite}
3939
- OPENMEMORY_MCP_URL=${OPENMEMORY_MCP_URL:-http://host.docker.internal:8765}
4040
- OPENMEMORY_USER_ID=${OPENMEMORY_USER_ID:-openmemory}
41+
- MYCELIA_URL=http://mycelia-backend-test:5173
42+
- MYCELIA_DB=mycelia_test
4143
# Disable speaker recognition in test environment to prevent segment duplication
4244
- DISABLE_SPEAKER_RECOGNITION=false
4345
- SPEAKER_SERVICE_URL=https://localhost:8085
@@ -146,6 +148,8 @@ services:
146148
- MEMORY_PROVIDER=${MEMORY_PROVIDER:-friend_lite}
147149
- OPENMEMORY_MCP_URL=${OPENMEMORY_MCP_URL:-http://host.docker.internal:8765}
148150
- OPENMEMORY_USER_ID=${OPENMEMORY_USER_ID:-openmemory}
151+
- MYCELIA_URL=http://mycelia-backend-test:5173
152+
- MYCELIA_DB=mycelia_test
149153
- DISABLE_SPEAKER_RECOGNITION=false
150154
- SPEAKER_SERVICE_URL=https://localhost:8085
151155
# Set low inactivity timeout for tests (2 seconds instead of 60)
@@ -163,6 +167,60 @@ services:
163167
condition: service_started
164168
restart: unless-stopped
165169

170+
# Mycelia - AI memory and timeline service (test environment)
171+
mycelia-backend-test:
172+
build:
173+
context: ../../extras/mycelia/backend
174+
dockerfile: Dockerfile.simple
175+
ports:
176+
- "5100:5173" # Test backend port
177+
environment:
178+
# Shared JWT secret for Friend-Lite authentication (test key)
179+
- JWT_SECRET=test-jwt-signing-key-for-integration-tests
180+
- SECRET_KEY=test-jwt-signing-key-for-integration-tests
181+
# MongoDB connection (test database)
182+
- MONGO_URL=mongodb://mongo-test:27017
183+
- MONGO_DB=mycelia_test
184+
- DATABASE_NAME=mycelia_test
185+
# Redis connection (ioredis uses individual host/port, not URL)
186+
- REDIS_HOST=redis-test
187+
- REDIS_PORT=6379
188+
volumes:
189+
- ../../extras/mycelia/backend/app:/app/app # Mount source for development
190+
depends_on:
191+
mongo-test:
192+
condition: service_healthy
193+
redis-test:
194+
condition: service_started
195+
healthcheck:
196+
test: ["CMD", "deno", "eval", "fetch('http://localhost:5173/health').then(r => r.ok ? Deno.exit(0) : Deno.exit(1))"]
197+
interval: 30s
198+
timeout: 10s
199+
retries: 3
200+
start_period: 5s
201+
restart: unless-stopped
202+
profiles:
203+
- mycelia
204+
205+
mycelia-frontend-test:
206+
build:
207+
context: ../../extras/mycelia
208+
dockerfile: frontend/Dockerfile.simple
209+
args:
210+
- VITE_API_URL=http://localhost:5100
211+
ports:
212+
- "3002:8080" # Nginx serves on 8080 internally
213+
environment:
214+
- VITE_API_URL=http://localhost:5100
215+
volumes:
216+
- ../../extras/mycelia/frontend/src:/app/src # Mount source for development
217+
depends_on:
218+
mycelia-backend-test:
219+
condition: service_healthy
220+
restart: unless-stopped
221+
profiles:
222+
- mycelia
223+
166224
# caddy:
167225
# image: caddy:2-alpine
168226
# ports:
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
#!/usr/bin/env python3
2+
"""Create a proper Mycelia API key (not OAuth client) for Friend-Lite user."""
3+
4+
import base64
5+
import os
6+
import sys
7+
import secrets
8+
import hashlib
9+
from pymongo import MongoClient
10+
from bson import ObjectId
11+
from datetime import datetime
12+
13+
# MongoDB configuration
14+
MONGO_URL = os.getenv("MONGO_URL", "mongodb://localhost:27018")
15+
MYCELIA_DB = os.getenv("MYCELIA_DB", os.getenv("DATABASE_NAME", "mycelia_test"))
16+
17+
# User ID from JWT or argument
18+
USER_ID = os.getenv("USER_ID", "692c7727c7b16bdf58d23cd1") # test user
19+
20+
21+
def hash_api_key_with_salt(api_key: str, salt: bytes) -> str:
22+
"""Hash API key with salt (matches Mycelia's hashApiKey function)."""
23+
# SHA256(salt + apiKey) in base64
24+
h = hashlib.sha256()
25+
h.update(salt)
26+
h.update(api_key.encode('utf-8'))
27+
return base64.b64encode(h.digest()).decode('utf-8') # Use base64 like Mycelia
28+
29+
30+
def main():
31+
print(f"📊 MongoDB Configuration:")
32+
print(f" URL: {MONGO_URL}")
33+
print(f" Database: {MYCELIA_DB}\n")
34+
35+
print("🔐 Creating Mycelia API Key\n")
36+
37+
# Generate API key in Mycelia format: mycelia_{random_base64url}
38+
random_part = secrets.token_urlsafe(32)
39+
api_key = f"mycelia_{random_part}"
40+
41+
# Generate salt (32 bytes)
42+
salt = secrets.token_bytes(32)
43+
44+
# Hash the API key with salt
45+
hashed_key = hash_api_key_with_salt(api_key, salt)
46+
47+
# Open prefix (first 16 chars for fast lookup)
48+
open_prefix = api_key[:16]
49+
50+
print(f"✅ Generated API Key:")
51+
print(f" Key: {api_key}")
52+
print(f" Open Prefix: {open_prefix}")
53+
print(f" Owner: {USER_ID}\n")
54+
55+
# Connect to MongoDB
56+
client = MongoClient(MONGO_URL)
57+
db = client[MYCELIA_DB]
58+
api_keys = db["api_keys"]
59+
60+
# Check for existing active keys for this user
61+
existing = api_keys.find_one({"owner": USER_ID, "isActive": True})
62+
if existing:
63+
print(f"ℹ️ Existing active API key found: {existing['_id']}")
64+
print(f" Deactivating old key...\n")
65+
api_keys.update_one(
66+
{"_id": existing["_id"]},
67+
{"$set": {"isActive": False}}
68+
)
69+
70+
# Create API key document (matches Mycelia's format)
71+
api_key_doc = {
72+
"hashedKey": hashed_key, # Note: hashedKey, not hash!
73+
"salt": base64.b64encode(salt).decode('utf-8'), # Store as base64 like Mycelia
74+
"owner": USER_ID,
75+
"name": "Friend-Lite Integration",
76+
"policies": [
77+
{
78+
"resource": "**",
79+
"action": "*",
80+
"effect": "allow"
81+
}
82+
],
83+
"openPrefix": open_prefix,
84+
"createdAt": datetime.now(),
85+
"isActive": True,
86+
}
87+
88+
# Insert into database
89+
result = api_keys.insert_one(api_key_doc)
90+
client_id = str(result.inserted_id)
91+
92+
print(f"🎉 API Key Created Successfully!")
93+
print(f" Client ID: {client_id}")
94+
print(f" API Key: {api_key}")
95+
print(f"\n" + "=" * 70)
96+
print("📋 MYCELIA CONFIGURATION (Test Environment)")
97+
print("=" * 70)
98+
print(f"\n1️⃣ Configure Mycelia Frontend Settings:")
99+
print(f" • Go to: http://localhost:3002/settings")
100+
print(f" • API Endpoint: http://localhost:5100")
101+
print(f" • Client ID: {client_id}")
102+
print(f" • Client Secret: {api_key}")
103+
print(f" • Click 'Save' and then 'Test Token'")
104+
print(f"\n✅ This API key uses the proper Mycelia format with salt!")
105+
print("=" * 70 + "\n")
106+
107+
return 0
108+
109+
110+
if __name__ == "__main__":
111+
sys.exit(main())

0 commit comments

Comments
 (0)