From 7272eb92749f39cf67177ddaec8c2fbd8e969b79 Mon Sep 17 00:00:00 2001
From: Mario Zechner
Date: Sun, 14 Dec 2025 23:36:26 +0000
Subject: [PATCH] Add artifacts server documentation
---
packages/mom/README.md | 7 +
packages/mom/docs/artifacts-server.md | 475 ++++++++++++++++++++++++++
2 files changed, 482 insertions(+)
create mode 100644 packages/mom/docs/artifacts-server.md
diff --git a/packages/mom/README.md b/packages/mom/README.md
index 4114493e..63d3488e 100644
--- a/packages/mom/README.md
+++ b/packages/mom/README.md
@@ -13,6 +13,13 @@ A Slack bot powered by an LLM that can execute bash commands, read/write files,
- **Working Memory & Custom Tools**: Mom remembers context across sessions and creates workflow-specific CLI tools ([aka "skills"](https://mariozechner.at/posts/2025-11-02-what-if-you-dont-need-mcp/)) for your tasks
- **Thread-Based Details**: Clean main messages with verbose tool details in threads
+## Documentation
+
+- [Artifacts Server](docs/artifacts-server.md) - Share HTML/JS visualizations publicly with live reload
+- [Events System](docs/events.md) - Schedule reminders and periodic tasks
+- [Sandbox Guide](docs/sandbox.md) - Docker vs host mode security
+- [Slack Bot Setup](docs/slack-bot-minimal-guide.md) - Minimal Slack integration guide
+
## Installation
```bash
diff --git a/packages/mom/docs/artifacts-server.md b/packages/mom/docs/artifacts-server.md
new file mode 100644
index 00000000..3a538b34
--- /dev/null
+++ b/packages/mom/docs/artifacts-server.md
@@ -0,0 +1,475 @@
+# Artifacts Server
+
+Share HTML files, visualizations, and interactive demos publicly via Cloudflare Tunnel with live reload support.
+
+## What is it?
+
+The artifacts server lets Mom create HTML/JS/CSS files that you can instantly view in a browser, with WebSocket-based live reload for development. Perfect for dashboards, visualizations, prototypes, and interactive demos.
+
+## Installation
+
+### 1. Install Dependencies
+
+**Node.js packages:**
+```bash
+cd /workspace/artifacts
+npm init -y
+npm install express ws chokidar
+```
+
+**Cloudflared (Cloudflare Tunnel):**
+```bash
+wget -q https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64
+mv cloudflared-linux-amd64 /usr/local/bin/cloudflared
+chmod +x /usr/local/bin/cloudflared
+cloudflared --version
+```
+
+### 2. Create Server
+
+Save this as `/workspace/artifacts/server.js`:
+
+```javascript
+#!/usr/bin/env node
+
+const express = require('express');
+const { WebSocketServer } = require('ws');
+const chokidar = require('chokidar');
+const path = require('path');
+const fs = require('fs');
+const http = require('http');
+
+const PORT = 8080;
+const FILES_DIR = path.join(__dirname, 'files');
+
+// Ensure files directory exists
+if (!fs.existsSync(FILES_DIR)) {
+ fs.mkdirSync(FILES_DIR, { recursive: true });
+}
+
+const app = express();
+const server = http.createServer(app);
+const wss = new WebSocketServer({ server, clientTracking: true });
+
+// Track connected WebSocket clients
+const clients = new Set();
+
+// WebSocket connection handler with error handling
+wss.on('connection', (ws) => {
+ console.log('WebSocket client connected');
+ clients.add(ws);
+
+ ws.on('error', (err) => {
+ console.error('WebSocket client error:', err.message);
+ clients.delete(ws);
+ });
+
+ ws.on('close', () => {
+ console.log('WebSocket client disconnected');
+ clients.delete(ws);
+ });
+});
+
+wss.on('error', (err) => {
+ console.error('WebSocket server error:', err.message);
+});
+
+// Watch for file changes
+const watcher = chokidar.watch(FILES_DIR, {
+ persistent: true,
+ ignoreInitial: true,
+ depth: 99, // Watch all subdirectory levels
+ ignorePermissionErrors: true,
+ awaitWriteFinish: {
+ stabilityThreshold: 100,
+ pollInterval: 50
+ }
+});
+
+watcher.on('all', (event, filepath) => {
+ console.log(`File ${event}: ${filepath}`);
+
+ // If a new directory is created, explicitly watch it
+ // This ensures newly created artifact folders are monitored without restart
+ if (event === 'addDir') {
+ watcher.add(filepath);
+ console.log(`Now watching directory: ${filepath}`);
+ }
+
+ const relativePath = path.relative(FILES_DIR, filepath);
+ const message = JSON.stringify({
+ type: 'reload',
+ file: relativePath
+ });
+
+ clients.forEach(client => {
+ if (client.readyState === 1) {
+ try {
+ client.send(message);
+ } catch (err) {
+ console.error('Error sending to client:', err.message);
+ clients.delete(client);
+ }
+ } else {
+ clients.delete(client);
+ }
+ });
+});
+
+watcher.on('error', (err) => {
+ console.error('File watcher error:', err.message);
+});
+
+// Cache-busting headers
+app.use((req, res, next) => {
+ res.set({
+ 'Cache-Control': 'no-store, no-cache, must-revalidate, proxy-revalidate',
+ 'Pragma': 'no-cache',
+ 'Expires': '0',
+ 'Surrogate-Control': 'no-store'
+ });
+ next();
+});
+
+// Inject live reload script for HTML files with ?ws=true
+app.use((req, res, next) => {
+ if (!req.path.endsWith('.html') || req.query.ws !== 'true') {
+ return next();
+ }
+
+ const filePath = path.join(FILES_DIR, req.path);
+
+ // Security: Prevent path traversal attacks
+ const resolvedPath = path.resolve(filePath);
+ const resolvedBase = path.resolve(FILES_DIR);
+ if (!resolvedPath.startsWith(resolvedBase)) {
+ return res.status(403).send('Forbidden: Path traversal detected');
+ }
+
+ fs.readFile(filePath, 'utf8', (err, data) => {
+ if (err) {
+ return next();
+ }
+
+ const liveReloadScript = `
+`;
+
+ if (data.includes('
+ My Dashboard
+
+')) {
+ data = data.replace('', liveReloadScript + '');
+ } else {
+ data = data + liveReloadScript;
+ }
+
+ res.type('html').send(data);
+ });
+});
+
+// Serve static files
+app.use(express.static(FILES_DIR));
+
+// Error handling
+app.use((err, req, res, next) => {
+ console.error('Express error:', err.message);
+ res.status(500).send('Internal server error');
+});
+
+server.on('error', (err) => {
+ if (err.code === 'EADDRINUSE') {
+ console.error(`Port ${PORT} is already in use`);
+ process.exit(1);
+ } else {
+ console.error('Server error:', err.message);
+ }
+});
+
+// Global error handlers
+process.on('uncaughtException', (err) => {
+ console.error('Uncaught exception:', err);
+});
+
+process.on('unhandledRejection', (reason) => {
+ console.error('Unhandled rejection:', reason);
+});
+
+// Graceful shutdown
+process.on('SIGTERM', () => {
+ console.log('SIGTERM received, closing gracefully');
+ watcher.close();
+ server.close(() => process.exit(0));
+});
+
+process.on('SIGINT', () => {
+ console.log('SIGINT received, closing gracefully');
+ watcher.close();
+ server.close(() => process.exit(0));
+});
+
+// Start server
+server.listen(PORT, () => {
+ console.log(`Artifacts server running on http://localhost:${PORT}`);
+ console.log(`Serving files from: ${FILES_DIR}`);
+ console.log(`Add ?ws=true to any URL for live reload`);
+});
+```
+
+Make executable:
+```bash
+chmod +x /workspace/artifacts/server.js
+```
+
+### 3. Create Startup Script
+
+Save this as `/workspace/artifacts/start-server.sh`:
+
+```bash
+#!/bin/sh
+set -e
+
+SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
+cd "$SCRIPT_DIR"
+
+echo "Starting artifacts server..."
+
+# Start Node.js server in background
+node server.js > /tmp/server.log 2>&1 &
+NODE_PID=$!
+
+# Wait for server to be ready
+sleep 2
+
+# Start cloudflare tunnel
+echo "Starting Cloudflare Tunnel..."
+cloudflared tunnel --url http://localhost:8080 2>&1 | tee /tmp/cloudflared.log &
+TUNNEL_PID=$!
+
+# Wait for tunnel to establish
+sleep 5
+
+# Extract and display public URL
+PUBLIC_URL=$(grep -o 'https://.*\.trycloudflare\.com' /tmp/cloudflared.log | head -1)
+
+if [ -n "$PUBLIC_URL" ]; then
+ echo ""
+ echo "=========================================="
+ echo "Artifacts server is running!"
+ echo "=========================================="
+ echo "Public URL: $PUBLIC_URL"
+ echo "Files directory: $SCRIPT_DIR/files/"
+ echo ""
+ echo "Add ?ws=true to any URL for live reload"
+ echo "Example: $PUBLIC_URL/test.html?ws=true"
+ echo "=========================================="
+ echo ""
+
+ echo "$PUBLIC_URL" > /tmp/artifacts-url.txt
+else
+ echo "Warning: Could not extract public URL"
+fi
+
+# Keep script running
+cleanup() {
+ echo "Shutting down..."
+ kill $NODE_PID 2>/dev/null || true
+ kill $TUNNEL_PID 2>/dev/null || true
+ exit 0
+}
+
+trap cleanup INT TERM
+wait $NODE_PID $TUNNEL_PID
+```
+
+Make executable:
+```bash
+chmod +x /workspace/artifacts/start-server.sh
+```
+
+## Directory Structure
+
+```
+/workspace/artifacts/
+├── server.js # Node.js server
+├── start-server.sh # Startup script
+├── package.json # Dependencies
+├── node_modules/ # Installed packages
+└── files/ # PUT YOUR ARTIFACTS HERE
+ ├── 2025-12-14-demo/
+ │ ├── index.html
+ │ ├── style.css
+ │ └── logo.png
+ ├── 2025-12-15-chart/
+ │ └── index.html
+ └── test.html (standalone OK)
+```
+
+## Usage
+
+### Starting the Server
+
+```bash
+cd /workspace/artifacts
+./start-server.sh
+```
+
+This will:
+1. Start Node.js server on localhost:8080
+2. Create Cloudflare Tunnel with public URL
+3. Print the URL (e.g., `https://random-words-123.trycloudflare.com`)
+4. Save URL to `/tmp/artifacts-url.txt`
+
+**Note:** URL changes every time you restart (free Cloudflare Tunnel limitation).
+
+### Creating Artifacts
+
+**Folder organization:**
+- Create one subfolder per artifact: `$(date +%Y-%m-%d)-description/`
+- Put main file as `index.html` for clean URLs
+- Include images, CSS, JS, data in same folder
+- CDN resources (Tailwind, Three.js, etc.) work fine
+
+**Example:**
+```bash
+mkdir -p /workspace/artifacts/files/$(date +%Y-%m-%d)-dashboard
+cat > /workspace/artifacts/files/$(date +%Y-%m-%d)-dashboard/index.html << 'EOF'
+
+
+