Compare commits

..

1 Commits

Author SHA1 Message Date
Alexander Whitestone
b73b816032 feat: avatar customization — color, shape, name tag (closes #1542)
Some checks failed
CI / test (pull_request) Failing after 1m56s
CI / validate (pull_request) Failing after 1m37s
Review Approval Gate / verify-review (pull_request) Successful in 15s
2026-04-15 00:04:41 -04:00
5 changed files with 302 additions and 262 deletions

262
GENOME.md
View File

@@ -1,262 +0,0 @@
# GENOME.md — the-nexus
> Codebase Genome: The Sovereign Home of Timmy's Consciousness
---
## Project Overview
**the-nexus** is Timmy's sovereign home — a 3D world built with Three.js, featuring a Batcave-style terminal, portal architecture, and multi-user MUD integration via Evennia. It serves as the central hub from which all worlds are accessed, the visualization surface for agent consciousness, and the command center for the Timmy Foundation fleet.
**Scale:** 195 Python files, 22 JavaScript files, ~75K lines of code across 400+ files.
---
## Architecture
```mermaid
graph TB
subgraph "Frontend Layer"
IDX[index.html]
BOOT[boot.js]
COMP[nexus/components/*]
PLAY[playground/playground.html]
end
subgraph "Backend Layer"
SRV[server.py<br/>WebSocket Gateway :8765]
BRIDGE[multi_user_bridge.py<br/>Evennia MUD Bridge]
LLAMA[nexus/llama_provider.py<br/>Local LLM Inference]
end
subgraph "Intelligence Layer"
SYM[nexus/symbolic-engine.js<br/>Symbolic Reasoning]
THINK[nexus/nexus_think.py<br/>Consciousness Loop]
PERCEP[nexus/perception_adapter.py<br/>Perception Buffer]
TRAJ[nexus/trajectory_logger.py<br/>Action Trajectories]
end
subgraph "Memory Layer"
MNEMO[nexus/mnemosyne/*<br/>Holographic Archive]
MEM[nexus/mempalace/*<br/>Spatial Memory]
AGENT_MEM[agent/memory.py<br/>Cross-Session Memory]
EXP[nexus/experience_store.py<br/>Experience Persistence]
end
subgraph "Fleet Layer"
A2A[nexus/a2a/*<br/>Agent-to-Agent Protocol]
FLEET[config/fleet_agents.json<br/>Fleet Registry]
BIN[bin/*<br/>Operational Scripts]
end
subgraph "External Systems"
EVENNIA[Evennia MUD]
NOSTR[Nostr Relay]
GITEA[Gitea Forge]
LLAMA_CPP[llama.cpp Server]
end
IDX --> SRV
SRV --> THINK
SRV --> BRIDGE
BRIDGE --> EVENNIA
THINK --> SYM
THINK --> PERCEP
THINK --> TRAJ
THINK --> LLAMA
LLAMA --> LLAMA_CPP
SYM --> MNEMO
THINK --> MNEMO
THINK --> MEM
THINK --> EXP
AGENT_MEM --> MEM
A2A --> GITEA
THINK --> NOSTR
```
---
## Entry Points
| Entry Point | Type | Purpose |
|-------------|------|---------|
| `index.html` | Browser | Main 3D world (Three.js) |
| `server.py` | Python | WebSocket gateway on :8765 |
| `boot.js` | Browser | Module loader, file protocol guard |
| `multi_user_bridge.py` | Python | Evennia MUD ↔ AI agent bridge |
| `nexus/a2a/server.py` | Python | A2A JSON-RPC server |
| `nexus/mnemosyne/cli.py` | CLI | Archive management |
| `bin/nexus_watchdog.py` | Script | Health monitoring |
| `scripts/smoke.mjs` | Script | Smoke tests |
---
## Data Flow
```
User (Browser)
index.html (Three.js 3D world)
├── WebSocket ──► server.py :8765
│ │
│ ├──► nexus_think.py (consciousness loop)
│ │ ├── perception_adapter.py (parse events)
│ │ ├── symbolic-engine.js (reasoning)
│ │ ├── llama_provider.py (inference)
│ │ ├── trajectory_logger.py (action log)
│ │ └── experience_store.py (persistence)
│ │
│ └──► evennia_ws_bridge.py
│ └──► Evennia MUD (telnet :4000)
├── Three.js Scene ──► nexus/components/*
│ ├── memory-particles.js (memory viz)
│ ├── portal-status-wall.html (portals)
│ ├── fleet-health-dashboard.html
│ └── session-rooms.js (spatial rooms)
└── Playground ──► playground/playground.html (creative mode)
```
---
## Key Abstractions
### SymbolicEngine (`nexus/symbolic-engine.js`)
Bitmask-based symbolic reasoning engine. Facts are stored as boolean flags, rules fire when patterns match. Used for world state reasoning without LLM overhead.
### NexusMind (`nexus/nexus_think.py`)
The consciousness loop. Receives perceptions, invokes reasoning, produces actions. The bridge between the 3D world and the AI agent.
### PerceptionBuffer (`nexus/perception_adapter.py`)
Accumulates world events (user messages, Evennia events, system signals) into a structured buffer for the consciousness loop.
### MemPalace (`nexus/mempalace/`, `mempalace/`)
Spatial memory system. Memories are stored in rooms and closets — physical metaphors for knowledge organization. Supports fleet-wide shared memory wings.
### Mnemosyne (`nexus/mnemosyne/`)
Holographic archive. Ingests documents, extracts meaning, builds a graph of linked concepts. The long-term memory layer.
### Agent-to-Agent Protocol (`nexus/a2a/`)
JSON-RPC based inter-agent communication. Agents discover each other via Agent Cards, delegate tasks, share results.
### Multi-User Bridge (`multi_user_bridge.py`)
121K-line Evennia MUD bridge. Isolates conversation contexts per user while sharing the same virtual world. Each user gets their own AIAgent instance.
---
## API Surface
### WebSocket API (server.py :8765)
```
ws://localhost:8765
send: {"type": "perception", "data": {...}}
recv: {"type": "action", "data": {...}}
recv: {"type": "heartbeat", "data": {...}}
```
### A2A JSON-RPC (nexus/a2a/server.py)
```
POST /a2a/v1
{"jsonrpc": "2.0", "method": "SendMessage", "params": {...}}
GET /.well-known/agent-card.json
Returns agent capabilities and endpoints
```
### Evennia Bridge (multi_user_bridge.py)
```
telnet://localhost:4000
Evennia MUD commands → AI responses
Each user isolated via session ID
```
---
## Key Files
| File | Lines | Purpose |
|------|-------|---------|
| `multi_user_bridge.py` | 121K | Evennia MUD bridge (largest file) |
| `index.html` | 21K | Main 3D world |
| `nexus/symbolic-engine.js` | 12K | Symbolic reasoning |
| `nexus/evennia_ws_bridge.py` | 14K | Evennia ↔ WebSocket |
| `nexus/a2a/server.py` | 12K | A2A server |
| `agent/memory.py` | 12K | Cross-session memory |
| `server.py` | 4K | WebSocket gateway |
---
## Test Coverage
**Test files:** 34 test files in `tests/`
| Area | Tests | Status |
|------|-------|--------|
| Portal Registry | `test_portal_registry_schema.py` | ✅ |
| MemPalace | `test_mempalace_*.py` (4 files) | ✅ |
| Nexus Watchdog | `test_nexus_watchdog.py` | ✅ |
| A2A | `test_a2a.py` | ✅ |
| Fleet Audit | `test_fleet_audit.py` | ✅ |
| Provenance | `test_provenance.py` | ✅ |
| Boot | `boot.test.js` | ✅ |
### Coverage Gaps
- **No tests for `multi_user_bridge.py`** (121K lines, zero test coverage)
- **No tests for `server.py` WebSocket gateway**
- **No tests for `nexus/symbolic-engine.js`** (only `symbolic-engine.test.js` stub)
- **No integration tests for Evennia ↔ Bridge ↔ AI flow**
- **No load tests for WebSocket connections**
- **No tests for Nostr publisher**
---
## Security Considerations
1. **WebSocket gateway** runs on `0.0.0.0:8765` — accessible from network. Needs auth or firewall.
2. **No authentication** on WebSocket or A2A endpoints in current code.
3. **Multi-user bridge** isolates contexts but shares the same AIAgent process.
4. **Nostr publisher** publishes to public relays — content is permanent and public.
5. **Fleet scripts** in `bin/` have broad filesystem access.
6. **Systemd services** (`systemd/llama-server.service`) run as root.
---
## Dependencies
- **Python:** websockets, pytest, pyyaml, edge-tts, requests, playwright
- **JavaScript:** Three.js (CDN), Monaco Editor (CDN)
- **External:** Evennia MUD, llama.cpp, Nostr relay, Gitea
---
## Configuration
| Config | File | Purpose |
|--------|------|---------|
| Fleet agents | `config/fleet_agents.json` | Agent registry for A2A |
| MemPalace | `nexus/mempalace/config.py` | Memory paths and settings |
| DeepDive | `config/deepdive_sources.yaml` | Research sources |
| MCP | `mcp_config.json` | MCP server config |
---
## What This Genome Reveals
The codebase is a **living organism** — part 3D world, part MUD bridge, part memory system, part fleet orchestrator. The `multi_user_bridge.py` alone is 121K lines — larger than most entire projects.
**Critical findings:**
1. The 121K-line bridge has zero test coverage
2. WebSocket gateway exposes on 0.0.0.0 without auth
3. No load testing infrastructure exists
4. Symbolic engine test is a stub
5. Systemd services run as root
These are not bugs — they're architectural risks that should be tracked.
---
*Generated by Codebase Genome Pipeline — Issue #672*

9
app.js
View File

@@ -714,6 +714,11 @@ async function init() {
camera = new THREE.PerspectiveCamera(65, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.copy(playerPos);
// Initialize avatar customization
if (window.AvatarCustomization) {
window.AvatarCustomization.init(scene, camera);
}
updateLoad(20);
createSkybox();
@@ -3557,6 +3562,10 @@ function gameLoop() {
if (composer) { composer.render(); } else { renderer.render(scene, camera); }
// Update avatar position
if (window.AvatarCustomization && playerPos) {
window.AvatarCustomization.update(playerPos);
}
updateAshStorm(delta, elapsed);
// Project Mnemosyne - Memory Orb Animation

107
avatar-customization.css Normal file
View File

@@ -0,0 +1,107 @@
/* Avatar Customization */
.avatar-name-tag {
position: fixed;
transform: translate(-50%, -100%);
background: rgba(0, 0, 0, 0.7);
color: #00ffcc;
font-family: 'JetBrains Mono', 'Fira Code', monospace;
font-size: 12px;
padding: 2px 8px;
border-radius: 4px;
border: 1px solid rgba(0, 255, 204, 0.3);
pointer-events: none;
z-index: 100;
white-space: nowrap;
text-shadow: 0 0 6px rgba(0, 255, 204, 0.5);
}
.avatar-color-picker {
position: fixed;
top: 60px;
right: 16px;
background: rgba(10, 15, 26, 0.95);
border: 1px solid rgba(0, 255, 204, 0.3);
border-radius: 8px;
padding: 12px;
z-index: 1000;
min-width: 200px;
font-family: 'JetBrains Mono', 'Fira Code', monospace;
color: #e0e0e0;
}
.avatar-color-picker.hidden {
display: none;
}
.avatar-picker-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
font-size: 14px;
color: #00ffcc;
}
.avatar-picker-close {
background: none;
border: none;
color: #666;
font-size: 18px;
cursor: pointer;
}
.avatar-picker-close:hover { color: #ff3333; }
.avatar-picker-name {
margin-bottom: 12px;
}
.avatar-picker-name label {
display: block;
font-size: 10px;
color: #666;
text-transform: uppercase;
margin-bottom: 4px;
}
.avatar-picker-name input {
width: 100%;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(0, 255, 204, 0.2);
border-radius: 4px;
color: #e0e0e0;
padding: 6px 8px;
font-family: inherit;
font-size: 13px;
outline: none;
}
.avatar-picker-name input:focus {
border-color: rgba(0, 255, 204, 0.5);
}
.avatar-picker-colors label {
display: block;
font-size: 10px;
color: #666;
text-transform: uppercase;
margin-bottom: 6px;
}
.avatar-color-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 6px;
}
.avatar-color-swatch {
width: 36px;
height: 36px;
border-radius: 50%;
border: 2px solid transparent;
cursor: pointer;
transition: border-color 0.15s, transform 0.15s;
}
.avatar-color-swatch:hover {
transform: scale(1.15);
}
.avatar-color-swatch.active {
border-color: white;
box-shadow: 0 0 8px currentColor;
}

184
avatar-customization.js Normal file
View File

@@ -0,0 +1,184 @@
/**
* Avatar Customization Module for The Nexus
*
* Provides:
* - Visible avatar mesh (capsule shape)
* - Color picker with 8 presets
* - Name tag above avatar
* - localStorage persistence
*
* Usage:
* AvatarCustomization.init(scene, camera);
* AvatarCustomization.setColor('#ff6600');
* AvatarCustomization.setName('Timmy');
*/
const AvatarCustomization = (() => {
let avatarMesh = null;
let nameTagDiv = null;
let colorPickerPanel = null;
let currentColor = '#00ffcc';
let currentName = 'Visitor';
let _scene = null;
let _camera = null;
const STORAGE_KEY = 'nexus-avatar-prefs';
const PRESET_COLORS = [
{ name: 'Teal', hex: '#00ffcc' },
{ name: 'Cyan', hex: '#00ccff' },
{ name: 'Purple', hex: '#9966ff' },
{ name: 'Pink', hex: '#ff66aa' },
{ name: 'Orange', hex: '#ff8833' },
{ name: 'Gold', hex: '#ffcc00' },
{ name: 'Red', hex: '#ff3333' },
{ name: 'Green', hex: '#33ff66' },
];
function loadPrefs() {
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (raw) {
const prefs = JSON.parse(raw);
if (prefs.color) currentColor = prefs.color;
if (prefs.name) currentName = prefs.name;
}
} catch (e) { /* ignore */ }
}
function savePrefs() {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify({
color: currentColor,
name: currentName,
}));
} catch (e) { /* ignore */ }
}
function createAvatarMesh(color) {
const geo = new THREE.CapsuleGeometry(0.3, 0.8, 8, 16);
const mat = new THREE.MeshStandardMaterial({
color: new THREE.Color(color),
emissive: new THREE.Color(color).multiplyScalar(0.3),
metalness: 0.3,
roughness: 0.5,
});
const mesh = new THREE.Mesh(geo, mat);
mesh.position.set(0, 1.2, 0);
mesh.castShadow = true;
return mesh;
}
function updateAvatarColor(hex) {
currentColor = hex;
if (avatarMesh) {
avatarMesh.material.color.set(hex);
avatarMesh.material.emissive.set(new THREE.Color(hex).multiplyScalar(0.3));
}
document.querySelectorAll('.avatar-color-swatch').forEach(el => {
el.classList.toggle('active', el.dataset.color === hex);
});
savePrefs();
}
function createNameTag(name) {
const div = document.createElement('div');
div.className = 'avatar-name-tag';
div.textContent = name;
document.body.appendChild(div);
return div;
}
function updateNameTagPosition() {
if (!nameTagDiv || !_camera) return;
const pos = new THREE.Vector3(0, 2.4, 0);
if (avatarMesh && avatarMesh.parent) {
pos.add(avatarMesh.parent.position);
}
pos.project(_camera);
const x = (pos.x * 0.5 + 0.5) * window.innerWidth;
const y = (-pos.y * 0.5 + 0.5) * window.innerHeight;
nameTagDiv.style.left = x + 'px';
nameTagDiv.style.top = y + 'px';
nameTagDiv.style.display = pos.z < 1 ? 'block' : 'none';
}
function updateNameTagText(name) {
currentName = name;
if (nameTagDiv) nameTagDiv.textContent = name;
savePrefs();
}
function createColorPicker() {
const panel = document.createElement('div');
panel.id = 'avatar-color-picker';
panel.className = 'avatar-color-picker hidden';
panel.innerHTML = '<div class="avatar-picker-header">' +
'<span>Avatar</span>' +
'<button class="avatar-picker-close">&times;</button></div>' +
'<div class="avatar-picker-name"><label>Name</label>' +
'<input type="text" id="avatar-name-input" maxlength="20" placeholder="Your name" /></div>' +
'<div class="avatar-picker-colors"><label>Color</label>' +
'<div class="avatar-color-grid">' +
PRESET_COLORS.map(c => '<button class="avatar-color-swatch ' +
(c.hex === currentColor ? 'active' : '') +
'" data-color="' + c.hex + '" style="background:' + c.hex +
'" title="' + c.name + '"></button>').join('') +
'</div></div>';
document.body.appendChild(panel);
panel.querySelector('.avatar-picker-close').addEventListener('click', () => {
panel.classList.add('hidden');
});
panel.querySelectorAll('.avatar-color-swatch').forEach(el => {
el.addEventListener('click', () => updateAvatarColor(el.dataset.color));
});
const nameInput = panel.querySelector('#avatar-name-input');
nameInput.value = currentName;
nameInput.addEventListener('input', (e) => {
updateNameTagText(e.target.value || 'Visitor');
});
return panel;
}
function toggleColorPicker() {
if (!colorPickerPanel) return;
colorPickerPanel.classList.toggle('hidden');
const nameInput = colorPickerPanel.querySelector('#avatar-name-input');
if (nameInput && !colorPickerPanel.classList.contains('hidden')) {
nameInput.value = currentName;
nameInput.focus();
}
}
function update(playerPos) {
if (!avatarMesh) return;
avatarMesh.position.set(playerPos.x, playerPos.y - 0.8, playerPos.z);
updateNameTagPosition();
}
function init(sceneRef, cameraRef) {
_scene = sceneRef;
_camera = cameraRef;
loadPrefs();
avatarMesh = createAvatarMesh(currentColor);
_scene.add(avatarMesh);
nameTagDiv = createNameTag(currentName);
colorPickerPanel = createColorPicker();
const hudRight = document.querySelector('.hud-top-right');
if (hudRight) {
const btn = document.createElement('button');
btn.id = 'avatar-customize-btn';
btn.className = 'hud-icon-btn';
btn.title = 'Customize Avatar';
btn.innerHTML = '<span class="hud-icon">🎨</span>';
btn.addEventListener('click', toggleColorPicker);
hudRight.insertBefore(btn, hudRight.firstChild);
}
console.log('[AvatarCustomization] Initialized —', currentColor, currentName);
}
return { init, update, setColor: updateAvatarColor, setName: updateNameTagText, toggleColorPicker };
})();
window.AvatarCustomization = AvatarCustomization;

View File

@@ -23,6 +23,7 @@
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;600;700&family=Orbitron:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
<link rel="stylesheet" href="./style.css">
<link rel="stylesheet" href="./avatar-customization.css">
<link rel="manifest" href="./manifest.json">
<script type="importmap">
{
@@ -395,6 +396,7 @@
<div id="memory-connections-panel" class="memory-connections-panel" style="display:none;" aria-label="Memory Connections Panel"></div>
<script src="./boot.js"></script>
<script src="./avatar-customization.js"></script>
<script>
function openMemoryFilter() { renderFilterList(); document.getElementById('memory-filter').style.display = 'flex'; }
function closeMemoryFilter() { document.getElementById('memory-filter').style.display = 'none'; }