UNPKG

claude-code-webui

Version:

Web-based interface for the Claude Code CLI with streaming chat interface

1,131 lines (1,111 loc) 34.1 kB
#!/usr/bin/env node // app.ts import { Hono } from "hono"; import { cors } from "hono/cors"; // middleware/config.ts import { createMiddleware } from "hono/factory"; function createConfigMiddleware(options) { return createMiddleware(async (c, next) => { c.set("config", options); await next(); }); } // history/pathUtils.ts async function getEncodedProjectName(projectPath, runtime2) { const homeDir = runtime2.getHomeDir(); if (!homeDir) { return null; } const projectsDir = `${homeDir}/.claude/projects`; try { const entries = []; for await (const entry of runtime2.readDir(projectsDir)) { if (entry.isDirectory) { entries.push(entry.name); } } const normalizedPath = projectPath.replace(/\/$/, ""); const expectedEncoded = normalizedPath.replace(/[/\\:.]/g, "-"); if (entries.includes(expectedEncoded)) { return expectedEncoded; } return null; } catch { return null; } } function validateEncodedProjectName(encodedName) { if (!encodedName) { return false; } const dangerousChars = /[<>:"|?*\x00-\x1f\/\\]/; if (dangerousChars.test(encodedName)) { return false; } return true; } // handlers/projects.ts async function handleProjectsRequest(c) { try { const { runtime: runtime2 } = c.var.config; const homeDir = runtime2.getHomeDir(); if (!homeDir) { return c.json({ error: "Home directory not found" }, 500); } const claudeConfigPath = `${homeDir}/.claude.json`; try { const configContent = await runtime2.readTextFile(claudeConfigPath); const config = JSON.parse(configContent); if (config.projects && typeof config.projects === "object") { const projectPaths = Object.keys(config.projects); const projects = []; for (const path of projectPaths) { const encodedName = await getEncodedProjectName(path, runtime2); if (encodedName) { projects.push({ path, encodedName }); } } const response = { projects }; return c.json(response); } else { const response = { projects: [] }; return c.json(response); } } catch (error) { if (error instanceof Error && error.message.includes("No such file")) { const response = { projects: [] }; return c.json(response); } throw error; } } catch (error) { console.error("Error reading projects:", error); return c.json({ error: "Failed to read projects" }, 500); } } // history/parser.ts async function parseHistoryFile(filePath, runtime2) { try { const content = await runtime2.readTextFile(filePath); const lines = content.trim().split("\n").filter((line) => line.trim()); if (lines.length === 0) { return null; } const messages = []; const messageIds = /* @__PURE__ */ new Set(); let startTime = ""; let lastTime = ""; let lastMessagePreview = ""; for (const line of lines) { try { const parsed = JSON.parse(line); messages.push(parsed); if (parsed.message?.role === "assistant" && parsed.message?.id) { messageIds.add(parsed.message.id); } if (!startTime || parsed.timestamp < startTime) { startTime = parsed.timestamp; } if (!lastTime || parsed.timestamp > lastTime) { lastTime = parsed.timestamp; } if (parsed.message?.role === "assistant" && parsed.message?.content) { const content2 = parsed.message.content; if (Array.isArray(content2)) { for (const item of content2) { if (typeof item === "object" && item && "text" in item) { lastMessagePreview = String(item.text).substring(0, 100); break; } } } else if (typeof content2 === "string") { lastMessagePreview = content2.substring(0, 100); } } } catch (parseError) { console.error(`Failed to parse line in ${filePath}:`, parseError); } } const fileName = filePath.split("/").pop() || ""; const sessionId = fileName.replace(".jsonl", ""); return { sessionId, filePath, messages, messageIds, startTime, lastTime, messageCount: messages.length, lastMessagePreview: lastMessagePreview || "No preview available" }; } catch (error) { console.error(`Failed to read history file ${filePath}:`, error); return null; } } async function getHistoryFiles(historyDir, runtime2) { try { const files = []; for await (const entry of runtime2.readDir(historyDir)) { if (entry.isFile && entry.name.endsWith(".jsonl")) { files.push(`${historyDir}/${entry.name}`); } } return files; } catch { return []; } } async function parseAllHistoryFiles(historyDir, runtime2) { const filePaths = await getHistoryFiles(historyDir, runtime2); const results = []; for (const filePath of filePaths) { const parsed = await parseHistoryFile(filePath, runtime2); if (parsed) { results.push(parsed); } } return results; } function isSubset(subset, superset) { if (subset.size > superset.size) { return false; } for (const item of subset) { if (!superset.has(item)) { return false; } } return true; } // history/grouping.ts function groupConversations(conversationFiles) { if (conversationFiles.length === 0) { return []; } const sortedConversations = [...conversationFiles].sort((a, b) => { return a.messageIds.size - b.messageIds.size; }); const uniqueConversations = []; for (const currentConv of sortedConversations) { const isSubsetOfExisting = uniqueConversations.some( (existingConv) => isSubset(currentConv.messageIds, existingConv.messageIds) ); if (!isSubsetOfExisting) { uniqueConversations.push(currentConv); } } const summaries = uniqueConversations.map( (conv) => createConversationSummary(conv) ); summaries.sort( (a, b) => new Date(b.startTime).getTime() - new Date(a.startTime).getTime() ); return summaries; } function createConversationSummary(conversationFile) { return { sessionId: conversationFile.sessionId, startTime: conversationFile.startTime, lastTime: conversationFile.lastTime, messageCount: conversationFile.messageCount, lastMessagePreview: conversationFile.lastMessagePreview }; } // handlers/histories.ts async function handleHistoriesRequest(c) { try { const { debugMode, runtime: runtime2 } = c.var.config; const encodedProjectName = c.req.param("encodedProjectName"); if (!encodedProjectName) { return c.json({ error: "Encoded project name is required" }, 400); } if (!validateEncodedProjectName(encodedProjectName)) { return c.json({ error: "Invalid encoded project name" }, 400); } if (debugMode) { console.debug( `[DEBUG] Fetching histories for encoded project: ${encodedProjectName}` ); } const homeDir = runtime2.getHomeDir(); if (!homeDir) { return c.json({ error: "Home directory not found" }, 500); } const historyDir = `${homeDir}/.claude/projects/${encodedProjectName}`; if (debugMode) { console.debug(`[DEBUG] History directory: ${historyDir}`); } try { const dirInfo = await runtime2.stat(historyDir); if (!dirInfo.isDirectory) { return c.json({ error: "Project not found" }, 404); } } catch (error) { if (error instanceof Error && error.message.includes("No such file")) { return c.json({ error: "Project not found" }, 404); } throw error; } const conversationFiles = await parseAllHistoryFiles(historyDir, runtime2); if (debugMode) { console.debug( `[DEBUG] Found ${conversationFiles.length} conversation files` ); } const conversations = groupConversations(conversationFiles); if (debugMode) { console.debug( `[DEBUG] After grouping: ${conversations.length} unique conversations` ); } const response = { conversations }; return c.json(response); } catch (error) { console.error("Error fetching conversation histories:", error); return c.json( { error: "Failed to fetch conversation histories", details: error instanceof Error ? error.message : String(error) }, 500 ); } } // history/timestampRestore.ts function restoreTimestamps(messages) { const timestampMap = /* @__PURE__ */ new Map(); for (const msg of messages) { if (msg.type === "assistant" && msg.message?.id) { const messageId = msg.message.id; if (!timestampMap.has(messageId)) { timestampMap.set(messageId, msg.timestamp); } else { const existingTimestamp = timestampMap.get(messageId); if (msg.timestamp < existingTimestamp) { timestampMap.set(messageId, msg.timestamp); } } } } return messages.map((msg) => { if (msg.type === "assistant" && msg.message?.id) { const restoredTimestamp = timestampMap.get(msg.message.id); if (restoredTimestamp) { return { ...msg, timestamp: restoredTimestamp }; } } return msg; }); } function sortMessagesByTimestamp(messages) { return [...messages].sort((a, b) => { return new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime(); }); } function calculateConversationMetadata(messages) { if (messages.length === 0) { const now = (/* @__PURE__ */ new Date()).toISOString(); return { startTime: now, endTime: now, messageCount: 0 }; } const sortedMessages = sortMessagesByTimestamp(messages); const startTime = sortedMessages[0].timestamp; const endTime = sortedMessages[sortedMessages.length - 1].timestamp; return { startTime, endTime, messageCount: messages.length }; } function processConversationMessages(messages, _sessionId) { const restoredMessages = restoreTimestamps(messages); const sortedMessages = sortMessagesByTimestamp(restoredMessages); const metadata = calculateConversationMetadata(sortedMessages); return { messages: sortedMessages, metadata }; } // history/conversationLoader.ts async function loadConversation(encodedProjectName, sessionId, runtime2) { if (!validateEncodedProjectName(encodedProjectName)) { throw new Error("Invalid encoded project name"); } if (!validateSessionId(sessionId)) { throw new Error("Invalid session ID format"); } const homeDir = runtime2.getHomeDir(); if (!homeDir) { throw new Error("Home directory not found"); } const historyDir = `${homeDir}/.claude/projects/${encodedProjectName}`; const filePath = `${historyDir}/${sessionId}.jsonl`; if (!await runtime2.exists(filePath)) { return null; } try { const conversationHistory = await parseConversationFile( filePath, sessionId, runtime2 ); return conversationHistory; } catch (error) { throw error; } } async function parseConversationFile(filePath, sessionId, runtime2) { const content = await runtime2.readTextFile(filePath); const lines = content.trim().split("\n").filter((line) => line.trim()); if (lines.length === 0) { throw new Error("Empty conversation file"); } const rawLines = []; for (const line of lines) { try { const parsed = JSON.parse(line); rawLines.push(parsed); } catch (parseError) { console.error(`Failed to parse line in ${filePath}:`, parseError); } } const { messages: processedMessages, metadata } = processConversationMessages( rawLines, sessionId ); return { sessionId, messages: processedMessages, metadata }; } function validateSessionId(sessionId) { if (!sessionId) { return false; } const dangerousChars = /[<>:"|?*\x00-\x1f\/\\]/; if (dangerousChars.test(sessionId)) { return false; } if (sessionId.length > 255) { return false; } if (sessionId.startsWith(".")) { return false; } return true; } // handlers/conversations.ts async function handleConversationRequest(c) { try { const { debugMode, runtime: runtime2 } = c.var.config; const encodedProjectName = c.req.param("encodedProjectName"); const sessionId = c.req.param("sessionId"); if (!encodedProjectName) { return c.json({ error: "Encoded project name is required" }, 400); } if (!sessionId) { return c.json({ error: "Session ID is required" }, 400); } if (!validateEncodedProjectName(encodedProjectName)) { return c.json({ error: "Invalid encoded project name" }, 400); } if (debugMode) { console.debug( `[DEBUG] Fetching conversation details for project: ${encodedProjectName}, session: ${sessionId}` ); } const conversationHistory = await loadConversation( encodedProjectName, sessionId, runtime2 ); if (!conversationHistory) { return c.json( { error: "Conversation not found", sessionId }, 404 ); } if (debugMode) { console.debug( `[DEBUG] Loaded conversation with ${conversationHistory.messages.length} messages` ); } return c.json(conversationHistory); } catch (error) { console.error("Error fetching conversation details:", error); if (error instanceof Error) { if (error.message.includes("Invalid session ID")) { return c.json( { error: "Invalid session ID format", details: error.message }, 400 ); } if (error.message.includes("Invalid encoded project name")) { return c.json( { error: "Invalid project name", details: error.message }, 400 ); } } return c.json( { error: "Failed to fetch conversation details", details: error instanceof Error ? error.message : String(error) }, 500 ); } } // handlers/chat.ts import { AbortError, query } from "@anthropic-ai/claude-code"; async function* executeClaudeCommand(message, requestId, requestAbortControllers, cliPath, sessionId, allowedTools, workingDirectory, debugMode) { let abortController; try { let processedMessage = message; if (message.startsWith("/")) { processedMessage = message.substring(1); } abortController = new AbortController(); requestAbortControllers.set(requestId, abortController); for await (const sdkMessage of query({ prompt: processedMessage, options: { abortController, executable: "node", executableArgs: [], pathToClaudeCodeExecutable: cliPath, ...sessionId ? { resume: sessionId } : {}, ...allowedTools ? { allowedTools } : {}, ...workingDirectory ? { cwd: workingDirectory } : {} } })) { if (debugMode) { console.debug("[DEBUG] Claude SDK Message:"); console.debug(JSON.stringify(sdkMessage, null, 2)); console.debug("---"); } yield { type: "claude_json", data: sdkMessage }; } yield { type: "done" }; } catch (error) { if (error instanceof AbortError) { yield { type: "aborted" }; } else { if (debugMode) { console.error("Claude Code execution failed:", error); } yield { type: "error", error: error instanceof Error ? error.message : String(error) }; } } finally { if (requestAbortControllers.has(requestId)) { requestAbortControllers.delete(requestId); } } } async function handleChatRequest(c, requestAbortControllers) { const chatRequest = await c.req.json(); const { debugMode, cliPath } = c.var.config; if (debugMode) { console.debug( "[DEBUG] Received chat request:", JSON.stringify(chatRequest, null, 2) ); } const stream = new ReadableStream({ async start(controller) { try { for await (const chunk of executeClaudeCommand( chatRequest.message, chatRequest.requestId, requestAbortControllers, cliPath, // Use detected CLI path from validateClaudeCli chatRequest.sessionId, chatRequest.allowedTools, chatRequest.workingDirectory, debugMode )) { const data = JSON.stringify(chunk) + "\n"; controller.enqueue(new TextEncoder().encode(data)); } controller.close(); } catch (error) { const errorResponse = { type: "error", error: error instanceof Error ? error.message : String(error) }; controller.enqueue( new TextEncoder().encode(JSON.stringify(errorResponse) + "\n") ); controller.close(); } } }); return new Response(stream, { headers: { "Content-Type": "application/x-ndjson", "Cache-Control": "no-cache", Connection: "keep-alive" } }); } // handlers/abort.ts function handleAbortRequest(c, requestAbortControllers) { const { debugMode } = c.var.config; const requestId = c.req.param("requestId"); if (!requestId) { return c.json({ error: "Request ID is required" }, 400); } if (debugMode) { console.debug(`[DEBUG] Abort attempt for request: ${requestId}`); console.debug( `[DEBUG] Active requests: ${Array.from(requestAbortControllers.keys())}` ); } const abortController = requestAbortControllers.get(requestId); if (abortController) { abortController.abort(); requestAbortControllers.delete(requestId); if (debugMode) { console.debug(`[DEBUG] Aborted request: ${requestId}`); } return c.json({ success: true, message: "Request aborted" }); } else { return c.json({ error: "Request not found or already completed" }, 404); } } // app.ts function createApp(runtime2, config) { const app = new Hono(); const requestAbortControllers = /* @__PURE__ */ new Map(); app.use( "*", cors({ origin: "*", allowMethods: ["GET", "POST", "OPTIONS"], allowHeaders: ["Content-Type"] }) ); app.use( "*", createConfigMiddleware({ debugMode: config.debugMode, runtime: runtime2, cliPath: config.cliPath }) ); app.get("/api/projects", (c) => handleProjectsRequest(c)); app.get( "/api/projects/:encodedProjectName/histories", (c) => handleHistoriesRequest(c) ); app.get( "/api/projects/:encodedProjectName/histories/:sessionId", (c) => handleConversationRequest(c) ); app.post( "/api/abort/:requestId", (c) => handleAbortRequest(c, requestAbortControllers) ); app.post("/api/chat", (c) => handleChatRequest(c, requestAbortControllers)); const serveStatic2 = runtime2.createStaticFileMiddleware({ root: config.staticPath }); app.use("/assets/*", serveStatic2); app.get("*", async (c) => { const path = c.req.path; if (path.startsWith("/api/")) { return c.text("Not found", 404); } try { const indexPath = `${config.staticPath}/index.html`; const indexFile = await runtime2.readBinaryFile(indexPath); return c.html(new TextDecoder().decode(indexFile)); } catch (error) { console.error("Error serving index.html:", error); return c.text("Internal server error", 500); } }); return app; } // runtime/node.ts import { constants as fsConstants, promises as fs } from "node:fs"; import { spawn } from "node:child_process"; import { homedir, tmpdir } from "node:os"; import { join } from "node:path"; import process from "node:process"; import { serve } from "@hono/node-server"; import { Hono as Hono2 } from "hono"; import { serveStatic } from "@hono/node-server/serve-static"; var NodeRuntime = class { async readTextFile(path) { return await fs.readFile(path, "utf8"); } async readBinaryFile(path) { const buffer = await fs.readFile(path); return new Uint8Array(buffer); } async writeTextFile(path, content, options) { await fs.writeFile(path, content, "utf8"); if (options?.mode !== void 0) { await fs.chmod(path, options.mode); } } async exists(path) { try { await fs.access(path, fsConstants.F_OK); return true; } catch { return false; } } async stat(path) { const stats = await fs.stat(path); return { isFile: stats.isFile(), isDirectory: stats.isDirectory(), isSymlink: stats.isSymbolicLink(), size: stats.size, mtime: stats.mtime }; } async *readDir(path) { const entries = await fs.readdir(path, { withFileTypes: true }); for (const entry of entries) { yield { name: entry.name, isFile: entry.isFile(), isDirectory: entry.isDirectory(), isSymlink: entry.isSymbolicLink() }; } } async withTempDir(callback) { const tempDir = await fs.mkdtemp(join(tmpdir(), "claude-webui-temp-")); try { return await callback(tempDir); } finally { try { await fs.rm(tempDir, { recursive: true, force: true }); } catch { } } } getEnv(key) { return process.env[key]; } getArgs() { return process.argv.slice(2); } getPlatform() { switch (process.platform) { case "win32": return "windows"; case "darwin": return "darwin"; case "linux": return "linux"; default: return "linux"; } } getHomeDir() { try { return homedir(); } catch { return void 0; } } exit(code) { process.exit(code); } async findExecutable(name) { const platform = this.getPlatform(); const candidates = []; if (platform === "windows") { const executableNames = [ name, `${name}.exe`, `${name}.cmd`, `${name}.bat` ]; for (const execName of executableNames) { const result = await this.runCommand("where", [execName]); if (result.success && result.stdout.trim()) { const paths = result.stdout.trim().split("\n").map((p) => p.trim()).filter((p) => p); candidates.push(...paths); } } } else { const result = await this.runCommand("which", [name]); if (result.success && result.stdout.trim()) { candidates.push(result.stdout.trim()); } } return candidates; } runCommand(command, args, options) { return new Promise((resolve) => { const isWindows = this.getPlatform() === "windows"; const spawnOptions = { stdio: ["ignore", "pipe", "pipe"], env: options?.env ? { ...process.env, ...options.env } : process.env }; let actualCommand = command; let actualArgs = args; if (isWindows) { actualCommand = "cmd.exe"; actualArgs = ["/c", command, ...args]; } const child = spawn(actualCommand, actualArgs, spawnOptions); const textDecoder = new TextDecoder(); let stdout = ""; let stderr = ""; child.stdout?.on("data", (data) => { stdout += textDecoder.decode(data, { stream: true }); }); child.stderr?.on("data", (data) => { stderr += textDecoder.decode(data, { stream: true }); }); child.on("close", (code) => { resolve({ success: code === 0, code: code ?? 1, stdout, stderr }); }); child.on("error", (error) => { resolve({ success: false, code: 1, stdout: "", stderr: error.message }); }); }); } serve(port, hostname, handler) { const app = new Hono2(); app.all("*", async (c) => { const response = await handler(c.req.raw); return response; }); serve({ fetch: app.fetch, port, hostname }); console.log(`Listening on http://${hostname}:${port}/`); } createStaticFileMiddleware(options) { return serveStatic(options); } }; // cli/args.ts import { program } from "commander"; // cli/version.ts var VERSION = "0.1.45"; // cli/args.ts function parseCliArgs(runtime2) { const version = VERSION; const defaultPort = parseInt(runtime2.getEnv("PORT") || "8080", 10); program.name("claude-code-webui").version(version, "-v, --version", "display version number").description("Claude Code Web UI Backend Server").option( "-p, --port <port>", "Port to listen on", (value) => { const parsed = parseInt(value, 10); if (isNaN(parsed)) { throw new Error(`Invalid port number: ${value}`); } return parsed; }, defaultPort ).option( "--host <host>", "Host address to bind to (use 0.0.0.0 for all interfaces)", "127.0.0.1" ).option( "--claude-path <path>", "Path to claude executable (overrides automatic detection)" ).option("-d, --debug", "Enable debug mode", false); program.parse(runtime2.getArgs(), { from: "user" }); const options = program.opts(); const debugEnv = runtime2.getEnv("DEBUG"); const debugFromEnv = debugEnv?.toLowerCase() === "true" || debugEnv === "1"; return { debug: options.debug || debugFromEnv, port: options.port, host: options.host, claudePath: options.claudePath }; } // cli/validation.ts import { dirname, join as join2 } from "node:path"; var DOUBLE_BACKSLASH_REGEX = /\\\\/g; async function parseCmdScript(runtime2, cmdPath) { try { console.debug(`[DEBUG] Parsing Windows .cmd script: ${cmdPath}`); const cmdContent = await runtime2.readTextFile(cmdPath); const cmdDir = dirname(cmdPath); const execLineMatch = cmdContent.match(/"%_prog%"[^"]*"(%dp0%\\[^"]+)"/); if (execLineMatch) { const fullPath = execLineMatch[1]; const pathMatch = fullPath.match(/%dp0%\\(.+)/); if (pathMatch) { const relativePath = pathMatch[1]; const absolutePath = join2(cmdDir, relativePath); console.debug(`[DEBUG] Found CLI script reference: ${relativePath}`); console.debug(`[DEBUG] Resolved absolute path: ${absolutePath}`); if (await runtime2.exists(absolutePath)) { console.debug(`[DEBUG] .cmd parsing successful: ${absolutePath}`); return absolutePath; } else { console.debug( `[DEBUG] Resolved path does not exist: ${absolutePath}` ); } } else { console.debug( `[DEBUG] Could not extract relative path from: ${fullPath}` ); } } else { console.debug( `[DEBUG] No CLI script execution pattern found in .cmd content` ); } return null; } catch (error) { console.debug( `[DEBUG] Failed to parse .cmd script: ${error instanceof Error ? error.message : String(error)}` ); return null; } } function getWindowsWrapperScript(traceFile, nodePath) { return `@echo off echo %~1 >> "${traceFile}" "${nodePath}" %*`; } function getUnixWrapperScript(traceFile, nodePath) { return `#!/bin/bash echo "$1" >> "${traceFile}" exec "${nodePath}" "$@"`; } async function detectClaudeCliPath(runtime2, claudePath) { const platform = runtime2.getPlatform(); const isWindows = platform === "windows"; let pathWrappingResult = null; try { pathWrappingResult = await runtime2.withTempDir(async (tempDir) => { const traceFile = `${tempDir}/trace.log`; const nodeExecutables = await runtime2.findExecutable("node"); if (nodeExecutables.length === 0) { return null; } const originalNodePath = nodeExecutables[0]; const wrapperFileName = isWindows ? "node.bat" : "node"; const wrapperScript = isWindows ? getWindowsWrapperScript(traceFile, originalNodePath) : getUnixWrapperScript(traceFile, originalNodePath); await runtime2.writeTextFile( `${tempDir}/${wrapperFileName}`, wrapperScript, isWindows ? void 0 : { mode: 493 } ); const currentPath = runtime2.getEnv("PATH") || ""; const modifiedPath = isWindows ? `${tempDir};${currentPath}` : `${tempDir}:${currentPath}`; const executionResult = await runtime2.runCommand( claudePath, ["--version"], { env: { PATH: modifiedPath } } ); if (!executionResult.success) { return null; } const versionOutput = executionResult.stdout.trim(); let traceContent; try { traceContent = await runtime2.readTextFile(traceFile); } catch { return { scriptPath: "", versionOutput }; } if (!traceContent.trim()) { return { scriptPath: "", versionOutput }; } const traceLines = traceContent.split("\n").map((line) => line.trim()).filter((line) => line.length > 0); for (const traceLine of traceLines) { let scriptPath = traceLine.trim(); if (scriptPath) { if (isWindows) { scriptPath = scriptPath.replace(DOUBLE_BACKSLASH_REGEX, "\\"); } } if (scriptPath) { return { scriptPath, versionOutput }; } } return { scriptPath: "", versionOutput }; }); } catch (error) { console.debug( `[DEBUG] PATH wrapping detection failed: ${error instanceof Error ? error.message : String(error)}` ); pathWrappingResult = null; } if (pathWrappingResult && pathWrappingResult.scriptPath) { return pathWrappingResult; } if (isWindows && claudePath.endsWith(".cmd")) { console.debug( "[DEBUG] PATH wrapping method failed, trying .cmd parsing fallback..." ); try { const cmdParsedPath = await parseCmdScript(runtime2, claudePath); if (cmdParsedPath) { let versionOutput = pathWrappingResult?.versionOutput || ""; if (!versionOutput) { try { const versionResult = await runtime2.runCommand(claudePath, [ "--version" ]); if (versionResult.success) { versionOutput = versionResult.stdout.trim(); } } catch { } } return { scriptPath: cmdParsedPath, versionOutput }; } } catch (fallbackError) { console.debug( `[DEBUG] .cmd parsing fallback failed: ${fallbackError instanceof Error ? fallbackError.message : String(fallbackError)}` ); } } return { scriptPath: "", versionOutput: pathWrappingResult?.versionOutput || "" }; } async function validateClaudeCli(runtime2, customPath) { try { const platform = runtime2.getPlatform(); const isWindows = platform === "windows"; let claudePath = ""; if (customPath) { claudePath = customPath; console.log(`\u{1F50D} Validating custom Claude path: ${customPath}`); } else { console.log("\u{1F50D} Searching for Claude CLI in PATH..."); const candidates = await runtime2.findExecutable("claude"); if (candidates.length === 0) { console.error("\u274C Claude CLI not found in PATH"); console.error(" Please install claude-code globally:"); console.error( " Visit: https://claude.ai/code for installation instructions" ); runtime2.exit(1); } if (isWindows && candidates.length > 1) { const cmdCandidate = candidates.find((path) => path.endsWith(".cmd")); claudePath = cmdCandidate || candidates[0]; console.debug( `[DEBUG] Found Claude CLI candidates: ${candidates.join(", ")}` ); console.debug( `[DEBUG] Using Claude CLI path: ${claudePath} (Windows .cmd preferred)` ); } else { claudePath = candidates[0]; console.debug( `[DEBUG] Found Claude CLI candidates: ${candidates.join(", ")}` ); console.debug(`[DEBUG] Using Claude CLI path: ${claudePath}`); } } const isCmdFile = claudePath.endsWith(".cmd"); if (isWindows && isCmdFile) { console.debug( "[DEBUG] Detected Windows .cmd file - fallback parsing available if needed" ); } console.log("\u{1F50D} Detecting actual Claude CLI script path..."); const detection = await detectClaudeCliPath(runtime2, claudePath); if (detection.scriptPath) { console.log(`\u2705 Claude CLI script detected: ${detection.scriptPath}`); if (detection.versionOutput) { console.log(`\u2705 Claude CLI found: ${detection.versionOutput}`); } return detection.scriptPath; } else { console.log( `\u26A0\uFE0F CLI script detection failed, using original path: ${claudePath}` ); if (detection.versionOutput) { console.log(`\u2705 Claude CLI found: ${detection.versionOutput}`); } return claudePath; } } catch (error) { console.error("\u274C Failed to validate Claude CLI"); console.error( ` Error: ${error instanceof Error ? error.message : String(error)}` ); runtime2.exit(1); } } // cli/node.ts import { fileURLToPath } from "node:url"; import { dirname as dirname2, join as join3 } from "node:path"; async function main(runtime2) { const args = parseCliArgs(runtime2); const cliPath = await validateClaudeCli(runtime2, args.claudePath); if (args.debug) { console.log("\u{1F41B} Debug mode enabled"); } const __dirname = import.meta.dirname ?? dirname2(fileURLToPath(import.meta.url)); const staticPath = join3(__dirname, "../static"); const app = createApp(runtime2, { debugMode: args.debug, staticPath, cliPath }); console.log(`\u{1F680} Server starting on ${args.host}:${args.port}`); runtime2.serve(args.port, args.host, app.fetch); } var runtime = new NodeRuntime(); main(runtime).catch((error) => { console.error("Failed to start server:", error); runtime.exit(1); }); //# sourceMappingURL=node.js.map