#!/usr/bin/env node /** * Patch the OpenNext worker to export Durable Objects * Directly inlines the DO code into the worker */ const fs = require('fs') const path = require('path') console.log('🔨 Patching worker to export Durable Object...') const workerPath = path.join(__dirname, '../.open-next/worker.js') const doPath = path.join(__dirname, '../src/lib/durable-objects/BanditAgentDO.ts') if (!fs.existsSync(workerPath)) { console.error('❌ Worker file not found at:', workerPath) process.exit(1) } if (!fs.existsSync(doPath)) { console.error('❌ Durable Object file not found at:', doPath) process.exit(1) } // Read worker file let workerContent = fs.readFileSync(workerPath, 'utf-8') // Check if already patched if (workerContent.includes('export class BanditAgentDO')) { console.log('✅ Worker already patched, skipping') process.exit(0) } // Read the DO source (not used, but keep for reference) const doSource = fs.readFileSync(doPath, 'utf-8') // Create the DO class inline (minimal working version) const doCode = ` // ===== Durable Object: BanditAgentDO ===== export class BanditAgentDO { constructor(ctx, env) { this.ctx = ctx; this.env = env; this.state = null; this.isRunning = false; } async fetch(request) { try { const url = new URL(request.url); const pathname = url.pathname; // Handle WebSocket upgrade using Hibernatable WebSockets API if (request.headers.get("Upgrade") === "websocket") { const pair = new WebSocketPair(); const [client, server] = Object.values(pair); // Use modern Hibernatable WebSockets API this.ctx.acceptWebSocket(server); return new Response(null, { status: 101, webSocket: client }); } // Handle HTTP requests if (pathname.endsWith('/start')) { const body = await request.json(); // Initialize state this.state = { runId: body.runId, modelName: body.modelName, status: 'running', currentLevel: body.startLevel || 0, targetLevel: body.endLevel || 33 }; // Save to storage await this.ctx.storage.put('state', this.state); // Broadcast to WebSocket clients this.broadcast({ type: 'agent_message', data: { content: \`Run started: \${body.modelName} - Levels \${body.startLevel}-\${body.endLevel}\`, }, timestamp: new Date().toISOString() }); // Start agent execution in background this.runAgent().catch(err => console.error('Agent error:', err)); return new Response(JSON.stringify({ success: true, runId: body.runId, state: this.state }), { headers: { 'Content-Type': 'application/json' } }); } if (pathname.endsWith('/pause')) { if (this.state) { this.state.status = 'paused'; this.isRunning = false; await this.ctx.storage.put('state', this.state); } return new Response(JSON.stringify({ success: true, state: this.state }), { headers: { 'Content-Type': 'application/json' } }); } if (pathname.endsWith('/resume')) { if (this.state) { this.state.status = 'running'; this.isRunning = true; await this.ctx.storage.put('state', this.state); this.runAgent().catch(err => console.error('Agent error:', err)); } return new Response(JSON.stringify({ success: true, state: this.state }), { headers: { 'Content-Type': 'application/json' } }); } if (pathname.endsWith('/status')) { return new Response(JSON.stringify({ state: this.state, isRunning: this.isRunning, connectedClients: this.ctx.getWebSockets().length }), { headers: { 'Content-Type': 'application/json' } }); } return new Response('Not found', { status: 404 }); } catch (error) { console.error('DO fetch error:', error); return new Response(JSON.stringify({ error: error.message }), { status: 500, headers: { 'Content-Type': 'application/json' } }); } } // Hibernatable WebSockets API handlers async webSocketMessage(ws, message) { try { if (typeof message !== 'string') return; const data = JSON.parse(message); if (data.type === 'ping') { ws.send(JSON.stringify({ type: 'pong', timestamp: new Date().toISOString() })); } } catch (error) { console.error('WebSocket message error:', error); } } async webSocketClose(ws, code, reason, wasClean) { console.log(\`WebSocket closed: Code \${code}, Reason: \${reason}, Clean: \${wasClean}\`); } async webSocketError(ws, error) { console.error('WebSocket error:', error); } async runAgent() { if (!this.state) return; this.isRunning = true; try { // Call SSH proxy agent endpoint const response = await fetch(\`\${this.env.SSH_PROXY_URL}/agent/run\`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ runId: this.state.runId, modelName: this.state.modelName, startLevel: this.state.currentLevel, endLevel: this.state.targetLevel, apiKey: this.env.OPENROUTER_API_KEY }) }); // Stream agent events const reader = response.body.getReader(); const decoder = new TextDecoder(); while (true) { const { done, value } = await reader.read(); if (done) break; const chunk = decoder.decode(value); const lines = chunk.split('\\n').filter(l => l.trim()); for (const line of lines) { try { const event = JSON.parse(line); this.broadcast(event); // Update state based on events if (event.type === 'level_complete') { this.state.currentLevel = event.data.level + 1; } if (event.type === 'run_complete') { this.state.status = 'complete'; this.isRunning = false; } if (event.type === 'error') { this.state.status = 'failed'; this.state.error = event.data.content; this.isRunning = false; } } catch (e) { // Ignore parse errors } } } } catch (error) { this.state.status = 'failed'; this.state.error = error.message; this.isRunning = false; this.broadcast({ type: 'error', data: { content: error.message }, timestamp: new Date().toISOString() }); } } broadcast(event) { const message = JSON.stringify(event); const sockets = this.ctx.getWebSockets(); console.log(\`Broadcasting \${event.type} to \${sockets.length} clients\`); for (const socket of sockets) { try { socket.send(message); } catch (error) { console.error('Broadcast error:', error); } } } async alarm() { // Cleanup after 2 hours if (!this.isRunning && this.state) { const startedAt = new Date(this.state.startedAt || 0).getTime(); if (Date.now() - startedAt > 2 * 60 * 60 * 1000) { await this.ctx.storage.deleteAll(); this.state = null; } } await this.ctx.storage.setAlarm(Date.now() + 60 * 60 * 1000); } } // ===== End Durable Object ===== ` // Insert DO code right after the other DO exports // Find the line with "export { BucketCachePurge }" const bucketCacheLine = 'export { BucketCachePurge } from "./.build/durable-objects/bucket-cache-purge.js";' const insertIndex = workerContent.indexOf(bucketCacheLine) if (insertIndex === -1) { console.error('❌ Could not find insertion point in worker.js') process.exit(1) } // Insert right after that line const insertPosition = insertIndex + bucketCacheLine.length // Add __name polyfill at the very beginning const polyfill = ` // Polyfill for esbuild __name helper globalThis.__name = globalThis.__name || function(fn, name) { return fn }; ` const patchedContent = polyfill + '\n' + workerContent.slice(0, insertPosition) + '\n' + doCode + '\n' + workerContent.slice(insertPosition) // Write back fs.writeFileSync(workerPath, patchedContent, 'utf-8') console.log('✅ Worker patched successfully - BanditAgentDO exported') console.log('📝 Note: Using stub DO implementation. Full LangGraph integration via SSH proxy.')