fixing file uploads on context page

This commit is contained in:
Test User
2025-12-21 23:44:26 -05:00
parent 17c69ea1ca
commit e2718b37e3
22 changed files with 2808 additions and 1693 deletions

View File

@@ -6,53 +6,54 @@
* In web mode, this server runs on a remote host.
*/
import express from "express";
import cors from "cors";
import morgan from "morgan";
import { WebSocketServer, WebSocket } from "ws";
import { createServer } from "http";
import dotenv from "dotenv";
import express from 'express';
import cors from 'cors';
import morgan from 'morgan';
import { WebSocketServer, WebSocket } from 'ws';
import { createServer } from 'http';
import dotenv from 'dotenv';
import { createEventEmitter, type EventEmitter } from "./lib/events.js";
import { initAllowedPaths } from "@automaker/platform";
import { authMiddleware, getAuthStatus } from "./lib/auth.js";
import { createFsRoutes } from "./routes/fs/index.js";
import { createHealthRoutes } from "./routes/health/index.js";
import { createAgentRoutes } from "./routes/agent/index.js";
import { createSessionsRoutes } from "./routes/sessions/index.js";
import { createFeaturesRoutes } from "./routes/features/index.js";
import { createAutoModeRoutes } from "./routes/auto-mode/index.js";
import { createEnhancePromptRoutes } from "./routes/enhance-prompt/index.js";
import { createWorktreeRoutes } from "./routes/worktree/index.js";
import { createGitRoutes } from "./routes/git/index.js";
import { createSetupRoutes } from "./routes/setup/index.js";
import { createSuggestionsRoutes } from "./routes/suggestions/index.js";
import { createModelsRoutes } from "./routes/models/index.js";
import { createRunningAgentsRoutes } from "./routes/running-agents/index.js";
import { createWorkspaceRoutes } from "./routes/workspace/index.js";
import { createTemplatesRoutes } from "./routes/templates/index.js";
import { createEventEmitter, type EventEmitter } from './lib/events.js';
import { initAllowedPaths } from '@automaker/platform';
import { authMiddleware, getAuthStatus } from './lib/auth.js';
import { createFsRoutes } from './routes/fs/index.js';
import { createHealthRoutes } from './routes/health/index.js';
import { createAgentRoutes } from './routes/agent/index.js';
import { createSessionsRoutes } from './routes/sessions/index.js';
import { createFeaturesRoutes } from './routes/features/index.js';
import { createAutoModeRoutes } from './routes/auto-mode/index.js';
import { createEnhancePromptRoutes } from './routes/enhance-prompt/index.js';
import { createWorktreeRoutes } from './routes/worktree/index.js';
import { createGitRoutes } from './routes/git/index.js';
import { createSetupRoutes } from './routes/setup/index.js';
import { createSuggestionsRoutes } from './routes/suggestions/index.js';
import { createModelsRoutes } from './routes/models/index.js';
import { createRunningAgentsRoutes } from './routes/running-agents/index.js';
import { createWorkspaceRoutes } from './routes/workspace/index.js';
import { createTemplatesRoutes } from './routes/templates/index.js';
import {
createTerminalRoutes,
validateTerminalToken,
isTerminalEnabled,
isTerminalPasswordRequired,
} from "./routes/terminal/index.js";
import { createSettingsRoutes } from "./routes/settings/index.js";
import { AgentService } from "./services/agent-service.js";
import { FeatureLoader } from "./services/feature-loader.js";
import { AutoModeService } from "./services/auto-mode-service.js";
import { getTerminalService } from "./services/terminal-service.js";
import { SettingsService } from "./services/settings-service.js";
import { createSpecRegenerationRoutes } from "./routes/app-spec/index.js";
import { createClaudeRoutes } from "./routes/claude/index.js";
import { ClaudeUsageService } from "./services/claude-usage-service.js";
} from './routes/terminal/index.js';
import { createSettingsRoutes } from './routes/settings/index.js';
import { AgentService } from './services/agent-service.js';
import { FeatureLoader } from './services/feature-loader.js';
import { AutoModeService } from './services/auto-mode-service.js';
import { getTerminalService } from './services/terminal-service.js';
import { SettingsService } from './services/settings-service.js';
import { createSpecRegenerationRoutes } from './routes/app-spec/index.js';
import { createClaudeRoutes } from './routes/claude/index.js';
import { ClaudeUsageService } from './services/claude-usage-service.js';
import { createContextRoutes } from './routes/context/index.js';
// Load environment variables
dotenv.config();
const PORT = parseInt(process.env.PORT || "3008", 10);
const DATA_DIR = process.env.DATA_DIR || "./data";
const ENABLE_REQUEST_LOGGING = process.env.ENABLE_REQUEST_LOGGING !== "false"; // Default to true
const PORT = parseInt(process.env.PORT || '3008', 10);
const DATA_DIR = process.env.DATA_DIR || './data';
const ENABLE_REQUEST_LOGGING = process.env.ENABLE_REQUEST_LOGGING !== 'false'; // Default to true
// Check for required environment variables
const hasAnthropicKey = !!process.env.ANTHROPIC_API_KEY;
@@ -71,7 +72,7 @@ if (!hasAnthropicKey) {
╚═══════════════════════════════════════════════════════════════════════╝
`);
} else {
console.log("[Server] ✓ ANTHROPIC_API_KEY detected (API key auth)");
console.log('[Server] ✓ ANTHROPIC_API_KEY detected (API key auth)');
}
// Initialize security
@@ -83,7 +84,7 @@ const app = express();
// Middleware
// Custom colored logger showing only endpoint and status code (configurable via ENABLE_REQUEST_LOGGING env var)
if (ENABLE_REQUEST_LOGGING) {
morgan.token("status-colored", (req, res) => {
morgan.token('status-colored', (req, res) => {
const status = res.statusCode;
if (status >= 500) return `\x1b[31m${status}\x1b[0m`; // Red for server errors
if (status >= 400) return `\x1b[33m${status}\x1b[0m`; // Yellow for client errors
@@ -92,18 +93,18 @@ if (ENABLE_REQUEST_LOGGING) {
});
app.use(
morgan(":method :url :status-colored", {
skip: (req) => req.url === "/api/health", // Skip health check logs
morgan(':method :url :status-colored', {
skip: (req) => req.url === '/api/health', // Skip health check logs
})
);
}
app.use(
cors({
origin: process.env.CORS_ORIGIN || "*",
origin: process.env.CORS_ORIGIN || '*',
credentials: true,
})
);
app.use(express.json({ limit: "50mb" }));
app.use(express.json({ limit: '50mb' }));
// Create shared event emitter for streaming
const events: EventEmitter = createEventEmitter();
@@ -118,33 +119,34 @@ const claudeUsageService = new ClaudeUsageService();
// Initialize services
(async () => {
await agentService.initialize();
console.log("[Server] Agent service initialized");
console.log('[Server] Agent service initialized');
})();
// Mount API routes - health is unauthenticated for monitoring
app.use("/api/health", createHealthRoutes());
app.use('/api/health', createHealthRoutes());
// Apply authentication to all other routes
app.use("/api", authMiddleware);
app.use('/api', authMiddleware);
app.use("/api/fs", createFsRoutes(events));
app.use("/api/agent", createAgentRoutes(agentService, events));
app.use("/api/sessions", createSessionsRoutes(agentService));
app.use("/api/features", createFeaturesRoutes(featureLoader));
app.use("/api/auto-mode", createAutoModeRoutes(autoModeService));
app.use("/api/enhance-prompt", createEnhancePromptRoutes());
app.use("/api/worktree", createWorktreeRoutes());
app.use("/api/git", createGitRoutes());
app.use("/api/setup", createSetupRoutes());
app.use("/api/suggestions", createSuggestionsRoutes(events));
app.use("/api/models", createModelsRoutes());
app.use("/api/spec-regeneration", createSpecRegenerationRoutes(events));
app.use("/api/running-agents", createRunningAgentsRoutes(autoModeService));
app.use("/api/workspace", createWorkspaceRoutes());
app.use("/api/templates", createTemplatesRoutes());
app.use("/api/terminal", createTerminalRoutes());
app.use("/api/settings", createSettingsRoutes(settingsService));
app.use("/api/claude", createClaudeRoutes(claudeUsageService));
app.use('/api/fs', createFsRoutes(events));
app.use('/api/agent', createAgentRoutes(agentService, events));
app.use('/api/sessions', createSessionsRoutes(agentService));
app.use('/api/features', createFeaturesRoutes(featureLoader));
app.use('/api/auto-mode', createAutoModeRoutes(autoModeService));
app.use('/api/enhance-prompt', createEnhancePromptRoutes());
app.use('/api/worktree', createWorktreeRoutes());
app.use('/api/git', createGitRoutes());
app.use('/api/setup', createSetupRoutes());
app.use('/api/suggestions', createSuggestionsRoutes(events));
app.use('/api/models', createModelsRoutes());
app.use('/api/spec-regeneration', createSpecRegenerationRoutes(events));
app.use('/api/running-agents', createRunningAgentsRoutes(autoModeService));
app.use('/api/workspace', createWorkspaceRoutes());
app.use('/api/templates', createTemplatesRoutes());
app.use('/api/terminal', createTerminalRoutes());
app.use('/api/settings', createSettingsRoutes(settingsService));
app.use('/api/claude', createClaudeRoutes(claudeUsageService));
app.use('/api/context', createContextRoutes());
// Create HTTP server
const server = createServer(app);
@@ -155,19 +157,16 @@ const terminalWss = new WebSocketServer({ noServer: true });
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}`
);
server.on('upgrade', (request, socket, head) => {
const { pathname } = new URL(request.url || '', `http://${request.headers.host}`);
if (pathname === "/api/events") {
if (pathname === '/api/events') {
wss.handleUpgrade(request, socket, head, (ws) => {
wss.emit("connection", ws, request);
wss.emit('connection', ws, request);
});
} else if (pathname === "/api/terminal/ws") {
} else if (pathname === '/api/terminal/ws') {
terminalWss.handleUpgrade(request, socket, head, (ws) => {
terminalWss.emit("connection", ws, request);
terminalWss.emit('connection', ws, request);
});
} else {
socket.destroy();
@@ -175,8 +174,8 @@ server.on("upgrade", (request, socket, head) => {
});
// Events WebSocket connection handler
wss.on("connection", (ws: WebSocket) => {
console.log("[WebSocket] Client connected");
wss.on('connection', (ws: WebSocket) => {
console.log('[WebSocket] Client connected');
// Subscribe to all events and forward to this client
const unsubscribe = events.subscribe((type, payload) => {
@@ -185,13 +184,13 @@ wss.on("connection", (ws: WebSocket) => {
}
});
ws.on("close", () => {
console.log("[WebSocket] Client disconnected");
ws.on('close', () => {
console.log('[WebSocket] Client disconnected');
unsubscribe();
});
ws.on("error", (error) => {
console.error("[WebSocket] Error:", error);
ws.on('error', (error) => {
console.error('[WebSocket] Error:', error);
unsubscribe();
});
});
@@ -212,184 +211,176 @@ terminalService.onExit((sessionId) => {
});
// 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;
}
// 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;
}
// 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;
}
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;
}
// 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}`);
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);
// Track this connection
if (!terminalConnections.has(sessionId)) {
terminalConnections.set(sessionId, new Set());
}
terminalConnections.get(sessionId)!.add(ws);
// Send initial connection success FIRST
// Send initial connection success FIRST
ws.send(
JSON.stringify({
type: 'connected',
sessionId,
shell: session.shell,
cwd: session.cwd,
})
);
// Send scrollback buffer BEFORE subscribing to prevent race condition
// Also clear pending output buffer to prevent duplicates from throttled flush
const scrollback = terminalService.getScrollbackAndClearPending(sessionId);
if (scrollback && scrollback.length > 0) {
ws.send(
JSON.stringify({
type: "connected",
sessionId,
shell: session.shell,
cwd: session.cwd,
type: 'scrollback',
data: scrollback,
})
);
// Send scrollback buffer BEFORE subscribing to prevent race condition
// Also clear pending output buffer to prevent duplicates from throttled flush
const scrollback = terminalService.getScrollbackAndClearPending(sessionId);
if (scrollback && scrollback.length > 0) {
ws.send(
JSON.stringify({
type: "scrollback",
data: scrollback,
})
);
}
// NOW subscribe to terminal data (after scrollback is sent)
const unsubscribeData = terminalService.onData((sid, data) => {
if (sid === sessionId && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: "data", data }));
}
});
// 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 with deduplication and rate limiting
if (msg.cols && msg.rows) {
const now = Date.now();
const lastTime = lastResizeTime.get(sessionId) || 0;
const lastDimensions = lastResizeDimensions.get(sessionId);
// Skip if resized too recently (prevents resize storm during splits)
if (now - lastTime < RESIZE_MIN_INTERVAL_MS) {
break;
}
// Check if dimensions are different from last resize
if (
!lastDimensions ||
lastDimensions.cols !== msg.cols ||
lastDimensions.rows !== msg.rows
) {
// Only suppress output on subsequent resizes, not the first one
// The first resize happens on terminal open and we don't want to drop the initial prompt
const isFirstResize = !lastDimensions;
terminalService.resize(sessionId, msg.cols, msg.rows, !isFirstResize);
lastResizeDimensions.set(sessionId, {
cols: msg.cols,
rows: msg.rows,
});
lastResizeTime.set(sessionId, now);
}
}
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);
// DON'T delete lastResizeDimensions/lastResizeTime here!
// The session still exists, and reconnecting clients need to know
// this isn't the "first resize" to prevent duplicate prompts.
// These get cleaned up when the session actually exits.
}
}
});
ws.on("error", (error) => {
console.error(`[Terminal WS] Error on session ${sessionId}:`, error);
unsubscribeData();
unsubscribeExit();
});
}
);
// NOW subscribe to terminal data (after scrollback is sent)
const unsubscribeData = terminalService.onData((sid, data) => {
if (sid === sessionId && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'data', data }));
}
});
// 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 with deduplication and rate limiting
if (msg.cols && msg.rows) {
const now = Date.now();
const lastTime = lastResizeTime.get(sessionId) || 0;
const lastDimensions = lastResizeDimensions.get(sessionId);
// Skip if resized too recently (prevents resize storm during splits)
if (now - lastTime < RESIZE_MIN_INTERVAL_MS) {
break;
}
// Check if dimensions are different from last resize
if (
!lastDimensions ||
lastDimensions.cols !== msg.cols ||
lastDimensions.rows !== msg.rows
) {
// Only suppress output on subsequent resizes, not the first one
// The first resize happens on terminal open and we don't want to drop the initial prompt
const isFirstResize = !lastDimensions;
terminalService.resize(sessionId, msg.cols, msg.rows, !isFirstResize);
lastResizeDimensions.set(sessionId, {
cols: msg.cols,
rows: msg.rows,
});
lastResizeTime.set(sessionId, now);
}
}
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);
// DON'T delete lastResizeDimensions/lastResizeTime here!
// The session still exists, and reconnecting clients need to know
// this isn't the "first resize" to prevent duplicate prompts.
// These get cleaned up when the session actually exits.
}
}
});
ws.on('error', (error) => {
console.error(`[Terminal WS] Error on session ${sessionId}:`, error);
unsubscribeData();
unsubscribeExit();
});
});
// Start server with error handling for port conflicts
const startServer = (port: number) => {
server.listen(port, () => {
const terminalStatus = isTerminalEnabled()
? isTerminalPasswordRequired()
? "enabled (password protected)"
: "enabled"
: "disabled";
? 'enabled (password protected)'
: 'enabled'
: 'disabled';
const portStr = port.toString().padEnd(4);
console.log(`
╔═══════════════════════════════════════════════════════╗
@@ -404,8 +395,8 @@ const startServer = (port: number) => {
`);
});
server.on("error", (error: NodeJS.ErrnoException) => {
if (error.code === "EADDRINUSE") {
server.on('error', (error: NodeJS.ErrnoException) => {
if (error.code === 'EADDRINUSE') {
console.error(`
╔═══════════════════════════════════════════════════════╗
║ ❌ ERROR: Port ${port} is already in use ║
@@ -426,7 +417,7 @@ const startServer = (port: number) => {
`);
process.exit(1);
} else {
console.error("[Server] Error starting server:", error);
console.error('[Server] Error starting server:', error);
process.exit(1);
}
});
@@ -435,20 +426,20 @@ const startServer = (port: number) => {
startServer(PORT);
// Graceful shutdown
process.on("SIGTERM", () => {
console.log("SIGTERM received, shutting down...");
process.on('SIGTERM', () => {
console.log('SIGTERM received, shutting down...');
terminalService.cleanup();
server.close(() => {
console.log("Server closed");
console.log('Server closed');
process.exit(0);
});
});
process.on("SIGINT", () => {
console.log("SIGINT received, shutting down...");
process.on('SIGINT', () => {
console.log('SIGINT received, shutting down...');
terminalService.cleanup();
server.close(() => {
console.log("Server closed");
console.log('Server closed');
process.exit(0);
});
});