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
JavaScript
// 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