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 <noreply@anthropic.com>
This commit is contained in:
parent
31b5e321a1
commit
1fe91d6f75
2 changed files with 121 additions and 1 deletions
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
119
agent/src/dev.ts
Normal file
119
agent/src/dev.ts
Normal file
|
|
@ -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')
|
||||
})
|
||||
Loading…
Reference in a new issue