package main import ( "encoding/json" "flag" "fmt" "log" "net/http" "os/exec" "strings" "sync" "time" "github.com/gorilla/websocket" ) var upgrader = websocket.Upgrader{ ReadBufferSize: 1024, WriteBufferSize: 1024, CheckOrigin: func(r *http.Request) bool { return true // Allow all origins for now }, } type Message struct { ID int64 `json:"id"` Sender string `json:"sender"` SenderType string `json:"sender_type"` // "ai", "human", "system" Content string `json:"content"` Timestamp int64 `json:"timestamp"` } type Client struct { conn *websocket.Conn sender string senderType string } var ( clients = make(map[*Client]bool) clientsMu sync.RWMutex messages []Message messagesMu sync.RWMutex maxMessages = 1000 ) const ( port = 1985 tailscaleIP = "100.85.192.60" // Only accessible via Tailscale openClawURL = "http://127.0.0.1:18789/tools/invoke" openClawToken = "601267edaccf8cd3d6afe222c3ce63602e210ff1ecc9a268" ) func main() { flag.Parse() // HTTP endpoints http.HandleFunc("/", handleHTML) http.HandleFunc("/chat.js", handleJS) http.HandleFunc("/api/send", handleSend) http.HandleFunc("/api/messages", handleMessages) http.HandleFunc("/api/status", handleStatus) // WebSocket http.HandleFunc("/ws", handleWS) addr := fmt.Sprintf("100.85.192.60:%d", port) // Bind to Tailscale IP only fmt.Printf("Vault1984 Chat v0.2 (Go+WS) running on http://localhost:%d\n", port) log.Fatal(http.ListenAndServe(addr, nil)) } func handleHTML(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/html") w.Write([]byte(htmlPage)) } func handleJS(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/javascript") w.Write([]byte(jsPage)) } func handleSend(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { http.Error(w, "Method not allowed", 405) return } var msg Message if err := json.NewDecoder(r.Body).Decode(&msg); err != nil { http.Error(w, "Invalid JSON", 400) return } if msg.Sender == "" || msg.Content == "" { http.Error(w, "Missing sender or content", 400) return } if msg.SenderType == "" { msg.SenderType = "ai" } msg.Timestamp = time.Now().UnixMilli() msg.ID = msg.Timestamp messagesMu.Lock() messages = append([]Message{msg}, messages...) if len(messages) > maxMessages { messages = messages[:maxMessages] } messagesMu.Unlock() // Broadcast to all connected WS clients broadcast(msg) // If it's a human message, respond via OpenClaw agent if msg.SenderType == "human" { log.Printf("Calling OpenClaw for message from %s: %s", msg.Sender, msg.Content) // Call OpenClaw to get real response (synchronous - wait for it) response := callOpenClaw(msg.Content, msg.Sender) // Add the response to messages responseMsg := Message{ ID: time.Now().UnixMilli(), Sender: "Hans", SenderType: "ai", Content: response, Timestamp: time.Now().UnixMilli(), } messagesMu.Lock() messages = append([]Message{responseMsg}, messages...) if len(messages) > maxMessages { messages = messages[:maxMessages] } messagesMu.Unlock() broadcast(responseMsg) } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "success": true, "message": msg, }) } func handleMessages(w http.ResponseWriter, r *http.Request) { since := 0 if s := r.URL.Query().Get("since"); s != "" { fmt.Sscanf(s, "%d", &since) } messagesMu.RLock() var filtered []Message for _, m := range messages { if int64(since) < m.Timestamp { filtered = append(filtered, m) } } messagesMu.RUnlock() w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(filtered) } func handleStatus(w http.ResponseWriter, r *http.Request) { clientsMu.RLock() defer clientsMu.RUnlock() w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "status": "online", "messages_count": len(messages), "connected_count": len(clients), "uptime": "N/A", // Could track startup time }) } func handleWS(w http.ResponseWriter, r *http.Request) { conn, err := upgrader.Upgrade(w, r, nil) if err != nil { log.Println("WS upgrade error:", err) return } client := &Client{conn: conn, sender: "unknown", senderType: "human"} clientsMu.Lock() clients[client] = true clientsMu.Unlock() // Send recent messages to new client (last 20) messagesMu.RLock() recent := messages if len(recent) > 20 { recent = recent[:20] } messagesMu.RUnlock() for _, m := range recent { client.sendJSON(map[string]interface{}{ "type": "history", "messages": []Message{m}, }) } // Handle incoming messages for { _, data, err := conn.ReadMessage() if err != nil { break } var msg map[string]interface{} if err := json.Unmarshal(data, &msg); err != nil { continue } if msg["type"] == "register" { if s, ok := msg["sender"].(string); ok { client.sender = s } if st, ok := msg["sender_type"].(string); ok { client.senderType = st } client.sendJSON(map[string]string{"type": "registered", "sender": client.sender}) } if msg["type"] == "ping" { client.sendJSON(map[string]string{"type": "pong"}) } // Handle message sent via WS log.Printf("WS message received: %v", msg) if content, ok := msg["content"].(string); ok && content != "" { sender := client.sender senderType := client.senderType if s, ok := msg["sender"].(string); ok && s != "" { sender = s } if st, ok := msg["sender_type"].(string); ok && st != "" { senderType = st } log.Printf("Saving message from %s: %s", sender, content) // Save and broadcast the user's message msgObj := Message{ ID: time.Now().UnixMilli(), Sender: sender, SenderType: senderType, Content: content, Timestamp: time.Now().UnixMilli(), } messagesMu.Lock() messages = append([]Message{msgObj}, messages...) if len(messages) > maxMessages { messages = messages[:maxMessages] } messagesMu.Unlock() broadcast(msgObj) // If it's a human message, call OpenClaw and broadcast response if senderType == "human" { go func() { response := callOpenClaw(content, sender) responseMsg := Message{ ID: time.Now().UnixMilli(), Sender: "Hans", SenderType: "ai", Content: response, Timestamp: time.Now().UnixMilli(), } messagesMu.Lock() messages = append([]Message{responseMsg}, messages...) if len(messages) > maxMessages { messages = messages[:maxMessages] } messagesMu.Unlock() broadcast(responseMsg) }() } } } clientsMu.Lock() delete(clients, client) clientsMu.Unlock() conn.Close() } func (c *Client) sendJSON(v interface{}) { if err := c.conn.WriteJSON(v); err != nil { log.Println("WS send error:", err) } } func broadcast(msg Message) { clientsMu.RLock() defer clientsMu.RUnlock() for client := range clients { client.sendJSON(map[string]interface{}{ "type": "message", "id": msg.ID, "sender": msg.Sender, "sender_type": msg.SenderType, "content": msg.Content, "timestamp": msg.Timestamp, }) } } // callOpenClaw sends a message to OpenClaw and returns a real response func callOpenClaw(content, sender string) string { // Use local agent - will load model (~10s) but works // This is the fastest we can get with CLI without Gateway integration cmd := exec.Command("openclaw", "agent", "--message", content, "--session-id", fmt.Sprintf("chat-%d", time.Now().UnixMilli()), "--local", "--timeout", "15") output, err := cmd.Output() if err != nil { log.Printf("OpenClaw agent error: %v, output: %s", err, string(output)) return "📩 Received: " + content } // Extract the response from the output response := strings.TrimSpace(string(output)) // Clean up the response - take the first meaningful line(s) lines := strings.Split(response, "\n") var cleanResponse string for _, line := range lines { line = strings.TrimSpace(line) if line != "" && !strings.HasPrefix(line, "{") && !strings.HasPrefix(line, "[") { cleanResponse = line break } } if cleanResponse == "" { cleanResponse = "📩 Received: " + content } // Truncate if too long if len(cleanResponse) > 500 { cleanResponse = cleanResponse[:500] + "..." } log.Printf("Agent response: %s", cleanResponse) return cleanResponse } const htmlPage = "\n\n\n \n \n Vault1984 — AI Chat\n \n\n\n
\n
\n

🔒 VAULT1984 // CHAT v0.3

\n
● CONNECTED
\n
\n
\n
\n \n \n
\n
\n \n\n" const jsPage = "let ws;\nlet reconnectAttempts = 0;\nconst maxReconnectAttempts = 10;\nconst reconnectDelay = 1000;\n\nfunction getSenderClass(sender, senderType) {\n if (!sender) return 'system';\n const s = sender.toLowerCase();\n if (s === 'johan') return 'johan';\n if (s === 'hans') return 'hans';\n if (s === 'james') return 'james';\n if (senderType === 'human') return 'human';\n return 'ai';\n}\n\nfunction connect() {\n ws = new WebSocket('ws://' + location.host + '/ws');\n \n ws.onopen = () => {\n document.getElementById('status').className = 'status online';\n document.getElementById('status').textContent = '● CONNECTED';\n reconnectAttempts = 0;\n ws.send(JSON.stringify({ type: 'register', sender: 'Johan', sender_type: 'human' }));\n };\n \n ws.onmessage = (e) => {\n const data = JSON.parse(e.data);\n if (data.type === 'history' && data.messages) {\n // Sort by timestamp (oldest first)\n data.messages.sort((a, b) => a.timestamp - b.timestamp);\n data.messages.forEach(addMessage);\n } else if (data.type === 'message') {\n addMessage(data);\n }\n };\n \n ws.onclose = () => {\n document.getElementById('status').className = 'status';\n document.getElementById('status').textContent = '○ DISCONNECTED';\n if (reconnectAttempts < maxReconnectAttempts) {\n reconnectAttempts++;\n setTimeout(connect, reconnectDelay * reconnectAttempts);\n }\n };\n}\n\nfunction addMessage(msg) {\n const div = document.createElement('div');\n const senderClass = getSenderClass(msg.sender, msg.sender_type);\n div.className = 'message ' + senderClass;\n \n let content = msg.content || '';\n content = content.replace(/`([^`]+)`/g, '$1');\n content = content.replace(/\\*\\*([^*]+)\\*\\*/g, '$1');\n \n div.innerHTML = '
' + (msg.sender || 'System') + '
' + content + '
' + new Date(msg.timestamp).toLocaleString() + '
';\n document.getElementById('chat').appendChild(div);\n document.getElementById('chat').scrollTop = document.getElementById('chat').scrollHeight;\n}\n\nfunction send() {\n const input = document.getElementById('msg');\n const content = input.value.trim();\n if (!content || !ws || ws.readyState !== 1) return;\n ws.send(JSON.stringify({ sender: 'Johan', sender_type: 'human', content: content }));\n input.value = '';\n}\n\ndocument.getElementById('msg').addEventListener('keypress', (e) => { if (e.key === 'Enter') send(); });\nsetInterval(() => { if (ws && ws.readyState === 1) ws.send(JSON.stringify({ type: 'ping' })); }, 30000);\nconnect();"