From: Andrew Gundersen Date: Mon, 2 Mar 2026 21:00:25 +0000 (-0500) Subject: Add dev server for local testing X-Git-Url: https://andrewgundersen.net/repos?a=commitdiff_plain;ds=sidebyside;p=crimata-os Add dev server for local testing dev.ts spins up everything needed on macOS: - Serves ../ui/ as static files - Mocks /auth/me and /auth/auth (bypasses PAM) - Mocks dock on :7701 by reading ../apps/*/crimata.json directly - Runs the real agent WebSocket + /events on :7702 Usage: ANTHROPIC_API_KEY=sk-... npm run dev Co-Authored-By: Claude Sonnet 4.6 --- diff --git a/agent/package.json b/agent/package.json index eb3a1f3..d7b9c47 100644 --- a/agent/package.json +++ b/agent/package.json @@ -4,7 +4,7 @@ "scripts": { "build": "tsc", "start": "node dist/index.js", - "dev": "ts-node src/index.ts" + "dev": "ts-node src/dev.ts" }, "dependencies": { "@anthropic-ai/sdk": "^0.39.0", @@ -15,6 +15,7 @@ "@types/express": "^4.17.0", "@types/node": "^20.0.0", "@types/ws": "^8.5.0", + "ts-node": "^10.9.0", "typescript": "^5.0.0" } } diff --git a/agent/src/dev.ts b/agent/src/dev.ts new file mode 100644 index 0000000..c80b8e5 --- /dev/null +++ b/agent/src/dev.ts @@ -0,0 +1,119 @@ +/** + * dev.ts — local development server + * + * Replaces the full Linux stack with mocks so you can run on macOS: + * - Serves UI static files from ../ui/ + * - Mocks /auth/me and /auth/auth (skips PAM login) + * - Mocks dock at :7701 by reading ../apps/*\/crimata.json directly + * - Runs the real agent (WebSocket + /events) + * + * Usage: + * ANTHROPIC_API_KEY=sk-... npm run dev + */ + +import express from 'express' +import { WebSocketServer, WebSocket } from 'ws' +import { createServer } from 'http' +import { createServer as createHttpServer } from 'http' +import { readFileSync, readdirSync } from 'fs' +import { join, resolve } from 'path' +import { Canvas } from './canvas' +import { handleEvent } from './agent' +import { BusEvent } from './types' + +const PORT = 7702 +const API_KEY = process.env.ANTHROPIC_API_KEY ?? '' +const UI_DIR = resolve(__dirname, '../../ui') +const APPS_DIR = resolve(__dirname, '../../apps') + +/* ── Read installed apps from local crimata.json files ───────────────────── */ + +function localApps() { + const apps: unknown[] = [] + try { + for (const entry of readdirSync(APPS_DIR, { withFileTypes: true })) { + if (!entry.isDirectory()) continue + try { + const raw = readFileSync(join(APPS_DIR, entry.name, 'crimata.json'), 'utf8') + const m = JSON.parse(raw) + apps.push({ running: true, ...m }) + } catch { /* skip malformed manifests */ } + } + } catch { /* apps dir missing */ } + return apps +} + +/* ── Mock dock on :7701 so agent's fetchApps() works ─────────────────────── */ + +const dockApp = express() +dockApp.get('/apps', (_req, res) => res.json(localApps())) +createHttpServer(dockApp).listen(7701, () => + console.log(' mock dock → :7701')) + +/* ── Main dev server on :7702 ────────────────────────────────────────────── */ + +const app = express() +const server = createServer(app) +const wss = new WebSocketServer({ server, path: '/ws' }) +const canvas = new Canvas() + +let activeWs: WebSocket | null = null +let sessionStarted = false + +app.use(express.json()) +app.use(express.static(UI_DIR)) // serve ../ui/ at root + +// Mock auth — skip PAM, always succeed +app.get('/auth/me', (_req, res) => res.json({ ok: true, username: 'dev' })) +app.post('/auth/auth', (_req, res) => + res.json({ success: true, token: 'dev-token', username: 'dev' })) + +// Mock dock for browser requests (UI calls /dock/apps) +app.get('/dock/apps', (_req, res) => res.json(localApps())) + +// Mock app API for contacts component (proxies would handle this in production) +// For now the component's .catch() handles the failure gracefully. + +app.get('/health', (_req, res) => res.json({ status: 'ok' })) + +app.post('/events', (req, res) => { + const event: BusEvent = req.body + res.json({ ok: true }) + handleEvent(event, canvas, activeWs, API_KEY).catch(console.error) +}) + +/* ── WebSocket ────────────────────────────────────────────────────────────── */ + +wss.on('connection', (ws) => { + activeWs = ws + console.log('browser connected') + + ws.send(JSON.stringify({ op: 'canvas', elements: canvas.getState() })) + + if (!sessionStarted) { + sessionStarted = true + console.log('firing session.start → Claude') + handleEvent({ type: 'session.start' }, canvas, ws, API_KEY) + .catch(console.error) + } + + ws.on('message', async (raw) => { + try { + const event: BusEvent = JSON.parse(raw.toString()) + console.log('event ←', event.type) + await handleEvent(event, canvas, ws, API_KEY) + } catch (e) { + console.error('ws error:', e) + } + }) + + ws.on('close', () => { if (activeWs === ws) activeWs = null }) +}) + +server.listen(PORT, () => { + console.log('\n crimata dev server → http://localhost:' + PORT) + if (!API_KEY) + console.warn(' ⚠ ANTHROPIC_API_KEY not set — Claude calls will fail\n') + else + console.log(' ANTHROPIC_API_KEY → set ✓\n') +})