mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-04 09:13:08 +00:00
chore: update package.json and refactor terminal WebSocket connection handling
- Added a postinstall script in package.json to set permissions for spawn-helper on macOS. - Refactored the terminal WebSocket connection handling in index.ts for improved readability and consistency. - Enhanced error logging and connection management in the terminal service. - Cleaned up formatting and indentation across multiple files for better code clarity.
This commit is contained in:
@@ -30,7 +30,12 @@ import { createSpecRegenerationRoutes } from "./routes/spec-regeneration.js";
|
|||||||
import { createRunningAgentsRoutes } from "./routes/running-agents.js";
|
import { createRunningAgentsRoutes } from "./routes/running-agents.js";
|
||||||
import { createWorkspaceRoutes } from "./routes/workspace.js";
|
import { createWorkspaceRoutes } from "./routes/workspace.js";
|
||||||
import { createTemplatesRoutes } from "./routes/templates.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 { AgentService } from "./services/agent-service.js";
|
||||||
import { FeatureLoader } from "./services/feature-loader.js";
|
import { FeatureLoader } from "./services/feature-loader.js";
|
||||||
import { AutoModeService } from "./services/auto-mode-service.js";
|
import { AutoModeService } from "./services/auto-mode-service.js";
|
||||||
@@ -64,7 +69,9 @@ if (!hasAnthropicKey && !hasOAuthToken) {
|
|||||||
╚═══════════════════════════════════════════════════════════════════════╝
|
╚═══════════════════════════════════════════════════════════════════════╝
|
||||||
`);
|
`);
|
||||||
} else if (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 {
|
} else {
|
||||||
console.log("[Server] ✓ ANTHROPIC_API_KEY detected (API key auth)");
|
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
|
// Handle HTTP upgrade requests manually to route to correct WebSocket server
|
||||||
server.on("upgrade", (request, socket, head) => {
|
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") {
|
if (pathname === "/api/events") {
|
||||||
wss.handleUpgrade(request, socket, head, (ws) => {
|
wss.handleUpgrade(request, socket, head, (ws) => {
|
||||||
@@ -171,139 +181,153 @@ wss.on("connection", (ws: WebSocket) => {
|
|||||||
const terminalConnections: Map<string, Set<WebSocket>> = new Map();
|
const terminalConnections: Map<string, Set<WebSocket>> = new Map();
|
||||||
|
|
||||||
// Terminal WebSocket connection handler
|
// Terminal WebSocket connection handler
|
||||||
terminalWss.on("connection", (ws: WebSocket, req: import("http").IncomingMessage) => {
|
terminalWss.on(
|
||||||
// Parse URL to get session ID and token
|
"connection",
|
||||||
const url = new URL(req.url || "", `http://${req.headers.host}`);
|
(ws: WebSocket, req: import("http").IncomingMessage) => {
|
||||||
const sessionId = url.searchParams.get("sessionId");
|
// Parse URL to get session ID and token
|
||||||
const token = url.searchParams.get("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
|
// Check if terminal is enabled
|
||||||
if (!isTerminalEnabled()) {
|
if (!isTerminalEnabled()) {
|
||||||
console.log("[Terminal WS] Terminal is disabled");
|
console.log("[Terminal WS] Terminal is disabled");
|
||||||
ws.close(4003, "Terminal access is disabled");
|
ws.close(4003, "Terminal access is disabled");
|
||||||
return;
|
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 }));
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
// Subscribe to terminal exit
|
// Validate token if password is required
|
||||||
const unsubscribeExit = terminalService.onExit((sid, exitCode) => {
|
if (
|
||||||
if (sid === sessionId && ws.readyState === WebSocket.OPEN) {
|
isTerminalPasswordRequired() &&
|
||||||
ws.send(JSON.stringify({ type: "exit", exitCode }));
|
!validateTerminalToken(token || undefined)
|
||||||
ws.close(1000, "Session ended");
|
) {
|
||||||
|
console.log("[Terminal WS] Invalid or missing token");
|
||||||
|
ws.close(4001, "Authentication required");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
// Handle incoming messages
|
if (!sessionId) {
|
||||||
ws.on("message", (message) => {
|
console.log("[Terminal WS] No session ID provided");
|
||||||
try {
|
ws.close(4002, "Session ID required");
|
||||||
const msg = JSON.parse(message.toString());
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
switch (msg.type) {
|
// Check if session exists
|
||||||
case "input":
|
const session = terminalService.getSession(sessionId);
|
||||||
// Write user input to terminal
|
if (!session) {
|
||||||
terminalService.write(sessionId, msg.data);
|
console.log(`[Terminal WS] Session ${sessionId} not found`);
|
||||||
break;
|
ws.close(4004, "Session not found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
case "resize":
|
console.log(`[Terminal WS] Client connected to session ${sessionId}`);
|
||||||
// Resize terminal
|
|
||||||
if (msg.cols && msg.rows) {
|
|
||||||
terminalService.resize(sessionId, msg.cols, msg.rows);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "ping":
|
// Track this connection
|
||||||
// Respond to ping
|
if (!terminalConnections.has(sessionId)) {
|
||||||
ws.send(JSON.stringify({ type: "pong" }));
|
terminalConnections.set(sessionId, new Set());
|
||||||
break;
|
}
|
||||||
|
terminalConnections.get(sessionId)!.add(ws);
|
||||||
|
|
||||||
default:
|
// Subscribe to terminal data
|
||||||
console.warn(`[Terminal WS] Unknown message type: ${msg.type}`);
|
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", () => {
|
// Subscribe to terminal exit
|
||||||
console.log(`[Terminal WS] Client disconnected from session ${sessionId}`);
|
const unsubscribeExit = terminalService.onExit((sid, exitCode) => {
|
||||||
unsubscribeData();
|
if (sid === sessionId && ws.readyState === WebSocket.OPEN) {
|
||||||
unsubscribeExit();
|
ws.send(JSON.stringify({ type: "exit", exitCode }));
|
||||||
|
ws.close(1000, "Session ended");
|
||||||
// Remove from connections tracking
|
|
||||||
const connections = terminalConnections.get(sessionId);
|
|
||||||
if (connections) {
|
|
||||||
connections.delete(ws);
|
|
||||||
if (connections.size === 0) {
|
|
||||||
terminalConnections.delete(sessionId);
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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
|
// Start server
|
||||||
server.listen(PORT, () => {
|
server.listen(PORT, () => {
|
||||||
const terminalStatus = isTerminalEnabled()
|
const terminalStatus = isTerminalEnabled()
|
||||||
? (isTerminalPasswordRequired() ? "enabled (password protected)" : "enabled")
|
? isTerminalPasswordRequired()
|
||||||
|
? "enabled (password protected)"
|
||||||
|
: "enabled"
|
||||||
: "disabled";
|
: "disabled";
|
||||||
console.log(`
|
console.log(`
|
||||||
╔═══════════════════════════════════════════════════════╗
|
╔═══════════════════════════════════════════════════════╗
|
||||||
|
|||||||
@@ -9,9 +9,7 @@ import fs from "fs/promises";
|
|||||||
import type { EventEmitter } from "../lib/events.js";
|
import type { EventEmitter } from "../lib/events.js";
|
||||||
import { ProviderFactory } from "../providers/provider-factory.js";
|
import { ProviderFactory } from "../providers/provider-factory.js";
|
||||||
import type { ExecuteOptions } from "../providers/types.js";
|
import type { ExecuteOptions } from "../providers/types.js";
|
||||||
import {
|
import { readImageAsBase64 } from "../lib/image-handler.js";
|
||||||
readImageAsBase64,
|
|
||||||
} from "../lib/image-handler.js";
|
|
||||||
import { buildPromptWithImages } from "../lib/prompt-builder.js";
|
import { buildPromptWithImages } from "../lib/prompt-builder.js";
|
||||||
import { getEffectiveModel } from "../lib/model-resolver.js";
|
import { getEffectiveModel } from "../lib/model-resolver.js";
|
||||||
import { isAbortError } from "../lib/error-handler.js";
|
import { isAbortError } from "../lib/error-handler.js";
|
||||||
@@ -136,7 +134,10 @@ export class AgentService {
|
|||||||
filename: imageData.filename,
|
filename: imageData.filename,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} 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",
|
"WebFetch",
|
||||||
],
|
],
|
||||||
abortController: session.abortController!,
|
abortController: session.abortController!,
|
||||||
conversationHistory: conversationHistory.length > 0 ? conversationHistory : undefined,
|
conversationHistory:
|
||||||
|
conversationHistory.length > 0 ? conversationHistory : undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Build prompt content with images
|
// Build prompt content with images
|
||||||
@@ -381,7 +383,11 @@ export class AgentService {
|
|||||||
const sessionFile = path.join(this.stateDir, `${sessionId}.json`);
|
const sessionFile = path.join(this.stateDir, `${sessionId}.json`);
|
||||||
|
|
||||||
try {
|
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);
|
await this.updateSessionTimestamp(sessionId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[AgentService] Failed to save session:", error);
|
console.error("[AgentService] Failed to save session:", error);
|
||||||
@@ -398,7 +404,11 @@ export class AgentService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async saveMetadata(metadata: Record<string, SessionMetadata>): Promise<void> {
|
async saveMetadata(metadata: Record<string, SessionMetadata>): Promise<void> {
|
||||||
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<void> {
|
async updateSessionTimestamp(sessionId: string): Promise<void> {
|
||||||
@@ -418,7 +428,8 @@ export class AgentService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return sessions.sort(
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private emitAgentEvent(sessionId: string, data: Record<string, unknown>): void {
|
private emitAgentEvent(
|
||||||
|
sessionId: string,
|
||||||
|
data: Record<string, unknown>
|
||||||
|
): void {
|
||||||
this.events.emit("agent:stream", { sessionId, ...data });
|
this.events.emit("agent:stream", { sessionId, ...data });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
"libs/*"
|
"libs/*"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"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": "npm run dev --workspace=apps/app",
|
||||||
"dev:web": "npm run dev:web --workspace=apps/app",
|
"dev:web": "npm run dev:web --workspace=apps/app",
|
||||||
"dev:electron": "npm run dev:electron --workspace=apps/app",
|
"dev:electron": "npm run dev:electron --workspace=apps/app",
|
||||||
|
|||||||
Reference in New Issue
Block a user