diff --git a/apps/server/src/index.ts b/apps/server/src/index.ts index e55f55bf..59ebffa7 100644 --- a/apps/server/src/index.ts +++ b/apps/server/src/index.ts @@ -30,7 +30,12 @@ import { createSpecRegenerationRoutes } from "./routes/spec-regeneration.js"; import { createRunningAgentsRoutes } from "./routes/running-agents.js"; import { createWorkspaceRoutes } from "./routes/workspace.js"; import { createTemplatesRoutes } from "./routes/templates.js"; -import { createTerminalRoutes, validateTerminalToken, isTerminalEnabled, isTerminalPasswordRequired } from "./routes/terminal.js"; +import { + createTerminalRoutes, + validateTerminalToken, + isTerminalEnabled, + isTerminalPasswordRequired, +} from "./routes/terminal.js"; import { AgentService } from "./services/agent-service.js"; import { FeatureLoader } from "./services/feature-loader.js"; import { AutoModeService } from "./services/auto-mode-service.js"; @@ -64,7 +69,9 @@ if (!hasAnthropicKey && !hasOAuthToken) { ╚═══════════════════════════════════════════════════════════════════════╝ `); } else if (hasOAuthToken) { - console.log("[Server] ✓ CLAUDE_CODE_OAUTH_TOKEN detected (subscription auth)"); + console.log( + "[Server] ✓ CLAUDE_CODE_OAUTH_TOKEN detected (subscription auth)" + ); } else { console.log("[Server] ✓ ANTHROPIC_API_KEY detected (API key auth)"); } @@ -130,7 +137,10 @@ const terminalService = getTerminalService(); // Handle HTTP upgrade requests manually to route to correct WebSocket server server.on("upgrade", (request, socket, head) => { - const { pathname } = new URL(request.url || "", `http://${request.headers.host}`); + const { pathname } = new URL( + request.url || "", + `http://${request.headers.host}` + ); if (pathname === "/api/events") { wss.handleUpgrade(request, socket, head, (ws) => { @@ -171,139 +181,153 @@ wss.on("connection", (ws: WebSocket) => { const terminalConnections: Map> = new Map(); // Terminal WebSocket connection handler -terminalWss.on("connection", (ws: WebSocket, req: import("http").IncomingMessage) => { - // Parse URL to get session ID and token - const url = new URL(req.url || "", `http://${req.headers.host}`); - const sessionId = url.searchParams.get("sessionId"); - const token = url.searchParams.get("token"); +terminalWss.on( + "connection", + (ws: WebSocket, req: import("http").IncomingMessage) => { + // Parse URL to get session ID and token + const url = new URL(req.url || "", `http://${req.headers.host}`); + const sessionId = url.searchParams.get("sessionId"); + const token = url.searchParams.get("token"); - console.log(`[Terminal WS] Connection attempt for session: ${sessionId}`); + console.log(`[Terminal WS] Connection attempt for session: ${sessionId}`); - // Check if terminal is enabled - if (!isTerminalEnabled()) { - console.log("[Terminal WS] Terminal is disabled"); - ws.close(4003, "Terminal access is disabled"); - return; - } - - // Validate token if password is required - if (isTerminalPasswordRequired() && !validateTerminalToken(token || undefined)) { - console.log("[Terminal WS] Invalid or missing token"); - ws.close(4001, "Authentication required"); - return; - } - - if (!sessionId) { - console.log("[Terminal WS] No session ID provided"); - ws.close(4002, "Session ID required"); - return; - } - - // Check if session exists - const session = terminalService.getSession(sessionId); - if (!session) { - console.log(`[Terminal WS] Session ${sessionId} not found`); - ws.close(4004, "Session not found"); - return; - } - - console.log(`[Terminal WS] Client connected to session ${sessionId}`); - - // Track this connection - if (!terminalConnections.has(sessionId)) { - terminalConnections.set(sessionId, new Set()); - } - terminalConnections.get(sessionId)!.add(ws); - - // Subscribe to terminal data - const unsubscribeData = terminalService.onData((sid, data) => { - if (sid === sessionId && ws.readyState === WebSocket.OPEN) { - ws.send(JSON.stringify({ type: "data", data })); + // Check if terminal is enabled + if (!isTerminalEnabled()) { + console.log("[Terminal WS] Terminal is disabled"); + ws.close(4003, "Terminal access is disabled"); + return; } - }); - // Subscribe to terminal exit - const unsubscribeExit = terminalService.onExit((sid, exitCode) => { - if (sid === sessionId && ws.readyState === WebSocket.OPEN) { - ws.send(JSON.stringify({ type: "exit", exitCode })); - ws.close(1000, "Session ended"); + // Validate token if password is required + if ( + isTerminalPasswordRequired() && + !validateTerminalToken(token || undefined) + ) { + console.log("[Terminal WS] Invalid or missing token"); + ws.close(4001, "Authentication required"); + return; } - }); - // Handle incoming messages - ws.on("message", (message) => { - try { - const msg = JSON.parse(message.toString()); + if (!sessionId) { + console.log("[Terminal WS] No session ID provided"); + ws.close(4002, "Session ID required"); + return; + } - switch (msg.type) { - case "input": - // Write user input to terminal - terminalService.write(sessionId, msg.data); - break; + // Check if session exists + const session = terminalService.getSession(sessionId); + if (!session) { + console.log(`[Terminal WS] Session ${sessionId} not found`); + ws.close(4004, "Session not found"); + return; + } - case "resize": - // Resize terminal - if (msg.cols && msg.rows) { - terminalService.resize(sessionId, msg.cols, msg.rows); - } - break; + console.log(`[Terminal WS] Client connected to session ${sessionId}`); - case "ping": - // Respond to ping - ws.send(JSON.stringify({ type: "pong" })); - break; + // Track this connection + if (!terminalConnections.has(sessionId)) { + terminalConnections.set(sessionId, new Set()); + } + terminalConnections.get(sessionId)!.add(ws); - default: - console.warn(`[Terminal WS] Unknown message type: ${msg.type}`); + // Subscribe to terminal data + const unsubscribeData = terminalService.onData((sid, data) => { + if (sid === sessionId && ws.readyState === WebSocket.OPEN) { + ws.send(JSON.stringify({ type: "data", data })); } - } catch (error) { - console.error("[Terminal WS] Error processing message:", error); - } - }); + }); - ws.on("close", () => { - console.log(`[Terminal WS] Client disconnected from session ${sessionId}`); - unsubscribeData(); - unsubscribeExit(); - - // Remove from connections tracking - const connections = terminalConnections.get(sessionId); - if (connections) { - connections.delete(ws); - if (connections.size === 0) { - terminalConnections.delete(sessionId); + // Subscribe to terminal exit + const unsubscribeExit = terminalService.onExit((sid, exitCode) => { + if (sid === sessionId && ws.readyState === WebSocket.OPEN) { + ws.send(JSON.stringify({ type: "exit", exitCode })); + ws.close(1000, "Session ended"); } + }); + + // Handle incoming messages + ws.on("message", (message) => { + try { + const msg = JSON.parse(message.toString()); + + switch (msg.type) { + case "input": + // Write user input to terminal + terminalService.write(sessionId, msg.data); + break; + + case "resize": + // Resize terminal + if (msg.cols && msg.rows) { + terminalService.resize(sessionId, msg.cols, msg.rows); + } + break; + + case "ping": + // Respond to ping + ws.send(JSON.stringify({ type: "pong" })); + break; + + default: + console.warn(`[Terminal WS] Unknown message type: ${msg.type}`); + } + } catch (error) { + console.error("[Terminal WS] Error processing message:", error); + } + }); + + ws.on("close", () => { + console.log( + `[Terminal WS] Client disconnected from session ${sessionId}` + ); + unsubscribeData(); + unsubscribeExit(); + + // Remove from connections tracking + const connections = terminalConnections.get(sessionId); + if (connections) { + connections.delete(ws); + if (connections.size === 0) { + terminalConnections.delete(sessionId); + } + } + }); + + ws.on("error", (error) => { + console.error(`[Terminal WS] Error on session ${sessionId}:`, error); + unsubscribeData(); + unsubscribeExit(); + }); + + // Send initial connection success + ws.send( + JSON.stringify({ + type: "connected", + sessionId, + shell: session.shell, + cwd: session.cwd, + }) + ); + + // Send scrollback buffer to replay previous output + const scrollback = terminalService.getScrollback(sessionId); + if (scrollback && scrollback.length > 0) { + ws.send( + JSON.stringify({ + type: "scrollback", + data: scrollback, + }) + ); } - }); - - ws.on("error", (error) => { - console.error(`[Terminal WS] Error on session ${sessionId}:`, error); - unsubscribeData(); - unsubscribeExit(); - }); - - // Send initial connection success - ws.send(JSON.stringify({ - type: "connected", - sessionId, - shell: session.shell, - cwd: session.cwd, - })); - - // Send scrollback buffer to replay previous output - const scrollback = terminalService.getScrollback(sessionId); - if (scrollback && scrollback.length > 0) { - ws.send(JSON.stringify({ - type: "scrollback", - data: scrollback, - })); } -}); +); // Start server server.listen(PORT, () => { const terminalStatus = isTerminalEnabled() - ? (isTerminalPasswordRequired() ? "enabled (password protected)" : "enabled") + ? isTerminalPasswordRequired() + ? "enabled (password protected)" + : "enabled" : "disabled"; console.log(` ╔═══════════════════════════════════════════════════════╗ diff --git a/apps/server/src/services/agent-service.ts b/apps/server/src/services/agent-service.ts index b1c8cd1b..9a58940a 100644 --- a/apps/server/src/services/agent-service.ts +++ b/apps/server/src/services/agent-service.ts @@ -9,9 +9,7 @@ import fs from "fs/promises"; import type { EventEmitter } from "../lib/events.js"; import { ProviderFactory } from "../providers/provider-factory.js"; import type { ExecuteOptions } from "../providers/types.js"; -import { - readImageAsBase64, -} from "../lib/image-handler.js"; +import { readImageAsBase64 } from "../lib/image-handler.js"; import { buildPromptWithImages } from "../lib/prompt-builder.js"; import { getEffectiveModel } from "../lib/model-resolver.js"; import { isAbortError } from "../lib/error-handler.js"; @@ -136,7 +134,10 @@ export class AgentService { filename: imageData.filename, }); } catch (error) { - console.error(`[AgentService] Failed to load image ${imagePath}:`, error); + console.error( + `[AgentService] Failed to load image ${imagePath}:`, + error + ); } } } @@ -197,7 +198,8 @@ export class AgentService { "WebFetch", ], abortController: session.abortController!, - conversationHistory: conversationHistory.length > 0 ? conversationHistory : undefined, + conversationHistory: + conversationHistory.length > 0 ? conversationHistory : undefined, }; // Build prompt content with images @@ -381,7 +383,11 @@ export class AgentService { const sessionFile = path.join(this.stateDir, `${sessionId}.json`); try { - await fs.writeFile(sessionFile, JSON.stringify(messages, null, 2), "utf-8"); + await fs.writeFile( + sessionFile, + JSON.stringify(messages, null, 2), + "utf-8" + ); await this.updateSessionTimestamp(sessionId); } catch (error) { console.error("[AgentService] Failed to save session:", error); @@ -398,7 +404,11 @@ export class AgentService { } async saveMetadata(metadata: Record): Promise { - await fs.writeFile(this.metadataFile, JSON.stringify(metadata, null, 2), "utf-8"); + await fs.writeFile( + this.metadataFile, + JSON.stringify(metadata, null, 2), + "utf-8" + ); } async updateSessionTimestamp(sessionId: string): Promise { @@ -418,7 +428,8 @@ export class AgentService { } return sessions.sort( - (a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime() + (a, b) => + new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime() ); } @@ -505,7 +516,10 @@ export class AgentService { return true; } - private emitAgentEvent(sessionId: string, data: Record): void { + private emitAgentEvent( + sessionId: string, + data: Record + ): void { this.events.emit("agent:stream", { sessionId, ...data }); } diff --git a/package.json b/package.json index 1d1e4b85..6ef56317 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "libs/*" ], "scripts": { + "postinstall": "node -e \"const fs=require('fs');if(process.platform==='darwin'){['darwin-arm64','darwin-x64'].forEach(a=>{const p='node_modules/node-pty/prebuilds/'+a+'/spawn-helper';if(fs.existsSync(p))fs.chmodSync(p,0o755)})}\"", "dev": "npm run dev --workspace=apps/app", "dev:web": "npm run dev:web --workspace=apps/app", "dev:electron": "npm run dev:electron --workspace=apps/app",