diff --git a/.gitignore b/.gitignore
index b24b6415..42ad7b8f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,3 +7,4 @@ node_modules/
# Build outputs
dist/
.next/
+node_modules
diff --git a/apps/app/electron/main-simplified.js b/apps/app/electron/main-simplified.js
new file mode 100644
index 00000000..f15e6946
--- /dev/null
+++ b/apps/app/electron/main-simplified.js
@@ -0,0 +1,241 @@
+/**
+ * Simplified Electron main process
+ *
+ * This version spawns the backend server and uses HTTP API for most operations.
+ * Only native features (dialogs, shell) use IPC.
+ */
+
+const path = require("path");
+const { spawn } = require("child_process");
+
+// Load environment variables from .env file
+require("dotenv").config({ path: path.join(__dirname, "../.env") });
+
+const { app, BrowserWindow, ipcMain, dialog, shell } = require("electron");
+
+let mainWindow = null;
+let serverProcess = null;
+const SERVER_PORT = 3008;
+
+// Get icon path - works in both dev and production
+function getIconPath() {
+ return app.isPackaged
+ ? path.join(process.resourcesPath, "app", "public", "logo.png")
+ : path.join(__dirname, "../public/logo.png");
+}
+
+/**
+ * Start the backend server
+ */
+async function startServer() {
+ const isDev = !app.isPackaged;
+
+ // Server entry point
+ const serverPath = isDev
+ ? path.join(__dirname, "../../server/dist/index.js")
+ : path.join(process.resourcesPath, "server", "index.js");
+
+ // Set environment variables for server
+ const env = {
+ ...process.env,
+ PORT: SERVER_PORT.toString(),
+ DATA_DIR: app.getPath("userData"),
+ };
+
+ console.log("[Electron] Starting backend server...");
+
+ serverProcess = spawn("node", [serverPath], {
+ env,
+ stdio: ["ignore", "pipe", "pipe"],
+ });
+
+ serverProcess.stdout.on("data", (data) => {
+ console.log(`[Server] ${data.toString().trim()}`);
+ });
+
+ serverProcess.stderr.on("data", (data) => {
+ console.error(`[Server Error] ${data.toString().trim()}`);
+ });
+
+ serverProcess.on("close", (code) => {
+ console.log(`[Server] Process exited with code ${code}`);
+ serverProcess = null;
+ });
+
+ // Wait for server to be ready
+ await waitForServer();
+}
+
+/**
+ * Wait for server to be available
+ */
+async function waitForServer(maxAttempts = 30) {
+ const http = require("http");
+
+ for (let i = 0; i < maxAttempts; i++) {
+ try {
+ await new Promise((resolve, reject) => {
+ const req = http.get(`http://localhost:${SERVER_PORT}/api/health`, (res) => {
+ if (res.statusCode === 200) {
+ resolve();
+ } else {
+ reject(new Error(`Status: ${res.statusCode}`));
+ }
+ });
+ req.on("error", reject);
+ req.setTimeout(1000, () => {
+ req.destroy();
+ reject(new Error("Timeout"));
+ });
+ });
+ console.log("[Electron] Server is ready");
+ return;
+ } catch {
+ await new Promise((r) => setTimeout(r, 500));
+ }
+ }
+
+ throw new Error("Server failed to start");
+}
+
+/**
+ * Create the main window
+ */
+function createWindow() {
+ mainWindow = new BrowserWindow({
+ width: 1400,
+ height: 900,
+ minWidth: 1024,
+ minHeight: 700,
+ icon: getIconPath(),
+ webPreferences: {
+ preload: path.join(__dirname, "preload-simplified.js"),
+ contextIsolation: true,
+ nodeIntegration: false,
+ },
+ titleBarStyle: "hiddenInset",
+ backgroundColor: "#0a0a0a",
+ });
+
+ // Load Next.js dev server in development or production build
+ const isDev = !app.isPackaged;
+ if (isDev) {
+ mainWindow.loadURL("http://localhost:3007");
+ if (process.env.OPEN_DEVTOOLS === "true") {
+ mainWindow.webContents.openDevTools();
+ }
+ } else {
+ mainWindow.loadFile(path.join(__dirname, "../.next/server/app/index.html"));
+ }
+
+ mainWindow.on("closed", () => {
+ mainWindow = null;
+ });
+}
+
+// App lifecycle
+app.whenReady().then(async () => {
+ // Set app icon (dock icon on macOS)
+ if (process.platform === "darwin" && app.dock) {
+ app.dock.setIcon(getIconPath());
+ }
+
+ try {
+ // Start backend server
+ await startServer();
+
+ // Create window
+ createWindow();
+ } catch (error) {
+ console.error("[Electron] Failed to start:", error);
+ app.quit();
+ }
+
+ app.on("activate", () => {
+ if (BrowserWindow.getAllWindows().length === 0) {
+ createWindow();
+ }
+ });
+});
+
+app.on("window-all-closed", () => {
+ if (process.platform !== "darwin") {
+ app.quit();
+ }
+});
+
+app.on("before-quit", () => {
+ // Kill server process
+ if (serverProcess) {
+ console.log("[Electron] Stopping server...");
+ serverProcess.kill();
+ serverProcess = null;
+ }
+});
+
+// ============================================
+// IPC Handlers - Only native features
+// ============================================
+
+// Native file dialogs
+ipcMain.handle("dialog:openDirectory", async () => {
+ const result = await dialog.showOpenDialog(mainWindow, {
+ properties: ["openDirectory", "createDirectory"],
+ });
+ return result;
+});
+
+ipcMain.handle("dialog:openFile", async (_, options = {}) => {
+ const result = await dialog.showOpenDialog(mainWindow, {
+ properties: ["openFile"],
+ ...options,
+ });
+ return result;
+});
+
+ipcMain.handle("dialog:saveFile", async (_, options = {}) => {
+ const result = await dialog.showSaveDialog(mainWindow, options);
+ return result;
+});
+
+// Shell operations
+ipcMain.handle("shell:openExternal", async (_, url) => {
+ try {
+ await shell.openExternal(url);
+ return { success: true };
+ } catch (error) {
+ return { success: false, error: error.message };
+ }
+});
+
+ipcMain.handle("shell:openPath", async (_, filePath) => {
+ try {
+ await shell.openPath(filePath);
+ return { success: true };
+ } catch (error) {
+ return { success: false, error: error.message };
+ }
+});
+
+// App info
+ipcMain.handle("app:getPath", async (_, name) => {
+ return app.getPath(name);
+});
+
+ipcMain.handle("app:getVersion", async () => {
+ return app.getVersion();
+});
+
+ipcMain.handle("app:isPackaged", async () => {
+ return app.isPackaged;
+});
+
+// Ping - for connection check
+ipcMain.handle("ping", async () => {
+ return "pong";
+});
+
+// Get server URL for HTTP client
+ipcMain.handle("server:getUrl", async () => {
+ return `http://localhost:${SERVER_PORT}`;
+});
diff --git a/apps/app/electron/preload-simplified.js b/apps/app/electron/preload-simplified.js
new file mode 100644
index 00000000..289d2cd7
--- /dev/null
+++ b/apps/app/electron/preload-simplified.js
@@ -0,0 +1,37 @@
+/**
+ * Simplified Electron preload script
+ *
+ * Only exposes native features (dialogs, shell) and server URL.
+ * All other operations go through HTTP API.
+ */
+
+const { contextBridge, ipcRenderer } = require("electron");
+
+// Expose minimal API for native features
+contextBridge.exposeInMainWorld("electronAPI", {
+ // Platform info
+ platform: process.platform,
+ isElectron: true,
+
+ // Connection check
+ ping: () => ipcRenderer.invoke("ping"),
+
+ // Get server URL for HTTP client
+ getServerUrl: () => ipcRenderer.invoke("server:getUrl"),
+
+ // Native dialogs - better UX than prompt()
+ openDirectory: () => ipcRenderer.invoke("dialog:openDirectory"),
+ openFile: (options) => ipcRenderer.invoke("dialog:openFile", options),
+ saveFile: (options) => ipcRenderer.invoke("dialog:saveFile", options),
+
+ // Shell operations
+ openExternalLink: (url) => ipcRenderer.invoke("shell:openExternal", url),
+ openPath: (filePath) => ipcRenderer.invoke("shell:openPath", filePath),
+
+ // App info
+ getPath: (name) => ipcRenderer.invoke("app:getPath", name),
+ getVersion: () => ipcRenderer.invoke("app:getVersion"),
+ isPackaged: () => ipcRenderer.invoke("app:isPackaged"),
+});
+
+console.log("[Preload] Electron API exposed (simplified mode)");
diff --git a/apps/app/src/app/page.tsx b/apps/app/src/app/page.tsx
index 0f50a5b0..d2a2a6f2 100644
--- a/apps/app/src/app/page.tsx
+++ b/apps/app/src/app/page.tsx
@@ -94,7 +94,7 @@ export default function Home() {
try {
const api = getElectronAPI();
const result = await api.ping();
- setIpcConnected(result === "pong" || result === "pong (mock)");
+ setIpcConnected(result === "pong");
} catch (error) {
console.error("IPC connection failed:", error);
setIpcConnected(false);
@@ -193,8 +193,8 @@ export default function Home() {
{/* Environment indicator */}
{isMounted && !isElectron() && (
-
- Web Mode (Mock IPC)
+
+ Web Mode
)}
@@ -210,8 +210,8 @@ export default function Home() {
{/* Environment indicator - only show after mount to prevent hydration issues */}
{isMounted && !isElectron() && (
-
- Web Mode (Mock IPC)
+
+ Web Mode
)}
diff --git a/apps/app/src/components/layout/sidebar.tsx b/apps/app/src/components/layout/sidebar.tsx
index 2c13361c..f5095351 100644
--- a/apps/app/src/components/layout/sidebar.tsx
+++ b/apps/app/src/components/layout/sidebar.tsx
@@ -413,14 +413,33 @@ export function Sidebar() {
return;
}
- const project = {
- id: `project-${Date.now()}`,
- name,
- path,
- lastOpened: new Date().toISOString(),
- };
+ // Check if project already exists (by path) to preserve theme and other settings
+ const existingProject = projects.find((p) => p.path === path);
+
+ let project: Project;
+ if (existingProject) {
+ // Update existing project, preserving theme and other properties
+ project = {
+ ...existingProject,
+ name, // Update name in case it changed
+ lastOpened: new Date().toISOString(),
+ };
+ // Update the project in the store (this will update the existing entry)
+ const updatedProjects = projects.map((p) =>
+ p.id === existingProject.id ? project : p
+ );
+ useAppStore.setState({ projects: updatedProjects });
+ } else {
+ // Create new project
+ project = {
+ id: `project-${Date.now()}`,
+ name,
+ path,
+ lastOpened: new Date().toISOString(),
+ };
+ addProject(project);
+ }
- addProject(project);
setCurrentProject(project);
// Check if app_spec.txt exists
@@ -455,7 +474,7 @@ export function Sidebar() {
});
}
}
- }, [addProject, setCurrentProject]);
+ }, [projects, addProject, setCurrentProject]);
const handleRestoreProject = useCallback(
(projectId: string) => {
diff --git a/apps/app/src/components/session-manager.tsx b/apps/app/src/components/session-manager.tsx
index d5f87140..7cafacdc 100644
--- a/apps/app/src/components/session-manager.tsx
+++ b/apps/app/src/components/session-manager.tsx
@@ -4,14 +4,13 @@ import { useState, useEffect } from "react";
import {
Card,
CardContent,
- CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { HotkeyButton } from "@/components/ui/hotkey-button";
import { Input } from "@/components/ui/input";
-import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
+import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
import {
Plus,
MessageSquare,
@@ -26,7 +25,6 @@ import {
import { cn } from "@/lib/utils";
import type { SessionListItem } from "@/types/electron";
import { useKeyboardShortcutsConfig } from "@/hooks/use-keyboard-shortcuts";
-import { useAppStore } from "@/store/app-store";
// Random session name generator
const adjectives = [
diff --git a/apps/app/src/components/ui/description-image-dropzone.tsx b/apps/app/src/components/ui/description-image-dropzone.tsx
index 7df30bf3..5b3b1a9c 100644
--- a/apps/app/src/components/ui/description-image-dropzone.tsx
+++ b/apps/app/src/components/ui/description-image-dropzone.tsx
@@ -1,6 +1,6 @@
"use client";
-import React, { useState, useRef, useCallback, useEffect } from "react";
+import React, { useState, useRef, useCallback } from "react";
import { cn } from "@/lib/utils";
import { ImageIcon, X, Loader2 } from "lucide-react";
import { Textarea } from "@/components/ui/textarea";
@@ -98,7 +98,7 @@ export function DescriptionImageDropZone({
});
};
- const saveImageToTemp = async (
+ const saveImageToTemp = useCallback(async (
base64Data: string,
filename: string,
mimeType: string
@@ -107,8 +107,8 @@ export function DescriptionImageDropZone({
const api = getElectronAPI();
// Check if saveImageToTemp method exists
if (!api.saveImageToTemp) {
- // Fallback for mock API - return a mock path in .automaker/images
- console.log("[DescriptionImageDropZone] Using mock path for image");
+ // Fallback path when saveImageToTemp is not available
+ console.log("[DescriptionImageDropZone] Using fallback path for image");
return `.automaker/images/${Date.now()}_${filename}`;
}
@@ -124,7 +124,7 @@ export function DescriptionImageDropZone({
console.error("[DescriptionImageDropZone] Error saving image:", error);
return null;
}
- };
+ }, [currentProject?.path]);
const processFiles = useCallback(
async (files: FileList) => {
@@ -193,7 +193,7 @@ export function DescriptionImageDropZone({
setIsProcessing(false);
},
- [disabled, isProcessing, images, maxFiles, maxFileSize, onImagesChange, previewImages]
+ [disabled, isProcessing, images, maxFiles, maxFileSize, onImagesChange, previewImages, saveImageToTemp]
);
const handleDrop = useCallback(
diff --git a/apps/app/src/components/views/agent-tools-view.tsx b/apps/app/src/components/views/agent-tools-view.tsx
index b9a4fada..94cd947a 100644
--- a/apps/app/src/components/views/agent-tools-view.tsx
+++ b/apps/app/src/components/views/agent-tools-view.tsx
@@ -149,12 +149,12 @@ export function AgentToolsView() {
setTerminalResult(null);
try {
- // Simulate agent requesting terminal command execution
- console.log(`[Agent Tool] Requesting to run command: ${terminalCommand}`);
+ // Terminal command simulation for demonstration purposes
+ console.log(`[Agent Tool] Simulating command: ${terminalCommand}`);
- // In mock mode, simulate terminal output
- // In real Electron mode, this would use child_process
- const mockOutputs: Record
= {
+ // Simulated outputs for common commands (preview mode)
+ // In production, the agent executes commands via Claude SDK
+ const simulatedOutputs: Record = {
ls: "app_spec.txt\nfeatures\nnode_modules\npackage.json\nsrc\ntests\ntsconfig.json",
pwd: currentProject?.path || "/Users/demo/project",
"echo hello": "hello",
@@ -168,8 +168,8 @@ export function AgentToolsView() {
await new Promise((resolve) => setTimeout(resolve, 500));
const output =
- mockOutputs[terminalCommand.toLowerCase()] ||
- `Command executed: ${terminalCommand}\n(Mock output - real execution requires Electron mode)`;
+ simulatedOutputs[terminalCommand.toLowerCase()] ||
+ `[Preview] ${terminalCommand}\n(Terminal commands are executed by the agent during feature implementation)`;
setTerminalResult({
success: true,
diff --git a/apps/app/src/components/views/board-view.tsx b/apps/app/src/components/views/board-view.tsx
index 057d7c22..edd819b1 100644
--- a/apps/app/src/components/views/board-view.tsx
+++ b/apps/app/src/components/views/board-view.tsx
@@ -394,22 +394,25 @@ export function BoardView() {
}, []);
// Load features using features API
+ // IMPORTANT: Do NOT add 'features' to dependency array - it would cause infinite reload loop
const loadFeatures = useCallback(async () => {
if (!currentProject) return;
const currentPath = currentProject.path;
const previousPath = prevProjectPathRef.current;
+ const isProjectSwitch = previousPath !== null && currentPath !== previousPath;
- // If project switched, clear features first to prevent cross-contamination
- // Also treat this as an initial load for the new project
- if (previousPath !== null && currentPath !== previousPath) {
+ // Get cached features from store (without adding to dependencies)
+ const cachedFeatures = useAppStore.getState().features;
+
+ // If project switched, mark it but don't clear features yet
+ // We'll clear after successful API load to prevent data loss
+ if (isProjectSwitch) {
console.log(
- `[BoardView] Project switch detected: ${previousPath} -> ${currentPath}, clearing features`
+ `[BoardView] Project switch detected: ${previousPath} -> ${currentPath}`
);
isSwitchingProjectRef.current = true;
isInitialLoadRef.current = true;
- setFeatures([]);
- setPersistedCategories([]); // Also clear categories
}
// Update the ref to track current project
@@ -424,6 +427,7 @@ export function BoardView() {
const api = getElectronAPI();
if (!api.features) {
console.error("[BoardView] Features API not available");
+ // Keep cached features if API is unavailable
return;
}
@@ -441,10 +445,31 @@ export function BoardView() {
thinkingLevel: f.thinkingLevel || "none",
})
);
+ // Successfully loaded features - now safe to set them
setFeatures(featuresWithIds);
+
+ // Only clear categories on project switch AFTER successful load
+ if (isProjectSwitch) {
+ setPersistedCategories([]);
+ }
+ } else if (!result.success && result.error) {
+ console.error("[BoardView] API returned error:", result.error);
+ // If it's a new project or the error indicates no features found,
+ // that's expected - start with empty array
+ if (isProjectSwitch) {
+ setFeatures([]);
+ setPersistedCategories([]);
+ }
+ // Otherwise keep cached features
}
} catch (error) {
console.error("Failed to load features:", error);
+ // On error, keep existing cached features for the current project
+ // Only clear on project switch if we have no features from server
+ if (isProjectSwitch && cachedFeatures.length === 0) {
+ setFeatures([]);
+ setPersistedCategories([]);
+ }
} finally {
setIsLoading(false);
isInitialLoadRef.current = false;
@@ -1475,8 +1500,14 @@ export function BoardView() {
if (isRunning) {
map.in_progress.push(f);
} else {
- // Otherwise, use the feature's status
- map[f.status].push(f);
+ // Otherwise, use the feature's status (fallback to backlog for unknown statuses)
+ const status = f.status as ColumnId;
+ if (map[status]) {
+ map[status].push(f);
+ } else {
+ // Unknown status, default to backlog
+ map.backlog.push(f);
+ }
}
});
diff --git a/apps/app/src/components/views/settings-view/api-keys/authentication-status-display.tsx b/apps/app/src/components/views/settings-view/api-keys/authentication-status-display.tsx
index 288c45ef..94a49338 100644
--- a/apps/app/src/components/views/settings-view/api-keys/authentication-status-display.tsx
+++ b/apps/app/src/components/views/settings-view/api-keys/authentication-status-display.tsx
@@ -61,12 +61,14 @@ export function AuthenticationStatusDisplay({
{claudeAuthStatus.method === "oauth_token_env"
? "Using CLAUDE_CODE_OAUTH_TOKEN"
: claudeAuthStatus.method === "oauth_token"
- ? "Using stored OAuth token"
+ ? "Using stored OAuth token (claude login)"
: claudeAuthStatus.method === "api_key_env"
? "Using ANTHROPIC_API_KEY"
: claudeAuthStatus.method === "api_key"
? "Using stored API key"
- : "Unknown method"}
+ : claudeAuthStatus.method === "credentials_file"
+ ? "Using credentials file"
+ : `Using ${claudeAuthStatus.method || "detected"} authentication`}
>
@@ -107,14 +109,16 @@ export function AuthenticationStatusDisplay({
- {codexAuthStatus.method === "cli_verified" ||
- codexAuthStatus.method === "cli_tokens"
+ {codexAuthStatus.method === "subscription"
+ ? "Using Codex subscription (Plus/Team)"
+ : codexAuthStatus.method === "cli_verified" ||
+ codexAuthStatus.method === "cli_tokens"
? "Using CLI login (OpenAI account)"
: codexAuthStatus.method === "api_key"
? "Using stored API key"
: codexAuthStatus.method === "env"
? "Using OPENAI_API_KEY"
- : "Unknown method"}
+ : `Using ${codexAuthStatus.method || "unknown"} authentication`}
>
diff --git a/apps/app/src/components/views/settings-view/hooks/use-cli-status.ts b/apps/app/src/components/views/settings-view/hooks/use-cli-status.ts
index 1fae138b..3f4422c4 100644
--- a/apps/app/src/components/views/settings-view/hooks/use-cli-status.ts
+++ b/apps/app/src/components/views/settings-view/hooks/use-cli-status.ts
@@ -68,19 +68,24 @@ export function useCliStatus() {
try {
const result = await api.setup.getClaudeStatus();
if (result.success && result.auth) {
- const auth = result.auth;
- // Validate method is one of the expected values, default to "none"
- const validMethods = ["oauth_token_env", "oauth_token", "api_key", "api_key_env", "none"] as const;
+ // Cast to extended type that includes server-added fields
+ const auth = result.auth as typeof result.auth & {
+ oauthTokenValid?: boolean;
+ apiKeyValid?: boolean;
+ };
+ // Map server method names to client method types
+ // Server returns: oauth_token_env, oauth_token, api_key_env, api_key, credentials_file, none
+ const validMethods = ["oauth_token_env", "oauth_token", "api_key", "api_key_env", "credentials_file", "none"] as const;
type AuthMethod = typeof validMethods[number];
const method: AuthMethod = validMethods.includes(auth.method as AuthMethod)
? (auth.method as AuthMethod)
- : "none";
+ : auth.authenticated ? "api_key" : "none"; // Default authenticated to api_key, not none
const authStatus = {
authenticated: auth.authenticated,
method,
hasCredentialsFile: auth.hasCredentialsFile ?? false,
- oauthTokenValid: auth.hasStoredOAuthToken || auth.hasEnvOAuthToken,
- apiKeyValid: auth.hasStoredApiKey || auth.hasEnvApiKey,
+ oauthTokenValid: auth.oauthTokenValid || auth.hasStoredOAuthToken || auth.hasEnvOAuthToken,
+ apiKeyValid: auth.apiKeyValid || auth.hasStoredApiKey || auth.hasEnvApiKey,
hasEnvOAuthToken: auth.hasEnvOAuthToken,
hasEnvApiKey: auth.hasEnvApiKey,
};
@@ -96,27 +101,30 @@ export function useCliStatus() {
try {
const result = await api.setup.getCodexStatus();
if (result.success && result.auth) {
- const auth = result.auth;
- // Determine method - prioritize cli_verified and cli_tokens over auth_file
- const method =
- auth.method === "cli_verified" || auth.method === "cli_tokens"
- ? auth.method === "cli_verified"
- ? ("cli_verified" as const)
- : ("cli_tokens" as const)
- : auth.method === "auth_file"
- ? ("api_key" as const)
- : auth.method === "env_var"
- ? ("env" as const)
- : ("none" as const);
+ // Cast to extended type that includes server-added fields
+ const auth = result.auth as typeof result.auth & {
+ hasSubscription?: boolean;
+ cliLoggedIn?: boolean;
+ hasEnvApiKey?: boolean;
+ };
+ // Map server method names to client method types
+ // Server returns: subscription, cli_verified, cli_tokens, api_key, env, none
+ const validMethods = ["subscription", "cli_verified", "cli_tokens", "api_key", "env", "none"] as const;
+ type CodexMethod = typeof validMethods[number];
+ const method: CodexMethod = validMethods.includes(auth.method as CodexMethod)
+ ? (auth.method as CodexMethod)
+ : auth.authenticated ? "api_key" : "none"; // Default authenticated to api_key
const authStatus = {
authenticated: auth.authenticated,
method,
- // Only set apiKeyValid for actual API key methods, not CLI login
+ // Only set apiKeyValid for actual API key methods, not CLI login or subscription
apiKeyValid:
- method === "cli_verified" || method === "cli_tokens"
+ method === "cli_verified" || method === "cli_tokens" || method === "subscription"
? undefined
- : auth.hasAuthFile || auth.hasEnvKey,
+ : auth.hasAuthFile || auth.hasEnvKey || auth.hasEnvApiKey,
+ hasSubscription: auth.hasSubscription,
+ cliLoggedIn: auth.cliLoggedIn,
};
setCodexAuthStatus(authStatus);
}
diff --git a/apps/app/src/components/views/welcome-view.tsx b/apps/app/src/components/views/welcome-view.tsx
index 6b6e4a4e..0cdef02d 100644
--- a/apps/app/src/components/views/welcome-view.tsx
+++ b/apps/app/src/components/views/welcome-view.tsx
@@ -21,7 +21,7 @@ import {
CardTitle,
} from "@/components/ui/card";
import { useAppStore } from "@/store/app-store";
-import { getElectronAPI } from "@/lib/electron";
+import { getElectronAPI, type Project } from "@/lib/electron";
import { initializeProject } from "@/lib/project-init";
import {
FolderOpen,
@@ -105,14 +105,34 @@ export function WelcomeView() {
return;
}
- const project = {
- id: `project-${Date.now()}`,
- name,
- path,
- lastOpened: new Date().toISOString(),
- };
+ // Check if project already exists (by path) to preserve theme and other settings
+ const existingProject = projects.find((p) => p.path === path);
+
+ let project: Project;
+ if (existingProject) {
+ // Update existing project, preserving theme and other properties
+ project = {
+ ...existingProject,
+ name, // Update name in case it changed
+ lastOpened: new Date().toISOString(),
+ };
+ // Update the project in the store (this will update the existing entry)
+ const updatedProjects = projects.map((p) =>
+ p.id === existingProject.id ? project : p
+ );
+ // We need to manually update projects since addProject would create a duplicate
+ useAppStore.setState({ projects: updatedProjects });
+ } else {
+ // Create new project
+ project = {
+ id: `project-${Date.now()}`,
+ name,
+ path,
+ lastOpened: new Date().toISOString(),
+ };
+ addProject(project);
+ }
- addProject(project);
setCurrentProject(project);
// Show initialization dialog if files were created
@@ -148,7 +168,7 @@ export function WelcomeView() {
setIsOpening(false);
}
},
- [addProject, setCurrentProject, analyzeProject]
+ [projects, addProject, setCurrentProject, analyzeProject]
);
const handleOpenProject = useCallback(async () => {
diff --git a/apps/app/src/lib/electron.ts b/apps/app/src/lib/electron.ts
index f27cd028..367e519e 100644
--- a/apps/app/src/lib/electron.ts
+++ b/apps/app/src/lib/electron.ts
@@ -443,13 +443,109 @@ export const isElectron = (): boolean => {
return typeof window !== "undefined" && window.isElectron === true;
};
-// Get the Electron API or a mock for web development
+// Check if backend server is available
+let serverAvailable: boolean | null = null;
+let serverCheckPromise: Promise
| null = null;
+
+export const checkServerAvailable = async (): Promise => {
+ if (serverAvailable !== null) return serverAvailable;
+ if (serverCheckPromise) return serverCheckPromise;
+
+ serverCheckPromise = (async () => {
+ try {
+ const serverUrl =
+ process.env.NEXT_PUBLIC_SERVER_URL || "http://localhost:3008";
+ const response = await fetch(`${serverUrl}/api/health`, {
+ method: "GET",
+ signal: AbortSignal.timeout(2000),
+ });
+ serverAvailable = response.ok;
+ } catch {
+ serverAvailable = false;
+ }
+ return serverAvailable;
+ })();
+
+ return serverCheckPromise;
+};
+
+// Reset server check (useful for retrying connection)
+export const resetServerCheck = (): void => {
+ serverAvailable = null;
+ serverCheckPromise = null;
+};
+
+// Cached HTTP client instance
+let httpClientInstance: ElectronAPI | null = null;
+
+// Check if we're in simplified Electron mode (HTTP backend instead of IPC)
+const isSimplifiedElectronMode = (): boolean => {
+ if (typeof window === "undefined") return false;
+ const api = window.electronAPI as any;
+ // Simplified mode has isElectron flag and getServerUrl but NOT readFile
+ return api?.isElectron === true &&
+ typeof api?.getServerUrl === "function" &&
+ typeof api?.readFile !== "function";
+};
+
+// Get the Electron API or HTTP client for web mode
+// In simplified Electron mode and web mode, uses HTTP client
export const getElectronAPI = (): ElectronAPI => {
+ // Check if we're in simplified Electron mode (uses HTTP backend)
+ if (isSimplifiedElectronMode()) {
+ if (typeof window !== "undefined" && !httpClientInstance) {
+ const { getHttpApiClient } = require("./http-api-client");
+ httpClientInstance = getHttpApiClient();
+ }
+ return httpClientInstance!;
+ }
+
+ // Full Electron API with IPC
if (isElectron() && window.electronAPI) {
return window.electronAPI;
}
- // Return mock API for web development
+ // Web mode: use HTTP API client
+ if (typeof window !== "undefined") {
+ if (!httpClientInstance) {
+ const { getHttpApiClient } = require("./http-api-client");
+ httpClientInstance = getHttpApiClient();
+ }
+ return httpClientInstance!;
+ }
+
+ // SSR fallback - this shouldn't be called during actual operation
+ throw new Error("Cannot get Electron API during SSR");
+};
+
+// Async version that checks server availability first
+export const getElectronAPIAsync = async (): Promise => {
+ // Simplified Electron mode or web mode: use HTTP client
+ if (isSimplifiedElectronMode() || !isElectron()) {
+ if (typeof window !== "undefined") {
+ const { getHttpApiClient } = await import("./http-api-client");
+ return getHttpApiClient();
+ }
+ }
+
+ // Full Electron API with IPC
+ if (isElectron() && window.electronAPI) {
+ return window.electronAPI;
+ }
+
+ throw new Error("Cannot get Electron API during SSR");
+};
+
+// Check if backend is connected (for showing connection status in UI)
+export const isBackendConnected = async (): Promise => {
+ // Full Electron mode: backend is built-in
+ if (isElectron() && !isSimplifiedElectronMode()) return true;
+ // Simplified Electron or web mode: check server availability
+ return await checkServerAvailable();
+};
+
+// Mock API for development/fallback when no backend is available
+const getMockElectronAPI = (): ElectronAPI => {
return {
ping: async () => "pong (mock)",
diff --git a/apps/app/src/lib/http-api-client.ts b/apps/app/src/lib/http-api-client.ts
new file mode 100644
index 00000000..123709de
--- /dev/null
+++ b/apps/app/src/lib/http-api-client.ts
@@ -0,0 +1,664 @@
+/**
+ * HTTP API Client for web mode
+ *
+ * This client provides the same API as the Electron IPC bridge,
+ * but communicates with the backend server via HTTP/WebSocket.
+ */
+
+import type {
+ ElectronAPI,
+ FileResult,
+ WriteResult,
+ ReaddirResult,
+ StatResult,
+ DialogResult,
+ SaveImageResult,
+ AutoModeAPI,
+ FeaturesAPI,
+ SuggestionsAPI,
+ SpecRegenerationAPI,
+ AutoModeEvent,
+ SuggestionsEvent,
+ SpecRegenerationEvent,
+ FeatureSuggestion,
+ SuggestionType,
+} from "./electron";
+import type { Feature } from "@/store/app-store";
+import type {
+ WorktreeAPI,
+ GitAPI,
+ ModelDefinition,
+ ProviderStatus,
+} from "@/types/electron";
+
+// Check if we're in simplified Electron mode (Electron with HTTP backend)
+const isSimplifiedElectronMode = (): boolean => {
+ if (typeof window === "undefined") return false;
+ const api = (window as any).electronAPI;
+ // Simplified mode has isElectron flag but limited methods
+ return api?.isElectron === true && typeof api?.getServerUrl === "function";
+};
+
+// Check if native Electron dialogs are available
+const hasNativeDialogs = (): boolean => {
+ if (typeof window === "undefined") return false;
+ const api = (window as any).electronAPI;
+ return typeof api?.openDirectory === "function";
+};
+
+// Server URL - configurable via environment variable or Electron
+const getServerUrl = async (): Promise => {
+ if (typeof window !== "undefined") {
+ // In simplified Electron mode, get URL from main process
+ const api = (window as any).electronAPI;
+ if (api?.getServerUrl) {
+ try {
+ return await api.getServerUrl();
+ } catch {
+ // Fall through to defaults
+ }
+ }
+
+ // Check for environment variable
+ const envUrl = process.env.NEXT_PUBLIC_SERVER_URL;
+ if (envUrl) return envUrl;
+
+ // Default to localhost for development
+ return "http://localhost:3008";
+ }
+ return "http://localhost:3008";
+};
+
+// Synchronous version for constructor (uses default, then updates)
+const getServerUrlSync = (): string => {
+ if (typeof window !== "undefined") {
+ const envUrl = process.env.NEXT_PUBLIC_SERVER_URL;
+ if (envUrl) return envUrl;
+ }
+ return "http://localhost:3008";
+};
+
+// Get API key from environment variable
+const getApiKey = (): string | null => {
+ if (typeof window !== "undefined") {
+ return process.env.NEXT_PUBLIC_AUTOMAKER_API_KEY || null;
+ }
+ return null;
+};
+
+type EventType =
+ | "agent:stream"
+ | "auto-mode:event"
+ | "suggestions:event"
+ | "spec-regeneration:event";
+
+type EventCallback = (payload: unknown) => void;
+
+/**
+ * HTTP API Client that implements ElectronAPI interface
+ */
+export class HttpApiClient implements ElectronAPI {
+ private serverUrl: string;
+ private ws: WebSocket | null = null;
+ private eventCallbacks: Map> = new Map();
+ private reconnectTimer: NodeJS.Timeout | null = null;
+ private isConnecting = false;
+
+ constructor() {
+ this.serverUrl = getServerUrlSync();
+ // Update server URL asynchronously if in Electron
+ this.initServerUrl();
+ this.connectWebSocket();
+ }
+
+ private async initServerUrl(): Promise {
+ const url = await getServerUrl();
+ if (url !== this.serverUrl) {
+ this.serverUrl = url;
+ // Reconnect WebSocket with new URL
+ if (this.ws) {
+ this.ws.close();
+ this.ws = null;
+ }
+ this.connectWebSocket();
+ }
+ }
+
+ private connectWebSocket(): void {
+ if (this.isConnecting || (this.ws && this.ws.readyState === WebSocket.OPEN)) {
+ return;
+ }
+
+ this.isConnecting = true;
+
+ try {
+ const wsUrl = this.serverUrl.replace(/^http/, "ws") + "/api/events";
+ this.ws = new WebSocket(wsUrl);
+
+ this.ws.onopen = () => {
+ console.log("[HttpApiClient] WebSocket connected");
+ this.isConnecting = false;
+ if (this.reconnectTimer) {
+ clearTimeout(this.reconnectTimer);
+ this.reconnectTimer = null;
+ }
+ };
+
+ this.ws.onmessage = (event) => {
+ try {
+ const data = JSON.parse(event.data);
+ const callbacks = this.eventCallbacks.get(data.type);
+ if (callbacks) {
+ callbacks.forEach((cb) => cb(data.payload));
+ }
+ } catch (error) {
+ console.error("[HttpApiClient] Failed to parse WebSocket message:", error);
+ }
+ };
+
+ this.ws.onclose = () => {
+ console.log("[HttpApiClient] WebSocket disconnected");
+ this.isConnecting = false;
+ this.ws = null;
+ // Attempt to reconnect after 5 seconds
+ if (!this.reconnectTimer) {
+ this.reconnectTimer = setTimeout(() => {
+ this.reconnectTimer = null;
+ this.connectWebSocket();
+ }, 5000);
+ }
+ };
+
+ this.ws.onerror = (error) => {
+ console.error("[HttpApiClient] WebSocket error:", error);
+ this.isConnecting = false;
+ };
+ } catch (error) {
+ console.error("[HttpApiClient] Failed to create WebSocket:", error);
+ this.isConnecting = false;
+ }
+ }
+
+ private subscribeToEvent(type: EventType, callback: EventCallback): () => void {
+ if (!this.eventCallbacks.has(type)) {
+ this.eventCallbacks.set(type, new Set());
+ }
+ this.eventCallbacks.get(type)!.add(callback);
+
+ // Ensure WebSocket is connected
+ this.connectWebSocket();
+
+ return () => {
+ const callbacks = this.eventCallbacks.get(type);
+ if (callbacks) {
+ callbacks.delete(callback);
+ }
+ };
+ }
+
+ private getHeaders(): Record {
+ const headers: Record = {
+ "Content-Type": "application/json",
+ };
+ const apiKey = getApiKey();
+ if (apiKey) {
+ headers["X-API-Key"] = apiKey;
+ }
+ return headers;
+ }
+
+ private async post(endpoint: string, body?: unknown): Promise {
+ const response = await fetch(`${this.serverUrl}${endpoint}`, {
+ method: "POST",
+ headers: this.getHeaders(),
+ body: body ? JSON.stringify(body) : undefined,
+ });
+ return response.json();
+ }
+
+ private async get(endpoint: string): Promise {
+ const headers = this.getHeaders();
+ const response = await fetch(`${this.serverUrl}${endpoint}`, { headers });
+ return response.json();
+ }
+
+ // Basic operations
+ async ping(): Promise {
+ const result = await this.get<{ status: string }>("/api/health");
+ return result.status === "ok" ? "pong" : "error";
+ }
+
+ async openExternalLink(url: string): Promise<{ success: boolean; error?: string }> {
+ // Use native Electron shell if available (better UX)
+ if (hasNativeDialogs()) {
+ const api = (window as any).electronAPI;
+ if (api.openExternalLink) {
+ return api.openExternalLink(url);
+ }
+ }
+ // Web mode: open in new tab
+ window.open(url, "_blank", "noopener,noreferrer");
+ return { success: true };
+ }
+
+ // File picker - uses native Electron dialogs when available, otherwise prompt
+ async openDirectory(): Promise {
+ // Use native Electron dialog if available
+ if (hasNativeDialogs()) {
+ const api = (window as any).electronAPI;
+ return api.openDirectory();
+ }
+
+ // Web mode: show a modal to let user type/paste path
+ const path = prompt("Enter project directory path:");
+ if (!path) {
+ return { canceled: true, filePaths: [] };
+ }
+
+ // Validate with server
+ const result = await this.post<{
+ success: boolean;
+ path?: string;
+ error?: string;
+ }>("/api/fs/validate-path", { filePath: path });
+
+ if (result.success && result.path) {
+ return { canceled: false, filePaths: [result.path] };
+ }
+
+ alert(result.error || "Invalid path");
+ return { canceled: true, filePaths: [] };
+ }
+
+ async openFile(options?: object): Promise {
+ // Use native Electron dialog if available
+ if (hasNativeDialogs()) {
+ const api = (window as any).electronAPI;
+ return api.openFile(options);
+ }
+
+ // Web mode: prompt for file path
+ const path = prompt("Enter file path:");
+ if (!path) {
+ return { canceled: true, filePaths: [] };
+ }
+
+ const result = await this.post<{ success: boolean; exists: boolean }>(
+ "/api/fs/exists",
+ { filePath: path }
+ );
+
+ if (result.success && result.exists) {
+ return { canceled: false, filePaths: [path] };
+ }
+
+ alert("File does not exist");
+ return { canceled: true, filePaths: [] };
+ }
+
+ // File system operations
+ async readFile(filePath: string): Promise {
+ return this.post("/api/fs/read", { filePath });
+ }
+
+ async writeFile(filePath: string, content: string): Promise {
+ return this.post("/api/fs/write", { filePath, content });
+ }
+
+ async mkdir(dirPath: string): Promise {
+ return this.post("/api/fs/mkdir", { dirPath });
+ }
+
+ async readdir(dirPath: string): Promise {
+ return this.post("/api/fs/readdir", { dirPath });
+ }
+
+ async exists(filePath: string): Promise {
+ const result = await this.post<{ success: boolean; exists: boolean }>(
+ "/api/fs/exists",
+ { filePath }
+ );
+ return result.exists;
+ }
+
+ async stat(filePath: string): Promise {
+ return this.post("/api/fs/stat", { filePath });
+ }
+
+ async deleteFile(filePath: string): Promise {
+ return this.post("/api/fs/delete", { filePath });
+ }
+
+ async trashItem(filePath: string): Promise {
+ // In web mode, trash is just delete
+ return this.deleteFile(filePath);
+ }
+
+ async getPath(name: string): Promise {
+ // Server provides data directory
+ if (name === "userData") {
+ const result = await this.get<{ dataDir: string }>("/api/health/detailed");
+ return result.dataDir || "/data";
+ }
+ return `/data/${name}`;
+ }
+
+ async saveImageToTemp(
+ data: string,
+ filename: string,
+ mimeType: string,
+ projectPath?: string
+ ): Promise {
+ return this.post("/api/fs/save-image", { data, filename, mimeType, projectPath });
+ }
+
+ // CLI checks - server-side
+ async checkClaudeCli(): Promise<{
+ success: boolean;
+ status?: string;
+ method?: string;
+ version?: string;
+ path?: string;
+ recommendation?: string;
+ installCommands?: {
+ macos?: string;
+ windows?: string;
+ linux?: string;
+ npm?: string;
+ };
+ error?: string;
+ }> {
+ return this.get("/api/setup/claude-status");
+ }
+
+ async checkCodexCli(): Promise<{
+ success: boolean;
+ status?: string;
+ method?: string;
+ version?: string;
+ path?: string;
+ hasApiKey?: boolean;
+ recommendation?: string;
+ installCommands?: {
+ macos?: string;
+ windows?: string;
+ linux?: string;
+ npm?: string;
+ };
+ error?: string;
+ }> {
+ return this.get("/api/setup/codex-status");
+ }
+
+ // Model API
+ model = {
+ getAvailable: async (): Promise<{
+ success: boolean;
+ models?: ModelDefinition[];
+ error?: string;
+ }> => {
+ return this.get("/api/models/available");
+ },
+ checkProviders: async (): Promise<{
+ success: boolean;
+ providers?: Record;
+ error?: string;
+ }> => {
+ return this.get("/api/models/providers");
+ },
+ };
+
+ async testOpenAIConnection(apiKey?: string): Promise<{
+ success: boolean;
+ message?: string;
+ error?: string;
+ }> {
+ return this.post("/api/setup/test-openai", { apiKey });
+ }
+
+ // Setup API
+ setup = {
+ getClaudeStatus: (): Promise<{
+ success: boolean;
+ status?: string;
+ installed?: boolean;
+ method?: string;
+ version?: string;
+ path?: string;
+ auth?: {
+ authenticated: boolean;
+ method: string;
+ hasCredentialsFile?: boolean;
+ hasToken?: boolean;
+ hasStoredOAuthToken?: boolean;
+ hasStoredApiKey?: boolean;
+ hasEnvApiKey?: boolean;
+ hasEnvOAuthToken?: boolean;
+ };
+ error?: string;
+ }> => this.get("/api/setup/claude-status"),
+
+ getCodexStatus: (): Promise<{
+ success: boolean;
+ status?: string;
+ method?: string;
+ version?: string;
+ path?: string;
+ auth?: {
+ authenticated: boolean;
+ method: string;
+ hasAuthFile: boolean;
+ hasEnvKey: boolean;
+ hasStoredApiKey?: boolean;
+ hasEnvApiKey?: boolean;
+ };
+ error?: string;
+ }> => this.get("/api/setup/codex-status"),
+
+ installClaude: (): Promise<{
+ success: boolean;
+ message?: string;
+ error?: string;
+ }> => this.post("/api/setup/install-claude"),
+
+ installCodex: (): Promise<{
+ success: boolean;
+ message?: string;
+ error?: string;
+ }> => this.post("/api/setup/install-codex"),
+
+ authClaude: (): Promise<{
+ success: boolean;
+ token?: string;
+ requiresManualAuth?: boolean;
+ terminalOpened?: boolean;
+ command?: string;
+ error?: string;
+ message?: string;
+ output?: string;
+ }> => this.post("/api/setup/auth-claude"),
+
+ authCodex: (apiKey?: string): Promise<{
+ success: boolean;
+ requiresManualAuth?: boolean;
+ command?: string;
+ error?: string;
+ }> => this.post("/api/setup/auth-codex", { apiKey }),
+
+ storeApiKey: (provider: string, apiKey: string): Promise<{
+ success: boolean;
+ error?: string;
+ }> => this.post("/api/setup/store-api-key", { provider, apiKey }),
+
+ getApiKeys: (): Promise<{
+ success: boolean;
+ hasAnthropicKey: boolean;
+ hasOpenAIKey: boolean;
+ hasGoogleKey: boolean;
+ }> => this.get("/api/setup/api-keys"),
+
+ configureCodexMcp: (projectPath: string): Promise<{
+ success: boolean;
+ configPath?: string;
+ error?: string;
+ }> => this.post("/api/setup/configure-codex-mcp", { projectPath }),
+
+ getPlatform: (): Promise<{
+ success: boolean;
+ platform: string;
+ arch: string;
+ homeDir: string;
+ isWindows: boolean;
+ isMac: boolean;
+ isLinux: boolean;
+ }> => this.get("/api/setup/platform"),
+
+ onInstallProgress: (callback: (progress: unknown) => void) => {
+ return this.subscribeToEvent("agent:stream", callback);
+ },
+
+ onAuthProgress: (callback: (progress: unknown) => void) => {
+ return this.subscribeToEvent("agent:stream", callback);
+ },
+ };
+
+ // Features API
+ features: FeaturesAPI = {
+ getAll: (projectPath: string) =>
+ this.post("/api/features/list", { projectPath }),
+ get: (projectPath: string, featureId: string) =>
+ this.post("/api/features/get", { projectPath, featureId }),
+ create: (projectPath: string, feature: Feature) =>
+ this.post("/api/features/create", { projectPath, feature }),
+ update: (projectPath: string, featureId: string, updates: Partial) =>
+ this.post("/api/features/update", { projectPath, featureId, updates }),
+ delete: (projectPath: string, featureId: string) =>
+ this.post("/api/features/delete", { projectPath, featureId }),
+ getAgentOutput: (projectPath: string, featureId: string) =>
+ this.post("/api/features/agent-output", { projectPath, featureId }),
+ };
+
+ // Auto Mode API
+ autoMode: AutoModeAPI = {
+ start: (projectPath: string, maxConcurrency?: number) =>
+ this.post("/api/auto-mode/start", { projectPath, maxConcurrency }),
+ stop: (projectPath: string) =>
+ this.post("/api/auto-mode/stop", { projectPath }),
+ stopFeature: (featureId: string) =>
+ this.post("/api/auto-mode/stop-feature", { featureId }),
+ status: (projectPath?: string) =>
+ this.post("/api/auto-mode/status", { projectPath }),
+ runFeature: (projectPath: string, featureId: string, useWorktrees?: boolean) =>
+ this.post("/api/auto-mode/run-feature", { projectPath, featureId, useWorktrees }),
+ verifyFeature: (projectPath: string, featureId: string) =>
+ this.post("/api/auto-mode/verify-feature", { projectPath, featureId }),
+ resumeFeature: (projectPath: string, featureId: string) =>
+ this.post("/api/auto-mode/resume-feature", { projectPath, featureId }),
+ contextExists: (projectPath: string, featureId: string) =>
+ this.post("/api/auto-mode/context-exists", { projectPath, featureId }),
+ analyzeProject: (projectPath: string) =>
+ this.post("/api/auto-mode/analyze-project", { projectPath }),
+ followUpFeature: (
+ projectPath: string,
+ featureId: string,
+ prompt: string,
+ imagePaths?: string[]
+ ) =>
+ this.post("/api/auto-mode/follow-up-feature", {
+ projectPath,
+ featureId,
+ prompt,
+ imagePaths,
+ }),
+ commitFeature: (projectPath: string, featureId: string) =>
+ this.post("/api/auto-mode/commit-feature", { projectPath, featureId }),
+ onEvent: (callback: (event: AutoModeEvent) => void) => {
+ return this.subscribeToEvent("auto-mode:event", callback as EventCallback);
+ },
+ };
+
+ // Worktree API
+ worktree: WorktreeAPI = {
+ revertFeature: (projectPath: string, featureId: string) =>
+ this.post("/api/worktree/revert", { projectPath, featureId }),
+ mergeFeature: (projectPath: string, featureId: string, options?: object) =>
+ this.post("/api/worktree/merge", { projectPath, featureId, options }),
+ getInfo: (projectPath: string, featureId: string) =>
+ this.post("/api/worktree/info", { projectPath, featureId }),
+ getStatus: (projectPath: string, featureId: string) =>
+ this.post("/api/worktree/status", { projectPath, featureId }),
+ list: (projectPath: string) =>
+ this.post("/api/worktree/list", { projectPath }),
+ getDiffs: (projectPath: string, featureId: string) =>
+ this.post("/api/worktree/diffs", { projectPath, featureId }),
+ getFileDiff: (projectPath: string, featureId: string, filePath: string) =>
+ this.post("/api/worktree/file-diff", { projectPath, featureId, filePath }),
+ };
+
+ // Git API
+ git: GitAPI = {
+ getDiffs: (projectPath: string) =>
+ this.post("/api/git/diffs", { projectPath }),
+ getFileDiff: (projectPath: string, filePath: string) =>
+ this.post("/api/git/file-diff", { projectPath, filePath }),
+ };
+
+ // Suggestions API
+ suggestions: SuggestionsAPI = {
+ generate: (projectPath: string, suggestionType?: SuggestionType) =>
+ this.post("/api/suggestions/generate", { projectPath, suggestionType }),
+ stop: () => this.post("/api/suggestions/stop"),
+ status: () => this.get("/api/suggestions/status"),
+ onEvent: (callback: (event: SuggestionsEvent) => void) => {
+ return this.subscribeToEvent("suggestions:event", callback as EventCallback);
+ },
+ };
+
+ // Spec Regeneration API
+ specRegeneration: SpecRegenerationAPI = {
+ create: (projectPath: string, projectOverview: string, generateFeatures?: boolean) =>
+ this.post("/api/spec-regeneration/create", {
+ projectPath,
+ projectOverview,
+ generateFeatures,
+ }),
+ generate: (projectPath: string, projectDefinition: string) =>
+ this.post("/api/spec-regeneration/generate", { projectPath, projectDefinition }),
+ generateFeatures: (projectPath: string) =>
+ this.post("/api/spec-regeneration/generate-features", { projectPath }),
+ stop: () => this.post("/api/spec-regeneration/stop"),
+ status: () => this.get("/api/spec-regeneration/status"),
+ onEvent: (callback: (event: SpecRegenerationEvent) => void) => {
+ return this.subscribeToEvent(
+ "spec-regeneration:event",
+ callback as EventCallback
+ );
+ },
+ };
+
+ // Running Agents API
+ runningAgents = {
+ getAll: (): Promise<{
+ success: boolean;
+ runningAgents?: Array<{
+ featureId: string;
+ projectPath: string;
+ projectName: string;
+ isAutoMode: boolean;
+ }>;
+ totalCount?: number;
+ autoLoopRunning?: boolean;
+ error?: string;
+ }> => this.get("/api/running-agents"),
+ };
+}
+
+// Singleton instance
+let httpApiClientInstance: HttpApiClient | null = null;
+
+export function getHttpApiClient(): HttpApiClient {
+ if (!httpApiClientInstance) {
+ httpApiClientInstance = new HttpApiClient();
+ }
+ return httpApiClientInstance;
+}
diff --git a/apps/app/src/store/app-store.ts b/apps/app/src/store/app-store.ts
index ba8a1130..8b35788a 100644
--- a/apps/app/src/store/app-store.ts
+++ b/apps/app/src/store/app-store.ts
@@ -1138,26 +1138,32 @@ export const useAppStore = create()(
{
name: "automaker-storage",
partialize: (state) => ({
+ // Project management
projects: state.projects,
currentProject: state.currentProject,
trashedProjects: state.trashedProjects,
projectHistory: state.projectHistory,
projectHistoryIndex: state.projectHistoryIndex,
+ // Features - cached locally for faster hydration (authoritative source is server)
+ features: state.features,
+ // UI state
currentView: state.currentView,
theme: state.theme,
sidebarOpen: state.sidebarOpen,
- apiKeys: state.apiKeys,
- chatSessions: state.chatSessions,
chatHistoryOpen: state.chatHistoryOpen,
+ kanbanCardDetailLevel: state.kanbanCardDetailLevel,
+ // Settings
+ apiKeys: state.apiKeys,
maxConcurrency: state.maxConcurrency,
autoModeByProject: state.autoModeByProject,
- kanbanCardDetailLevel: state.kanbanCardDetailLevel,
defaultSkipTests: state.defaultSkipTests,
useWorktrees: state.useWorktrees,
showProfilesOnly: state.showProfilesOnly,
keyboardShortcuts: state.keyboardShortcuts,
muteDoneSound: state.muteDoneSound,
+ // Profiles and sessions
aiProfiles: state.aiProfiles,
+ chatSessions: state.chatSessions,
lastSelectedSessionByProject: state.lastSelectedSessionByProject,
}),
}
diff --git a/apps/app/src/store/setup-store.ts b/apps/app/src/store/setup-store.ts
index 8609d339..0265e64b 100644
--- a/apps/app/src/store/setup-store.ts
+++ b/apps/app/src/store/setup-store.ts
@@ -10,10 +10,19 @@ export interface CliStatus {
error?: string;
}
+// Claude Auth Method - all possible authentication sources
+export type ClaudeAuthMethod =
+ | "oauth_token_env" // CLAUDE_CODE_OAUTH_TOKEN environment variable
+ | "oauth_token" // Stored OAuth token from claude login
+ | "api_key_env" // ANTHROPIC_API_KEY environment variable
+ | "api_key" // Manually stored API key
+ | "credentials_file" // Generic credentials file detection
+ | "none";
+
// Claude Auth Status
export interface ClaudeAuthStatus {
authenticated: boolean;
- method: "oauth_token_env" | "oauth_token" | "api_key" | "api_key_env" | "none";
+ method: ClaudeAuthMethod;
hasCredentialsFile?: boolean;
oauthTokenValid?: boolean;
apiKeyValid?: boolean;
@@ -22,12 +31,23 @@ export interface ClaudeAuthStatus {
error?: string;
}
+// Codex Auth Method - all possible authentication sources
+export type CodexAuthMethod =
+ | "subscription" // Codex/OpenAI Plus or Team subscription
+ | "cli_verified" // CLI logged in with OpenAI account
+ | "cli_tokens" // CLI with stored access tokens
+ | "api_key" // Manually stored API key
+ | "env" // OPENAI_API_KEY environment variable
+ | "none";
+
// Codex Auth Status
export interface CodexAuthStatus {
authenticated: boolean;
- method: "api_key" | "env" | "cli_verified" | "cli_tokens" | "none";
+ method: CodexAuthMethod;
apiKeyValid?: boolean;
mcpConfigured?: boolean;
+ hasSubscription?: boolean;
+ cliLoggedIn?: boolean;
error?: string;
}
diff --git a/apps/server/.env.example b/apps/server/.env.example
new file mode 100644
index 00000000..6ce580b1
--- /dev/null
+++ b/apps/server/.env.example
@@ -0,0 +1,45 @@
+# Automaker Server Configuration
+# Copy this file to .env and configure your settings
+
+# ============================================
+# REQUIRED
+# ============================================
+
+# Your Anthropic API key for Claude models
+ANTHROPIC_API_KEY=sk-ant-...
+
+# ============================================
+# OPTIONAL - Security
+# ============================================
+
+# API key for authenticating requests (leave empty to disable auth)
+# If set, all API requests must include X-API-Key header
+AUTOMAKER_API_KEY=
+
+# Restrict file operations to these directories (comma-separated)
+# Important for security in multi-tenant environments
+ALLOWED_PROJECT_DIRS=/home/user/projects,/var/www
+
+# CORS origin - which domains can access the API
+# Use "*" for development, set specific origin for production
+CORS_ORIGIN=*
+
+# ============================================
+# OPTIONAL - Server
+# ============================================
+
+# Port to run the server on
+PORT=3008
+
+# Data directory for sessions and metadata
+DATA_DIR=./data
+
+# ============================================
+# OPTIONAL - Additional AI Providers
+# ============================================
+
+# OpenAI API key (for Codex CLI support)
+OPENAI_API_KEY=
+
+# Google API key (for future Gemini support)
+GOOGLE_API_KEY=
diff --git a/apps/server/Dockerfile b/apps/server/Dockerfile
new file mode 100644
index 00000000..6f909af4
--- /dev/null
+++ b/apps/server/Dockerfile
@@ -0,0 +1,55 @@
+# Automaker Backend Server
+# Multi-stage build for minimal production image
+
+# Build stage
+FROM node:20-alpine AS builder
+
+WORKDIR /app
+
+# Copy package files
+COPY package*.json ./
+COPY apps/server/package*.json ./apps/server/
+
+# Install dependencies
+RUN npm ci --workspace=apps/server
+
+# Copy source
+COPY apps/server ./apps/server
+
+# Build TypeScript
+RUN npm run build --workspace=apps/server
+
+# Production stage
+FROM node:20-alpine
+
+WORKDIR /app
+
+# Create non-root user
+RUN addgroup -g 1001 -S automaker && \
+ adduser -S automaker -u 1001
+
+# Copy built files and production dependencies
+COPY --from=builder /app/apps/server/dist ./dist
+COPY --from=builder /app/apps/server/package*.json ./
+COPY --from=builder /app/node_modules ./node_modules
+
+# Create data directory
+RUN mkdir -p /data && chown automaker:automaker /data
+
+# Switch to non-root user
+USER automaker
+
+# Environment variables
+ENV NODE_ENV=production
+ENV PORT=3008
+ENV DATA_DIR=/data
+
+# Expose port
+EXPOSE 3008
+
+# Health check
+HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
+ CMD wget --no-verbose --tries=1 --spider http://localhost:3008/api/health || exit 1
+
+# Start server
+CMD ["node", "dist/index.js"]
diff --git a/apps/server/package.json b/apps/server/package.json
new file mode 100644
index 00000000..6e16d10b
--- /dev/null
+++ b/apps/server/package.json
@@ -0,0 +1,29 @@
+{
+ "name": "@automaker/server",
+ "version": "0.1.0",
+ "description": "Backend server for Automaker - provides API for both web and Electron modes",
+ "private": true,
+ "type": "module",
+ "main": "dist/index.js",
+ "scripts": {
+ "dev": "tsx watch src/index.ts",
+ "build": "tsc",
+ "start": "node dist/index.js",
+ "lint": "eslint src/"
+ },
+ "dependencies": {
+ "@anthropic-ai/claude-agent-sdk": "^0.1.61",
+ "cors": "^2.8.5",
+ "dotenv": "^17.2.3",
+ "express": "^5.1.0",
+ "ws": "^8.18.0"
+ },
+ "devDependencies": {
+ "@types/cors": "^2.8.18",
+ "@types/express": "^5.0.1",
+ "@types/node": "^20",
+ "@types/ws": "^8.18.1",
+ "tsx": "^4.19.4",
+ "typescript": "^5"
+ }
+}
diff --git a/apps/server/src/index.ts b/apps/server/src/index.ts
new file mode 100644
index 00000000..6590d13e
--- /dev/null
+++ b/apps/server/src/index.ts
@@ -0,0 +1,165 @@
+/**
+ * Automaker Backend Server
+ *
+ * Provides HTTP/WebSocket API for both web and Electron modes.
+ * In Electron mode, this server runs locally.
+ * In web mode, this server runs on a remote host.
+ */
+
+import express from "express";
+import cors from "cors";
+import { WebSocketServer, WebSocket } from "ws";
+import { createServer } from "http";
+import dotenv from "dotenv";
+
+import { createEventEmitter, type EventEmitter } from "./lib/events.js";
+import { initAllowedPaths } from "./lib/security.js";
+import { authMiddleware, getAuthStatus } from "./lib/auth.js";
+import { createFsRoutes } from "./routes/fs.js";
+import { createHealthRoutes } from "./routes/health.js";
+import { createAgentRoutes } from "./routes/agent.js";
+import { createSessionsRoutes } from "./routes/sessions.js";
+import { createFeaturesRoutes } from "./routes/features.js";
+import { createAutoModeRoutes } from "./routes/auto-mode.js";
+import { createWorktreeRoutes } from "./routes/worktree.js";
+import { createGitRoutes } from "./routes/git.js";
+import { createSetupRoutes } from "./routes/setup.js";
+import { createSuggestionsRoutes } from "./routes/suggestions.js";
+import { createModelsRoutes } from "./routes/models.js";
+import { createSpecRegenerationRoutes } from "./routes/spec-regeneration.js";
+import { createRunningAgentsRoutes } from "./routes/running-agents.js";
+import { AgentService } from "./services/agent-service.js";
+import { FeatureLoader } from "./services/feature-loader.js";
+
+// Load environment variables
+dotenv.config();
+
+const PORT = parseInt(process.env.PORT || "3008", 10);
+const DATA_DIR = process.env.DATA_DIR || "./data";
+
+// Check for required environment variables
+const hasAnthropicKey = !!process.env.ANTHROPIC_API_KEY;
+const hasOAuthToken = !!process.env.CLAUDE_CODE_OAUTH_TOKEN;
+
+if (!hasAnthropicKey) {
+ console.warn(`
+╔═══════════════════════════════════════════════════════════════════════╗
+║ ⚠️ WARNING: ANTHROPIC_API_KEY not set ║
+║ ║
+║ The Claude Agent SDK requires ANTHROPIC_API_KEY to function. ║
+║ ${hasOAuthToken ? ' You have CLAUDE_CODE_OAUTH_TOKEN set - this is for CLI auth only.' : ''}
+║ ║
+║ Set your API key: ║
+║ export ANTHROPIC_API_KEY="sk-ant-..." ║
+║ ║
+║ Or add to apps/server/.env: ║
+║ ANTHROPIC_API_KEY=sk-ant-... ║
+╚═══════════════════════════════════════════════════════════════════════╝
+`);
+} else {
+ console.log("[Server] ✓ ANTHROPIC_API_KEY detected");
+}
+
+// Initialize security
+initAllowedPaths();
+
+// Create Express app
+const app = express();
+
+// Middleware
+app.use(
+ cors({
+ origin: process.env.CORS_ORIGIN || "*",
+ credentials: true,
+ })
+);
+app.use(express.json({ limit: "50mb" }));
+
+// Create shared event emitter for streaming
+const events: EventEmitter = createEventEmitter();
+
+// Create services
+const agentService = new AgentService(DATA_DIR, events);
+const featureLoader = new FeatureLoader();
+
+// Initialize services
+(async () => {
+ await agentService.initialize();
+ console.log("[Server] Agent service initialized");
+})();
+
+// Mount API routes - health is unauthenticated for monitoring
+app.use("/api/health", createHealthRoutes());
+
+// Apply authentication to all other routes
+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(events));
+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());
+
+// Create HTTP server
+const server = createServer(app);
+
+// WebSocket server for streaming events
+const wss = new WebSocketServer({ server, path: "/api/events" });
+
+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) => {
+ if (ws.readyState === WebSocket.OPEN) {
+ ws.send(JSON.stringify({ type, payload }));
+ }
+ });
+
+ ws.on("close", () => {
+ console.log("[WebSocket] Client disconnected");
+ unsubscribe();
+ });
+
+ ws.on("error", (error) => {
+ console.error("[WebSocket] Error:", error);
+ unsubscribe();
+ });
+});
+
+// Start server
+server.listen(PORT, () => {
+ console.log(`
+╔═══════════════════════════════════════════════════════╗
+║ Automaker Backend Server ║
+╠═══════════════════════════════════════════════════════╣
+║ HTTP API: http://localhost:${PORT} ║
+║ WebSocket: ws://localhost:${PORT}/api/events ║
+║ Health: http://localhost:${PORT}/api/health ║
+╚═══════════════════════════════════════════════════════╝
+`);
+});
+
+// Graceful shutdown
+process.on("SIGTERM", () => {
+ console.log("SIGTERM received, shutting down...");
+ server.close(() => {
+ console.log("Server closed");
+ process.exit(0);
+ });
+});
+
+process.on("SIGINT", () => {
+ console.log("SIGINT received, shutting down...");
+ server.close(() => {
+ console.log("Server closed");
+ process.exit(0);
+ });
+});
diff --git a/apps/server/src/lib/auth.ts b/apps/server/src/lib/auth.ts
new file mode 100644
index 00000000..331af2cf
--- /dev/null
+++ b/apps/server/src/lib/auth.ts
@@ -0,0 +1,62 @@
+/**
+ * Authentication middleware for API security
+ *
+ * Supports API key authentication via header or environment variable.
+ */
+
+import type { Request, Response, NextFunction } from "express";
+
+// API key from environment (optional - if not set, auth is disabled)
+const API_KEY = process.env.AUTOMAKER_API_KEY;
+
+/**
+ * Authentication middleware
+ *
+ * If AUTOMAKER_API_KEY is set, requires matching key in X-API-Key header.
+ * If not set, allows all requests (development mode).
+ */
+export function authMiddleware(req: Request, res: Response, next: NextFunction): void {
+ // If no API key is configured, allow all requests
+ if (!API_KEY) {
+ next();
+ return;
+ }
+
+ // Check for API key in header
+ const providedKey = req.headers["x-api-key"] as string | undefined;
+
+ if (!providedKey) {
+ res.status(401).json({
+ success: false,
+ error: "Authentication required. Provide X-API-Key header.",
+ });
+ return;
+ }
+
+ if (providedKey !== API_KEY) {
+ res.status(403).json({
+ success: false,
+ error: "Invalid API key.",
+ });
+ return;
+ }
+
+ next();
+}
+
+/**
+ * Check if authentication is enabled
+ */
+export function isAuthEnabled(): boolean {
+ return !!API_KEY;
+}
+
+/**
+ * Get authentication status for health endpoint
+ */
+export function getAuthStatus(): { enabled: boolean; method: string } {
+ return {
+ enabled: !!API_KEY,
+ method: API_KEY ? "api_key" : "none",
+ };
+}
diff --git a/apps/server/src/lib/events.ts b/apps/server/src/lib/events.ts
new file mode 100644
index 00000000..d6f7036e
--- /dev/null
+++ b/apps/server/src/lib/events.ts
@@ -0,0 +1,57 @@
+/**
+ * Event emitter for streaming events to WebSocket clients
+ */
+
+export type EventType =
+ | "agent:stream"
+ | "auto-mode:event"
+ | "auto-mode:started"
+ | "auto-mode:stopped"
+ | "auto-mode:idle"
+ | "auto-mode:error"
+ | "feature:started"
+ | "feature:completed"
+ | "feature:stopped"
+ | "feature:error"
+ | "feature:progress"
+ | "feature:tool-use"
+ | "feature:follow-up-started"
+ | "feature:follow-up-completed"
+ | "feature:verified"
+ | "feature:committed"
+ | "project:analysis-started"
+ | "project:analysis-progress"
+ | "project:analysis-completed"
+ | "project:analysis-error"
+ | "suggestions:event"
+ | "spec-regeneration:event";
+
+export type EventCallback = (type: EventType, payload: unknown) => void;
+
+export interface EventEmitter {
+ emit: (type: EventType, payload: unknown) => void;
+ subscribe: (callback: EventCallback) => () => void;
+}
+
+export function createEventEmitter(): EventEmitter {
+ const subscribers = new Set();
+
+ return {
+ emit(type: EventType, payload: unknown) {
+ for (const callback of subscribers) {
+ try {
+ callback(type, payload);
+ } catch (error) {
+ console.error("Error in event subscriber:", error);
+ }
+ }
+ },
+
+ subscribe(callback: EventCallback) {
+ subscribers.add(callback);
+ return () => {
+ subscribers.delete(callback);
+ };
+ },
+ };
+}
diff --git a/apps/server/src/lib/security.ts b/apps/server/src/lib/security.ts
new file mode 100644
index 00000000..eac0af00
--- /dev/null
+++ b/apps/server/src/lib/security.ts
@@ -0,0 +1,72 @@
+/**
+ * Security utilities for path validation
+ */
+
+import path from "path";
+
+// Allowed project directories - loaded from environment
+const allowedPaths = new Set();
+
+/**
+ * Initialize allowed paths from environment variable
+ */
+export function initAllowedPaths(): void {
+ const dirs = process.env.ALLOWED_PROJECT_DIRS;
+ if (dirs) {
+ for (const dir of dirs.split(",")) {
+ const trimmed = dir.trim();
+ if (trimmed) {
+ allowedPaths.add(path.resolve(trimmed));
+ }
+ }
+ }
+
+ // Always allow the data directory
+ const dataDir = process.env.DATA_DIR;
+ if (dataDir) {
+ allowedPaths.add(path.resolve(dataDir));
+ }
+}
+
+/**
+ * Add a path to the allowed list
+ */
+export function addAllowedPath(filePath: string): void {
+ allowedPaths.add(path.resolve(filePath));
+}
+
+/**
+ * Check if a path is allowed
+ */
+export function isPathAllowed(filePath: string): boolean {
+ const resolved = path.resolve(filePath);
+
+ // Check if the path is under any allowed directory
+ for (const allowed of allowedPaths) {
+ if (resolved.startsWith(allowed + path.sep) || resolved === allowed) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+/**
+ * Validate a path and throw if not allowed
+ */
+export function validatePath(filePath: string): string {
+ const resolved = path.resolve(filePath);
+
+ if (!isPathAllowed(resolved)) {
+ throw new Error(`Access denied: ${filePath} is not in an allowed directory`);
+ }
+
+ return resolved;
+}
+
+/**
+ * Get list of allowed paths (for debugging)
+ */
+export function getAllowedPaths(): string[] {
+ return Array.from(allowedPaths);
+}
diff --git a/apps/server/src/routes/agent.ts b/apps/server/src/routes/agent.ts
new file mode 100644
index 00000000..966b8916
--- /dev/null
+++ b/apps/server/src/routes/agent.ts
@@ -0,0 +1,132 @@
+/**
+ * Agent routes - HTTP API for Claude agent interactions
+ */
+
+import { Router, type Request, type Response } from "express";
+import { AgentService } from "../services/agent-service.js";
+import type { EventEmitter } from "../lib/events.js";
+
+export function createAgentRoutes(
+ agentService: AgentService,
+ _events: EventEmitter
+): Router {
+ const router = Router();
+
+ // Start a conversation
+ router.post("/start", async (req: Request, res: Response) => {
+ try {
+ const { sessionId, workingDirectory } = req.body as {
+ sessionId: string;
+ workingDirectory?: string;
+ };
+
+ if (!sessionId) {
+ res.status(400).json({ success: false, error: "sessionId is required" });
+ return;
+ }
+
+ const result = await agentService.startConversation({
+ sessionId,
+ workingDirectory,
+ });
+
+ res.json(result);
+ } catch (error) {
+ const message = error instanceof Error ? error.message : "Unknown error";
+ res.status(500).json({ success: false, error: message });
+ }
+ });
+
+ // Send a message
+ router.post("/send", async (req: Request, res: Response) => {
+ try {
+ const { sessionId, message, workingDirectory, imagePaths } = req.body as {
+ sessionId: string;
+ message: string;
+ workingDirectory?: string;
+ imagePaths?: string[];
+ };
+
+ if (!sessionId || !message) {
+ res
+ .status(400)
+ .json({ success: false, error: "sessionId and message are required" });
+ return;
+ }
+
+ // Start the message processing (don't await - it streams via WebSocket)
+ agentService
+ .sendMessage({
+ sessionId,
+ message,
+ workingDirectory,
+ imagePaths,
+ })
+ .catch((error) => {
+ console.error("[Agent Route] Error sending message:", error);
+ });
+
+ // Return immediately - responses come via WebSocket
+ res.json({ success: true, message: "Message sent" });
+ } catch (error) {
+ const message = error instanceof Error ? error.message : "Unknown error";
+ res.status(500).json({ success: false, error: message });
+ }
+ });
+
+ // Get conversation history
+ router.post("/history", async (req: Request, res: Response) => {
+ try {
+ const { sessionId } = req.body as { sessionId: string };
+
+ if (!sessionId) {
+ res.status(400).json({ success: false, error: "sessionId is required" });
+ return;
+ }
+
+ const result = agentService.getHistory(sessionId);
+ res.json(result);
+ } catch (error) {
+ const message = error instanceof Error ? error.message : "Unknown error";
+ res.status(500).json({ success: false, error: message });
+ }
+ });
+
+ // Stop execution
+ router.post("/stop", async (req: Request, res: Response) => {
+ try {
+ const { sessionId } = req.body as { sessionId: string };
+
+ if (!sessionId) {
+ res.status(400).json({ success: false, error: "sessionId is required" });
+ return;
+ }
+
+ const result = await agentService.stopExecution(sessionId);
+ res.json(result);
+ } catch (error) {
+ const message = error instanceof Error ? error.message : "Unknown error";
+ res.status(500).json({ success: false, error: message });
+ }
+ });
+
+ // Clear conversation
+ router.post("/clear", async (req: Request, res: Response) => {
+ try {
+ const { sessionId } = req.body as { sessionId: string };
+
+ if (!sessionId) {
+ res.status(400).json({ success: false, error: "sessionId is required" });
+ return;
+ }
+
+ const result = await agentService.clearSession(sessionId);
+ res.json(result);
+ } catch (error) {
+ const message = error instanceof Error ? error.message : "Unknown error";
+ res.status(500).json({ success: false, error: message });
+ }
+ });
+
+ return router;
+}
diff --git a/apps/server/src/routes/auto-mode.ts b/apps/server/src/routes/auto-mode.ts
new file mode 100644
index 00000000..408b0d96
--- /dev/null
+++ b/apps/server/src/routes/auto-mode.ts
@@ -0,0 +1,263 @@
+/**
+ * Auto Mode routes - HTTP API for autonomous feature implementation
+ *
+ * Uses the AutoModeService for real feature execution with Claude Agent SDK
+ */
+
+import { Router, type Request, type Response } from "express";
+import type { EventEmitter } from "../lib/events.js";
+import { AutoModeService } from "../services/auto-mode-service.js";
+
+export function createAutoModeRoutes(events: EventEmitter): Router {
+ const router = Router();
+ const autoModeService = new AutoModeService(events);
+
+ // Start auto mode loop
+ router.post("/start", async (req: Request, res: Response) => {
+ try {
+ const { projectPath, maxConcurrency } = req.body as {
+ projectPath: string;
+ maxConcurrency?: number;
+ };
+
+ if (!projectPath) {
+ res.status(400).json({ success: false, error: "projectPath is required" });
+ return;
+ }
+
+ await autoModeService.startAutoLoop(projectPath, maxConcurrency || 3);
+ res.json({ success: true });
+ } catch (error) {
+ const message = error instanceof Error ? error.message : "Unknown error";
+ res.status(500).json({ success: false, error: message });
+ }
+ });
+
+ // Stop auto mode loop
+ router.post("/stop", async (req: Request, res: Response) => {
+ try {
+ const runningCount = await autoModeService.stopAutoLoop();
+ res.json({ success: true, runningFeatures: runningCount });
+ } catch (error) {
+ const message = error instanceof Error ? error.message : "Unknown error";
+ res.status(500).json({ success: false, error: message });
+ }
+ });
+
+ // Stop a specific feature
+ router.post("/stop-feature", async (req: Request, res: Response) => {
+ try {
+ const { featureId } = req.body as { featureId: string };
+
+ if (!featureId) {
+ res.status(400).json({ success: false, error: "featureId is required" });
+ return;
+ }
+
+ const stopped = await autoModeService.stopFeature(featureId);
+ res.json({ success: true, stopped });
+ } catch (error) {
+ const message = error instanceof Error ? error.message : "Unknown error";
+ res.status(500).json({ success: false, error: message });
+ }
+ });
+
+ // Get auto mode status
+ router.post("/status", async (req: Request, res: Response) => {
+ try {
+ const status = autoModeService.getStatus();
+ res.json({
+ success: true,
+ ...status,
+ });
+ } catch (error) {
+ const message = error instanceof Error ? error.message : "Unknown error";
+ res.status(500).json({ success: false, error: message });
+ }
+ });
+
+ // Run a single feature
+ router.post("/run-feature", async (req: Request, res: Response) => {
+ try {
+ const { projectPath, featureId, useWorktrees } = req.body as {
+ projectPath: string;
+ featureId: string;
+ useWorktrees?: boolean;
+ };
+
+ if (!projectPath || !featureId) {
+ res
+ .status(400)
+ .json({ success: false, error: "projectPath and featureId are required" });
+ return;
+ }
+
+ // Start execution in background
+ autoModeService
+ .executeFeature(projectPath, featureId, useWorktrees ?? true, false)
+ .catch((error) => {
+ console.error(`[AutoMode] Feature ${featureId} error:`, error);
+ });
+
+ res.json({ success: true });
+ } catch (error) {
+ const message = error instanceof Error ? error.message : "Unknown error";
+ res.status(500).json({ success: false, error: message });
+ }
+ });
+
+ // Verify a feature
+ router.post("/verify-feature", async (req: Request, res: Response) => {
+ try {
+ const { projectPath, featureId } = req.body as {
+ projectPath: string;
+ featureId: string;
+ };
+
+ if (!projectPath || !featureId) {
+ res
+ .status(400)
+ .json({ success: false, error: "projectPath and featureId are required" });
+ return;
+ }
+
+ const passes = await autoModeService.verifyFeature(projectPath, featureId);
+ res.json({ success: true, passes });
+ } catch (error) {
+ const message = error instanceof Error ? error.message : "Unknown error";
+ res.status(500).json({ success: false, error: message });
+ }
+ });
+
+ // Resume a feature
+ router.post("/resume-feature", async (req: Request, res: Response) => {
+ try {
+ const { projectPath, featureId, useWorktrees } = req.body as {
+ projectPath: string;
+ featureId: string;
+ useWorktrees?: boolean;
+ };
+
+ if (!projectPath || !featureId) {
+ res
+ .status(400)
+ .json({ success: false, error: "projectPath and featureId are required" });
+ return;
+ }
+
+ // Start resume in background
+ autoModeService
+ .resumeFeature(projectPath, featureId, useWorktrees ?? true)
+ .catch((error) => {
+ console.error(`[AutoMode] Resume feature ${featureId} error:`, error);
+ });
+
+ res.json({ success: true });
+ } catch (error) {
+ const message = error instanceof Error ? error.message : "Unknown error";
+ res.status(500).json({ success: false, error: message });
+ }
+ });
+
+ // Check if context exists for a feature
+ router.post("/context-exists", async (req: Request, res: Response) => {
+ try {
+ const { projectPath, featureId } = req.body as {
+ projectPath: string;
+ featureId: string;
+ };
+
+ if (!projectPath || !featureId) {
+ res
+ .status(400)
+ .json({ success: false, error: "projectPath and featureId are required" });
+ return;
+ }
+
+ const exists = await autoModeService.contextExists(projectPath, featureId);
+ res.json({ success: true, exists });
+ } catch (error) {
+ const message = error instanceof Error ? error.message : "Unknown error";
+ res.status(500).json({ success: false, error: message });
+ }
+ });
+
+ // Analyze project
+ router.post("/analyze-project", async (req: Request, res: Response) => {
+ try {
+ const { projectPath } = req.body as { projectPath: string };
+
+ if (!projectPath) {
+ res.status(400).json({ success: false, error: "projectPath is required" });
+ return;
+ }
+
+ // Start analysis in background
+ autoModeService.analyzeProject(projectPath).catch((error) => {
+ console.error(`[AutoMode] Project analysis error:`, error);
+ });
+
+ res.json({ success: true, message: "Project analysis started" });
+ } catch (error) {
+ const message = error instanceof Error ? error.message : "Unknown error";
+ res.status(500).json({ success: false, error: message });
+ }
+ });
+
+ // Follow up on a feature
+ router.post("/follow-up-feature", async (req: Request, res: Response) => {
+ try {
+ const { projectPath, featureId, prompt, imagePaths } = req.body as {
+ projectPath: string;
+ featureId: string;
+ prompt: string;
+ imagePaths?: string[];
+ };
+
+ if (!projectPath || !featureId || !prompt) {
+ res.status(400).json({
+ success: false,
+ error: "projectPath, featureId, and prompt are required",
+ });
+ return;
+ }
+
+ // Start follow-up in background
+ autoModeService
+ .followUpFeature(projectPath, featureId, prompt, imagePaths)
+ .catch((error) => {
+ console.error(`[AutoMode] Follow up feature ${featureId} error:`, error);
+ });
+
+ res.json({ success: true });
+ } catch (error) {
+ const message = error instanceof Error ? error.message : "Unknown error";
+ res.status(500).json({ success: false, error: message });
+ }
+ });
+
+ // Commit feature changes
+ router.post("/commit-feature", async (req: Request, res: Response) => {
+ try {
+ const { projectPath, featureId } = req.body as {
+ projectPath: string;
+ featureId: string;
+ };
+
+ if (!projectPath || !featureId) {
+ res
+ .status(400)
+ .json({ success: false, error: "projectPath and featureId are required" });
+ return;
+ }
+
+ const commitHash = await autoModeService.commitFeature(projectPath, featureId);
+ res.json({ success: true, commitHash });
+ } catch (error) {
+ const message = error instanceof Error ? error.message : "Unknown error";
+ res.status(500).json({ success: false, error: message });
+ }
+ });
+
+ return router;
+}
diff --git a/apps/server/src/routes/features.ts b/apps/server/src/routes/features.ts
new file mode 100644
index 00000000..2878ef08
--- /dev/null
+++ b/apps/server/src/routes/features.ts
@@ -0,0 +1,159 @@
+/**
+ * Features routes - HTTP API for feature management
+ */
+
+import { Router, type Request, type Response } from "express";
+import { FeatureLoader, type Feature } from "../services/feature-loader.js";
+import { addAllowedPath } from "../lib/security.js";
+
+export function createFeaturesRoutes(featureLoader: FeatureLoader): Router {
+ const router = Router();
+
+ // List all features for a project
+ router.post("/list", async (req: Request, res: Response) => {
+ try {
+ const { projectPath } = req.body as { projectPath: string };
+
+ if (!projectPath) {
+ res.status(400).json({ success: false, error: "projectPath is required" });
+ return;
+ }
+
+ // Add project path to allowed paths
+ addAllowedPath(projectPath);
+
+ const features = await featureLoader.getAll(projectPath);
+ res.json({ success: true, features });
+ } catch (error) {
+ const message = error instanceof Error ? error.message : "Unknown error";
+ res.status(500).json({ success: false, error: message });
+ }
+ });
+
+ // Get a single feature
+ router.post("/get", async (req: Request, res: Response) => {
+ try {
+ const { projectPath, featureId } = req.body as {
+ projectPath: string;
+ featureId: string;
+ };
+
+ if (!projectPath || !featureId) {
+ res
+ .status(400)
+ .json({ success: false, error: "projectPath and featureId are required" });
+ return;
+ }
+
+ const feature = await featureLoader.get(projectPath, featureId);
+ if (!feature) {
+ res.status(404).json({ success: false, error: "Feature not found" });
+ return;
+ }
+
+ res.json({ success: true, feature });
+ } catch (error) {
+ const message = error instanceof Error ? error.message : "Unknown error";
+ res.status(500).json({ success: false, error: message });
+ }
+ });
+
+ // Create a new feature
+ router.post("/create", async (req: Request, res: Response) => {
+ try {
+ const { projectPath, feature } = req.body as {
+ projectPath: string;
+ feature: Partial;
+ };
+
+ if (!projectPath || !feature) {
+ res
+ .status(400)
+ .json({ success: false, error: "projectPath and feature are required" });
+ return;
+ }
+
+ // Add project path to allowed paths
+ addAllowedPath(projectPath);
+
+ const created = await featureLoader.create(projectPath, feature);
+ res.json({ success: true, feature: created });
+ } catch (error) {
+ const message = error instanceof Error ? error.message : "Unknown error";
+ res.status(500).json({ success: false, error: message });
+ }
+ });
+
+ // Update a feature
+ router.post("/update", async (req: Request, res: Response) => {
+ try {
+ const { projectPath, featureId, updates } = req.body as {
+ projectPath: string;
+ featureId: string;
+ updates: Partial;
+ };
+
+ if (!projectPath || !featureId || !updates) {
+ res.status(400).json({
+ success: false,
+ error: "projectPath, featureId, and updates are required",
+ });
+ return;
+ }
+
+ const updated = await featureLoader.update(projectPath, featureId, updates);
+ res.json({ success: true, feature: updated });
+ } catch (error) {
+ const message = error instanceof Error ? error.message : "Unknown error";
+ res.status(500).json({ success: false, error: message });
+ }
+ });
+
+ // Delete a feature
+ router.post("/delete", async (req: Request, res: Response) => {
+ try {
+ const { projectPath, featureId } = req.body as {
+ projectPath: string;
+ featureId: string;
+ };
+
+ if (!projectPath || !featureId) {
+ res
+ .status(400)
+ .json({ success: false, error: "projectPath and featureId are required" });
+ return;
+ }
+
+ const success = await featureLoader.delete(projectPath, featureId);
+ res.json({ success });
+ } catch (error) {
+ const message = error instanceof Error ? error.message : "Unknown error";
+ res.status(500).json({ success: false, error: message });
+ }
+ });
+
+ // Get agent output for a feature
+ router.post("/agent-output", async (req: Request, res: Response) => {
+ try {
+ const { projectPath, featureId } = req.body as {
+ projectPath: string;
+ featureId: string;
+ };
+
+ if (!projectPath || !featureId) {
+ res
+ .status(400)
+ .json({ success: false, error: "projectPath and featureId are required" });
+ return;
+ }
+
+ const content = await featureLoader.getAgentOutput(projectPath, featureId);
+ res.json({ success: true, content });
+ } catch (error) {
+ const message = error instanceof Error ? error.message : "Unknown error";
+ res.status(500).json({ success: false, error: message });
+ }
+ });
+
+ return router;
+}
diff --git a/apps/server/src/routes/fs.ts b/apps/server/src/routes/fs.ts
new file mode 100644
index 00000000..581cd335
--- /dev/null
+++ b/apps/server/src/routes/fs.ts
@@ -0,0 +1,221 @@
+/**
+ * File system routes
+ * Provides REST API equivalents for Electron IPC file operations
+ */
+
+import { Router, type Request, type Response } from "express";
+import fs from "fs/promises";
+import path from "path";
+import { validatePath, addAllowedPath, isPathAllowed } from "../lib/security.js";
+import type { EventEmitter } from "../lib/events.js";
+
+export function createFsRoutes(_events: EventEmitter): Router {
+ const router = Router();
+
+ // Read file
+ router.post("/read", async (req: Request, res: Response) => {
+ try {
+ const { filePath } = req.body as { filePath: string };
+
+ if (!filePath) {
+ res.status(400).json({ success: false, error: "filePath is required" });
+ return;
+ }
+
+ const resolvedPath = validatePath(filePath);
+ const content = await fs.readFile(resolvedPath, "utf-8");
+
+ res.json({ success: true, content });
+ } catch (error) {
+ const message = error instanceof Error ? error.message : "Unknown error";
+ res.status(500).json({ success: false, error: message });
+ }
+ });
+
+ // Write file
+ router.post("/write", async (req: Request, res: Response) => {
+ try {
+ const { filePath, content } = req.body as {
+ filePath: string;
+ content: string;
+ };
+
+ if (!filePath) {
+ res.status(400).json({ success: false, error: "filePath is required" });
+ return;
+ }
+
+ const resolvedPath = validatePath(filePath);
+
+ // Ensure parent directory exists
+ await fs.mkdir(path.dirname(resolvedPath), { recursive: true });
+ await fs.writeFile(resolvedPath, content, "utf-8");
+
+ res.json({ success: true });
+ } catch (error) {
+ const message = error instanceof Error ? error.message : "Unknown error";
+ res.status(500).json({ success: false, error: message });
+ }
+ });
+
+ // Create directory
+ router.post("/mkdir", async (req: Request, res: Response) => {
+ try {
+ const { dirPath } = req.body as { dirPath: string };
+
+ if (!dirPath) {
+ res.status(400).json({ success: false, error: "dirPath is required" });
+ return;
+ }
+
+ const resolvedPath = validatePath(dirPath);
+ await fs.mkdir(resolvedPath, { recursive: true });
+
+ res.json({ success: true });
+ } catch (error) {
+ const message = error instanceof Error ? error.message : "Unknown error";
+ res.status(500).json({ success: false, error: message });
+ }
+ });
+
+ // Read directory
+ router.post("/readdir", async (req: Request, res: Response) => {
+ try {
+ const { dirPath } = req.body as { dirPath: string };
+
+ if (!dirPath) {
+ res.status(400).json({ success: false, error: "dirPath is required" });
+ return;
+ }
+
+ const resolvedPath = validatePath(dirPath);
+ const entries = await fs.readdir(resolvedPath, { withFileTypes: true });
+
+ const result = entries.map((entry) => ({
+ name: entry.name,
+ isDirectory: entry.isDirectory(),
+ isFile: entry.isFile(),
+ }));
+
+ res.json({ success: true, entries: result });
+ } catch (error) {
+ const message = error instanceof Error ? error.message : "Unknown error";
+ res.status(500).json({ success: false, error: message });
+ }
+ });
+
+ // Check if file/directory exists
+ router.post("/exists", async (req: Request, res: Response) => {
+ try {
+ const { filePath } = req.body as { filePath: string };
+
+ if (!filePath) {
+ res.status(400).json({ success: false, error: "filePath is required" });
+ return;
+ }
+
+ // For exists, we check but don't require the path to be pre-allowed
+ // This allows the UI to validate user-entered paths
+ const resolvedPath = path.resolve(filePath);
+
+ try {
+ await fs.access(resolvedPath);
+ res.json({ success: true, exists: true });
+ } catch {
+ res.json({ success: true, exists: false });
+ }
+ } catch (error) {
+ const message = error instanceof Error ? error.message : "Unknown error";
+ res.status(500).json({ success: false, error: message });
+ }
+ });
+
+ // Get file stats
+ router.post("/stat", async (req: Request, res: Response) => {
+ try {
+ const { filePath } = req.body as { filePath: string };
+
+ if (!filePath) {
+ res.status(400).json({ success: false, error: "filePath is required" });
+ return;
+ }
+
+ const resolvedPath = validatePath(filePath);
+ const stats = await fs.stat(resolvedPath);
+
+ res.json({
+ success: true,
+ stats: {
+ isDirectory: stats.isDirectory(),
+ isFile: stats.isFile(),
+ size: stats.size,
+ mtime: stats.mtime,
+ },
+ });
+ } catch (error) {
+ const message = error instanceof Error ? error.message : "Unknown error";
+ res.status(500).json({ success: false, error: message });
+ }
+ });
+
+ // Delete file
+ router.post("/delete", async (req: Request, res: Response) => {
+ try {
+ const { filePath } = req.body as { filePath: string };
+
+ if (!filePath) {
+ res.status(400).json({ success: false, error: "filePath is required" });
+ return;
+ }
+
+ const resolvedPath = validatePath(filePath);
+ await fs.rm(resolvedPath, { recursive: true });
+
+ res.json({ success: true });
+ } catch (error) {
+ const message = error instanceof Error ? error.message : "Unknown error";
+ res.status(500).json({ success: false, error: message });
+ }
+ });
+
+ // Validate and add path to allowed list
+ // This is the web equivalent of dialog:openDirectory
+ router.post("/validate-path", async (req: Request, res: Response) => {
+ try {
+ const { filePath } = req.body as { filePath: string };
+
+ if (!filePath) {
+ res.status(400).json({ success: false, error: "filePath is required" });
+ return;
+ }
+
+ const resolvedPath = path.resolve(filePath);
+
+ // Check if path exists
+ try {
+ const stats = await fs.stat(resolvedPath);
+
+ if (!stats.isDirectory()) {
+ res.status(400).json({ success: false, error: "Path is not a directory" });
+ return;
+ }
+
+ // Add to allowed paths
+ addAllowedPath(resolvedPath);
+
+ res.json({
+ success: true,
+ path: resolvedPath,
+ isAllowed: isPathAllowed(resolvedPath),
+ });
+ } catch {
+ res.status(400).json({ success: false, error: "Path does not exist" });
+ }
+ } catch (error) {
+ const message = error instanceof Error ? error.message : "Unknown error";
+ res.status(500).json({ success: false, error: message });
+ }
+ });
+
+ return router;
+}
diff --git a/apps/server/src/routes/git.ts b/apps/server/src/routes/git.ts
new file mode 100644
index 00000000..e6a65ba4
--- /dev/null
+++ b/apps/server/src/routes/git.ts
@@ -0,0 +1,102 @@
+/**
+ * Git routes - HTTP API for git operations (non-worktree)
+ */
+
+import { Router, type Request, type Response } from "express";
+import { exec } from "child_process";
+import { promisify } from "util";
+
+const execAsync = promisify(exec);
+
+export function createGitRoutes(): Router {
+ const router = Router();
+
+ // Get diffs for the main project
+ router.post("/diffs", async (req: Request, res: Response) => {
+ try {
+ const { projectPath } = req.body as { projectPath: string };
+
+ if (!projectPath) {
+ res.status(400).json({ success: false, error: "projectPath required" });
+ return;
+ }
+
+ try {
+ const { stdout: diff } = await execAsync("git diff HEAD", {
+ cwd: projectPath,
+ maxBuffer: 10 * 1024 * 1024,
+ });
+ const { stdout: status } = await execAsync("git status --porcelain", {
+ cwd: projectPath,
+ });
+
+ const files = status
+ .split("\n")
+ .filter(Boolean)
+ .map((line) => {
+ const statusChar = line[0];
+ const filePath = line.slice(3);
+ const statusMap: Record = {
+ M: "Modified",
+ A: "Added",
+ D: "Deleted",
+ R: "Renamed",
+ C: "Copied",
+ U: "Updated",
+ "?": "Untracked",
+ };
+ return {
+ status: statusChar,
+ path: filePath,
+ statusText: statusMap[statusChar] || "Unknown",
+ };
+ });
+
+ res.json({
+ success: true,
+ diff,
+ files,
+ hasChanges: files.length > 0,
+ });
+ } catch {
+ res.json({ success: true, diff: "", files: [], hasChanges: false });
+ }
+ } catch (error) {
+ const message = error instanceof Error ? error.message : "Unknown error";
+ res.status(500).json({ success: false, error: message });
+ }
+ });
+
+ // Get diff for a specific file
+ router.post("/file-diff", async (req: Request, res: Response) => {
+ try {
+ const { projectPath, filePath } = req.body as {
+ projectPath: string;
+ filePath: string;
+ };
+
+ if (!projectPath || !filePath) {
+ res
+ .status(400)
+ .json({ success: false, error: "projectPath and filePath required" });
+ return;
+ }
+
+ try {
+ const { stdout: diff } = await execAsync(`git diff HEAD -- "${filePath}"`, {
+ cwd: projectPath,
+ maxBuffer: 10 * 1024 * 1024,
+ });
+
+ res.json({ success: true, diff, filePath });
+ } catch {
+ res.json({ success: true, diff: "", filePath });
+ }
+ } catch (error) {
+ const message = error instanceof Error ? error.message : "Unknown error";
+ res.status(500).json({ success: false, error: message });
+ }
+ });
+
+ return router;
+}
diff --git a/apps/server/src/routes/health.ts b/apps/server/src/routes/health.ts
new file mode 100644
index 00000000..78111004
--- /dev/null
+++ b/apps/server/src/routes/health.ts
@@ -0,0 +1,39 @@
+/**
+ * Health check routes
+ */
+
+import { Router } from "express";
+import { getAuthStatus } from "../lib/auth.js";
+
+export function createHealthRoutes(): Router {
+ const router = Router();
+
+ // Basic health check
+ router.get("/", (_req, res) => {
+ res.json({
+ status: "ok",
+ timestamp: new Date().toISOString(),
+ version: process.env.npm_package_version || "0.1.0",
+ });
+ });
+
+ // Detailed health check
+ router.get("/detailed", (_req, res) => {
+ res.json({
+ status: "ok",
+ timestamp: new Date().toISOString(),
+ version: process.env.npm_package_version || "0.1.0",
+ uptime: process.uptime(),
+ memory: process.memoryUsage(),
+ dataDir: process.env.DATA_DIR || "./data",
+ auth: getAuthStatus(),
+ env: {
+ nodeVersion: process.version,
+ platform: process.platform,
+ arch: process.arch,
+ },
+ });
+ });
+
+ return router;
+}
diff --git a/apps/server/src/routes/models.ts b/apps/server/src/routes/models.ts
new file mode 100644
index 00000000..529c50a6
--- /dev/null
+++ b/apps/server/src/routes/models.ts
@@ -0,0 +1,128 @@
+/**
+ * Models routes - HTTP API for model providers and availability
+ */
+
+import { Router, type Request, type Response } from "express";
+
+interface ModelDefinition {
+ id: string;
+ name: string;
+ provider: string;
+ contextWindow: number;
+ maxOutputTokens: number;
+ supportsVision: boolean;
+ supportsTools: boolean;
+}
+
+interface ProviderStatus {
+ available: boolean;
+ hasApiKey: boolean;
+ error?: string;
+}
+
+export function createModelsRoutes(): Router {
+ const router = Router();
+
+ // Get available models
+ router.get("/available", async (_req: Request, res: Response) => {
+ try {
+ const models: ModelDefinition[] = [
+ {
+ id: "claude-opus-4-5-20251101",
+ name: "Claude Opus 4.5",
+ provider: "anthropic",
+ contextWindow: 200000,
+ maxOutputTokens: 16384,
+ supportsVision: true,
+ supportsTools: true,
+ },
+ {
+ id: "claude-sonnet-4-20250514",
+ name: "Claude Sonnet 4",
+ provider: "anthropic",
+ contextWindow: 200000,
+ maxOutputTokens: 16384,
+ supportsVision: true,
+ supportsTools: true,
+ },
+ {
+ id: "claude-3-5-sonnet-20241022",
+ name: "Claude 3.5 Sonnet",
+ provider: "anthropic",
+ contextWindow: 200000,
+ maxOutputTokens: 8192,
+ supportsVision: true,
+ supportsTools: true,
+ },
+ {
+ id: "claude-3-5-haiku-20241022",
+ name: "Claude 3.5 Haiku",
+ provider: "anthropic",
+ contextWindow: 200000,
+ maxOutputTokens: 8192,
+ supportsVision: true,
+ supportsTools: true,
+ },
+ {
+ id: "gpt-4o",
+ name: "GPT-4o",
+ provider: "openai",
+ contextWindow: 128000,
+ maxOutputTokens: 16384,
+ supportsVision: true,
+ supportsTools: true,
+ },
+ {
+ id: "gpt-4o-mini",
+ name: "GPT-4o Mini",
+ provider: "openai",
+ contextWindow: 128000,
+ maxOutputTokens: 16384,
+ supportsVision: true,
+ supportsTools: true,
+ },
+ {
+ id: "o1",
+ name: "o1",
+ provider: "openai",
+ contextWindow: 200000,
+ maxOutputTokens: 100000,
+ supportsVision: true,
+ supportsTools: false,
+ },
+ ];
+
+ res.json({ success: true, models });
+ } catch (error) {
+ const message = error instanceof Error ? error.message : "Unknown error";
+ res.status(500).json({ success: false, error: message });
+ }
+ });
+
+ // Check provider status
+ router.get("/providers", async (_req: Request, res: Response) => {
+ try {
+ const providers: Record = {
+ anthropic: {
+ available: !!process.env.ANTHROPIC_API_KEY,
+ hasApiKey: !!process.env.ANTHROPIC_API_KEY,
+ },
+ openai: {
+ available: !!process.env.OPENAI_API_KEY,
+ hasApiKey: !!process.env.OPENAI_API_KEY,
+ },
+ google: {
+ available: !!process.env.GOOGLE_API_KEY,
+ hasApiKey: !!process.env.GOOGLE_API_KEY,
+ },
+ };
+
+ res.json({ success: true, providers });
+ } catch (error) {
+ const message = error instanceof Error ? error.message : "Unknown error";
+ res.status(500).json({ success: false, error: message });
+ }
+ });
+
+ return router;
+}
diff --git a/apps/server/src/routes/running-agents.ts b/apps/server/src/routes/running-agents.ts
new file mode 100644
index 00000000..57285636
--- /dev/null
+++ b/apps/server/src/routes/running-agents.ts
@@ -0,0 +1,70 @@
+/**
+ * Running Agents routes - HTTP API for tracking active agent executions
+ */
+
+import { Router, type Request, type Response } from "express";
+import path from "path";
+
+interface RunningAgent {
+ featureId: string;
+ projectPath: string;
+ projectName: string;
+ isAutoMode: boolean;
+}
+
+// In-memory tracking of running agents (shared with auto-mode service via reference)
+const runningAgentsMap = new Map();
+let autoLoopRunning = false;
+
+export function createRunningAgentsRoutes(): Router {
+ const router = Router();
+
+ // Get all running agents
+ router.get("/", async (_req: Request, res: Response) => {
+ try {
+ const runningAgents = Array.from(runningAgentsMap.values());
+
+ res.json({
+ success: true,
+ runningAgents,
+ totalCount: runningAgents.length,
+ autoLoopRunning,
+ });
+ } catch (error) {
+ const message = error instanceof Error ? error.message : "Unknown error";
+ res.status(500).json({ success: false, error: message });
+ }
+ });
+
+ return router;
+}
+
+// Export functions to update running agents from other services
+export function registerRunningAgent(
+ featureId: string,
+ projectPath: string,
+ isAutoMode: boolean
+): void {
+ runningAgentsMap.set(featureId, {
+ featureId,
+ projectPath,
+ projectName: path.basename(projectPath),
+ isAutoMode,
+ });
+}
+
+export function unregisterRunningAgent(featureId: string): void {
+ runningAgentsMap.delete(featureId);
+}
+
+export function setAutoLoopRunning(running: boolean): void {
+ autoLoopRunning = running;
+}
+
+export function getRunningAgentsCount(): number {
+ return runningAgentsMap.size;
+}
+
+export function isAgentRunning(featureId: string): boolean {
+ return runningAgentsMap.has(featureId);
+}
diff --git a/apps/server/src/routes/sessions.ts b/apps/server/src/routes/sessions.ts
new file mode 100644
index 00000000..15fce21b
--- /dev/null
+++ b/apps/server/src/routes/sessions.ts
@@ -0,0 +1,126 @@
+/**
+ * Sessions routes - HTTP API for session management
+ */
+
+import { Router, type Request, type Response } from "express";
+import { AgentService } from "../services/agent-service.js";
+
+export function createSessionsRoutes(agentService: AgentService): Router {
+ const router = Router();
+
+ // List all sessions
+ router.get("/", async (req: Request, res: Response) => {
+ try {
+ const includeArchived = req.query.includeArchived === "true";
+ const sessions = await agentService.listSessions(includeArchived);
+ res.json({ success: true, sessions });
+ } catch (error) {
+ const message = error instanceof Error ? error.message : "Unknown error";
+ res.status(500).json({ success: false, error: message });
+ }
+ });
+
+ // Create a new session
+ router.post("/", async (req: Request, res: Response) => {
+ try {
+ const { name, projectPath, workingDirectory } = req.body as {
+ name: string;
+ projectPath?: string;
+ workingDirectory?: string;
+ };
+
+ if (!name) {
+ res.status(400).json({ success: false, error: "name is required" });
+ return;
+ }
+
+ const session = await agentService.createSession(
+ name,
+ projectPath,
+ workingDirectory
+ );
+ res.json({ success: true, session });
+ } catch (error) {
+ const message = error instanceof Error ? error.message : "Unknown error";
+ res.status(500).json({ success: false, error: message });
+ }
+ });
+
+ // Update a session
+ router.put("/:sessionId", async (req: Request, res: Response) => {
+ try {
+ const { sessionId } = req.params;
+ const { name, tags } = req.body as {
+ name?: string;
+ tags?: string[];
+ };
+
+ const session = await agentService.updateSession(sessionId, { name, tags });
+ if (!session) {
+ res.status(404).json({ success: false, error: "Session not found" });
+ return;
+ }
+
+ res.json({ success: true, session });
+ } catch (error) {
+ const message = error instanceof Error ? error.message : "Unknown error";
+ res.status(500).json({ success: false, error: message });
+ }
+ });
+
+ // Archive a session
+ router.post("/:sessionId/archive", async (req: Request, res: Response) => {
+ try {
+ const { sessionId } = req.params;
+ const success = await agentService.archiveSession(sessionId);
+
+ if (!success) {
+ res.status(404).json({ success: false, error: "Session not found" });
+ return;
+ }
+
+ res.json({ success: true });
+ } catch (error) {
+ const message = error instanceof Error ? error.message : "Unknown error";
+ res.status(500).json({ success: false, error: message });
+ }
+ });
+
+ // Unarchive a session
+ router.post("/:sessionId/unarchive", async (req: Request, res: Response) => {
+ try {
+ const { sessionId } = req.params;
+ const success = await agentService.unarchiveSession(sessionId);
+
+ if (!success) {
+ res.status(404).json({ success: false, error: "Session not found" });
+ return;
+ }
+
+ res.json({ success: true });
+ } catch (error) {
+ const message = error instanceof Error ? error.message : "Unknown error";
+ res.status(500).json({ success: false, error: message });
+ }
+ });
+
+ // Delete a session
+ router.delete("/:sessionId", async (req: Request, res: Response) => {
+ try {
+ const { sessionId } = req.params;
+ const success = await agentService.deleteSession(sessionId);
+
+ if (!success) {
+ res.status(404).json({ success: false, error: "Session not found" });
+ return;
+ }
+
+ res.json({ success: true });
+ } catch (error) {
+ const message = error instanceof Error ? error.message : "Unknown error";
+ res.status(500).json({ success: false, error: message });
+ }
+ });
+
+ return router;
+}
diff --git a/apps/server/src/routes/setup.ts b/apps/server/src/routes/setup.ts
new file mode 100644
index 00000000..1017db8b
--- /dev/null
+++ b/apps/server/src/routes/setup.ts
@@ -0,0 +1,407 @@
+/**
+ * Setup routes - HTTP API for CLI detection, API keys, and platform info
+ */
+
+import { Router, type Request, type Response } from "express";
+import { exec } from "child_process";
+import { promisify } from "util";
+import os from "os";
+import path from "path";
+import fs from "fs/promises";
+
+const execAsync = promisify(exec);
+
+// Storage for API keys (in-memory for now, should be persisted)
+const apiKeys: Record = {};
+
+export function createSetupRoutes(): Router {
+ const router = Router();
+
+ // Get Claude CLI status
+ router.get("/claude-status", async (_req: Request, res: Response) => {
+ try {
+ let installed = false;
+ let version = "";
+ let cliPath = "";
+ let method = "none";
+
+ // Try to find Claude CLI
+ try {
+ const { stdout } = await execAsync("which claude || where claude 2>/dev/null");
+ cliPath = stdout.trim();
+ installed = true;
+ method = "path";
+
+ // Get version
+ try {
+ const { stdout: versionOut } = await execAsync("claude --version");
+ version = versionOut.trim();
+ } catch {
+ // Version command might not be available
+ }
+ } catch {
+ // Not in PATH, try common locations
+ const commonPaths = [
+ path.join(os.homedir(), ".local", "bin", "claude"),
+ "/usr/local/bin/claude",
+ path.join(os.homedir(), ".npm-global", "bin", "claude"),
+ ];
+
+ for (const p of commonPaths) {
+ try {
+ await fs.access(p);
+ cliPath = p;
+ installed = true;
+ method = "local";
+ break;
+ } catch {
+ // Not found at this path
+ }
+ }
+ }
+
+ // Check authentication - detect all possible auth methods
+ let auth = {
+ authenticated: false,
+ method: "none" as string,
+ hasCredentialsFile: false,
+ hasToken: false,
+ hasStoredOAuthToken: false,
+ hasStoredApiKey: !!apiKeys.anthropic,
+ hasEnvApiKey: !!process.env.ANTHROPIC_API_KEY,
+ hasEnvOAuthToken: !!process.env.CLAUDE_CODE_OAUTH_TOKEN,
+ // Additional fields for detailed status
+ oauthTokenValid: false,
+ apiKeyValid: false,
+ };
+
+ // Check for credentials file (OAuth tokens from claude login)
+ const credentialsPath = path.join(os.homedir(), ".claude", "credentials.json");
+ try {
+ const credentialsContent = await fs.readFile(credentialsPath, "utf-8");
+ const credentials = JSON.parse(credentialsContent);
+ auth.hasCredentialsFile = true;
+
+ // Check what type of token is in credentials
+ if (credentials.oauth_token || credentials.access_token) {
+ auth.hasStoredOAuthToken = true;
+ auth.oauthTokenValid = true;
+ auth.authenticated = true;
+ auth.method = "oauth_token"; // Stored OAuth token from credentials file
+ } else if (credentials.api_key) {
+ auth.apiKeyValid = true;
+ auth.authenticated = true;
+ auth.method = "api_key"; // Stored API key in credentials file
+ }
+ } catch {
+ // No credentials file or invalid format
+ }
+
+ // Environment variables override stored credentials (higher priority)
+ if (auth.hasEnvOAuthToken) {
+ auth.authenticated = true;
+ auth.oauthTokenValid = true;
+ auth.method = "oauth_token_env"; // OAuth token from CLAUDE_CODE_OAUTH_TOKEN env var
+ } else if (auth.hasEnvApiKey) {
+ auth.authenticated = true;
+ auth.apiKeyValid = true;
+ auth.method = "api_key_env"; // API key from ANTHROPIC_API_KEY env var
+ }
+
+ // In-memory stored API key (from settings UI)
+ if (!auth.authenticated && apiKeys.anthropic) {
+ auth.authenticated = true;
+ auth.method = "api_key"; // Manually stored API key
+ }
+
+ res.json({
+ success: true,
+ status: installed ? "installed" : "not_installed",
+ installed,
+ method,
+ version,
+ path: cliPath,
+ auth,
+ });
+ } catch (error) {
+ const message = error instanceof Error ? error.message : "Unknown error";
+ res.status(500).json({ success: false, error: message });
+ }
+ });
+
+ // Get Codex CLI status
+ router.get("/codex-status", async (_req: Request, res: Response) => {
+ try {
+ let installed = false;
+ let version = "";
+ let cliPath = "";
+ let method = "none";
+
+ // Try to find Codex CLI
+ try {
+ const { stdout } = await execAsync("which codex || where codex 2>/dev/null");
+ cliPath = stdout.trim();
+ installed = true;
+ method = "path";
+
+ try {
+ const { stdout: versionOut } = await execAsync("codex --version");
+ version = versionOut.trim();
+ } catch {
+ // Version command might not be available
+ }
+ } catch {
+ // Not found
+ }
+
+ // Check for OpenAI/Codex authentication
+ let auth = {
+ authenticated: false,
+ method: "none" as string,
+ hasAuthFile: false,
+ hasEnvKey: !!process.env.OPENAI_API_KEY,
+ hasStoredApiKey: !!apiKeys.openai,
+ hasEnvApiKey: !!process.env.OPENAI_API_KEY,
+ // Additional fields for subscription/account detection
+ hasSubscription: false,
+ cliLoggedIn: false,
+ };
+
+ // Check for OpenAI CLI auth file (~/.codex/auth.json or similar)
+ const codexAuthPaths = [
+ path.join(os.homedir(), ".codex", "auth.json"),
+ path.join(os.homedir(), ".openai", "credentials"),
+ path.join(os.homedir(), ".config", "openai", "credentials.json"),
+ ];
+
+ for (const authPath of codexAuthPaths) {
+ try {
+ const authContent = await fs.readFile(authPath, "utf-8");
+ const authData = JSON.parse(authContent);
+ auth.hasAuthFile = true;
+
+ // Check for subscription/tokens
+ if (authData.subscription || authData.plan || authData.account_type) {
+ auth.hasSubscription = true;
+ auth.authenticated = true;
+ auth.method = "subscription"; // Codex subscription (Plus/Team)
+ } else if (authData.access_token || authData.api_key) {
+ auth.cliLoggedIn = true;
+ auth.authenticated = true;
+ auth.method = "cli_verified"; // CLI logged in with account
+ }
+ break;
+ } catch {
+ // Auth file not found at this path
+ }
+ }
+
+ // Environment variable has highest priority
+ if (auth.hasEnvApiKey) {
+ auth.authenticated = true;
+ auth.method = "env"; // OPENAI_API_KEY environment variable
+ }
+
+ // In-memory stored API key (from settings UI)
+ if (!auth.authenticated && apiKeys.openai) {
+ auth.authenticated = true;
+ auth.method = "api_key"; // Manually stored API key
+ }
+
+ res.json({
+ success: true,
+ status: installed ? "installed" : "not_installed",
+ method,
+ version,
+ path: cliPath,
+ auth,
+ });
+ } catch (error) {
+ const message = error instanceof Error ? error.message : "Unknown error";
+ res.status(500).json({ success: false, error: message });
+ }
+ });
+
+ // Install Claude CLI
+ router.post("/install-claude", async (_req: Request, res: Response) => {
+ try {
+ // In web mode, we can't install CLIs directly
+ // Return instructions instead
+ res.json({
+ success: false,
+ error:
+ "CLI installation requires terminal access. Please install manually using: npm install -g @anthropic-ai/claude-code",
+ });
+ } catch (error) {
+ const message = error instanceof Error ? error.message : "Unknown error";
+ res.status(500).json({ success: false, error: message });
+ }
+ });
+
+ // Install Codex CLI
+ router.post("/install-codex", async (_req: Request, res: Response) => {
+ try {
+ res.json({
+ success: false,
+ error:
+ "CLI installation requires terminal access. Please install manually using: npm install -g @openai/codex",
+ });
+ } catch (error) {
+ const message = error instanceof Error ? error.message : "Unknown error";
+ res.status(500).json({ success: false, error: message });
+ }
+ });
+
+ // Auth Claude
+ router.post("/auth-claude", async (_req: Request, res: Response) => {
+ try {
+ res.json({
+ success: true,
+ requiresManualAuth: true,
+ command: "claude login",
+ message: "Please run 'claude login' in your terminal to authenticate",
+ });
+ } catch (error) {
+ const message = error instanceof Error ? error.message : "Unknown error";
+ res.status(500).json({ success: false, error: message });
+ }
+ });
+
+ // Auth Codex
+ router.post("/auth-codex", async (req: Request, res: Response) => {
+ try {
+ const { apiKey } = req.body as { apiKey?: string };
+
+ if (apiKey) {
+ apiKeys.openai = apiKey;
+ process.env.OPENAI_API_KEY = apiKey;
+ res.json({ success: true });
+ } else {
+ res.json({
+ success: true,
+ requiresManualAuth: true,
+ command: "codex auth login",
+ });
+ }
+ } catch (error) {
+ const message = error instanceof Error ? error.message : "Unknown error";
+ res.status(500).json({ success: false, error: message });
+ }
+ });
+
+ // Store API key
+ router.post("/store-api-key", async (req: Request, res: Response) => {
+ try {
+ const { provider, apiKey } = req.body as { provider: string; apiKey: string };
+
+ if (!provider || !apiKey) {
+ res.status(400).json({ success: false, error: "provider and apiKey required" });
+ return;
+ }
+
+ apiKeys[provider] = apiKey;
+
+ // Also set as environment variable
+ if (provider === "anthropic") {
+ process.env.ANTHROPIC_API_KEY = apiKey;
+ } else if (provider === "openai") {
+ process.env.OPENAI_API_KEY = apiKey;
+ } else if (provider === "google") {
+ process.env.GOOGLE_API_KEY = apiKey;
+ }
+
+ res.json({ success: true });
+ } catch (error) {
+ const message = error instanceof Error ? error.message : "Unknown error";
+ res.status(500).json({ success: false, error: message });
+ }
+ });
+
+ // Get API keys status
+ router.get("/api-keys", async (_req: Request, res: Response) => {
+ try {
+ res.json({
+ success: true,
+ hasAnthropicKey: !!apiKeys.anthropic || !!process.env.ANTHROPIC_API_KEY,
+ hasOpenAIKey: !!apiKeys.openai || !!process.env.OPENAI_API_KEY,
+ hasGoogleKey: !!apiKeys.google || !!process.env.GOOGLE_API_KEY,
+ });
+ } catch (error) {
+ const message = error instanceof Error ? error.message : "Unknown error";
+ res.status(500).json({ success: false, error: message });
+ }
+ });
+
+ // Configure Codex MCP
+ router.post("/configure-codex-mcp", async (req: Request, res: Response) => {
+ try {
+ const { projectPath } = req.body as { projectPath: string };
+
+ if (!projectPath) {
+ res.status(400).json({ success: false, error: "projectPath required" });
+ return;
+ }
+
+ // Create .codex directory and config
+ const codexDir = path.join(projectPath, ".codex");
+ await fs.mkdir(codexDir, { recursive: true });
+
+ const configPath = path.join(codexDir, "config.toml");
+ const config = `# Codex configuration
+[mcp]
+enabled = true
+`;
+ await fs.writeFile(configPath, config);
+
+ res.json({ success: true, configPath });
+ } catch (error) {
+ const message = error instanceof Error ? error.message : "Unknown error";
+ res.status(500).json({ success: false, error: message });
+ }
+ });
+
+ // Get platform info
+ router.get("/platform", async (_req: Request, res: Response) => {
+ try {
+ const platform = os.platform();
+ res.json({
+ success: true,
+ platform,
+ arch: os.arch(),
+ homeDir: os.homedir(),
+ isWindows: platform === "win32",
+ isMac: platform === "darwin",
+ isLinux: platform === "linux",
+ });
+ } catch (error) {
+ const message = error instanceof Error ? error.message : "Unknown error";
+ res.status(500).json({ success: false, error: message });
+ }
+ });
+
+ // Test OpenAI connection
+ router.post("/test-openai", async (req: Request, res: Response) => {
+ try {
+ const { apiKey } = req.body as { apiKey?: string };
+ const key = apiKey || apiKeys.openai || process.env.OPENAI_API_KEY;
+
+ if (!key) {
+ res.json({ success: false, error: "No OpenAI API key provided" });
+ return;
+ }
+
+ // Simple test - just verify the key format
+ if (!key.startsWith("sk-")) {
+ res.json({ success: false, error: "Invalid OpenAI API key format" });
+ return;
+ }
+
+ res.json({ success: true, message: "API key format is valid" });
+ } catch (error) {
+ const message = error instanceof Error ? error.message : "Unknown error";
+ res.status(500).json({ success: false, error: message });
+ }
+ });
+
+ return router;
+}
diff --git a/apps/server/src/routes/spec-regeneration.ts b/apps/server/src/routes/spec-regeneration.ts
new file mode 100644
index 00000000..f2409d34
--- /dev/null
+++ b/apps/server/src/routes/spec-regeneration.ts
@@ -0,0 +1,412 @@
+/**
+ * Spec Regeneration routes - HTTP API for AI-powered spec generation
+ */
+
+import { Router, type Request, type Response } from "express";
+import { query, type Options } from "@anthropic-ai/claude-agent-sdk";
+import path from "path";
+import fs from "fs/promises";
+import type { EventEmitter } from "../lib/events.js";
+
+let isRunning = false;
+let currentAbortController: AbortController | null = null;
+
+export function createSpecRegenerationRoutes(events: EventEmitter): Router {
+ const router = Router();
+
+ // Create project spec from overview
+ router.post("/create", async (req: Request, res: Response) => {
+ try {
+ const { projectPath, projectOverview, generateFeatures } = req.body as {
+ projectPath: string;
+ projectOverview: string;
+ generateFeatures?: boolean;
+ };
+
+ if (!projectPath || !projectOverview) {
+ res.status(400).json({
+ success: false,
+ error: "projectPath and projectOverview required",
+ });
+ return;
+ }
+
+ if (isRunning) {
+ res.json({ success: false, error: "Spec generation already running" });
+ return;
+ }
+
+ isRunning = true;
+ currentAbortController = new AbortController();
+
+ // Start generation in background
+ generateSpec(
+ projectPath,
+ projectOverview,
+ events,
+ currentAbortController,
+ generateFeatures
+ )
+ .catch((error) => {
+ console.error("[SpecRegeneration] Error:", error);
+ events.emit("spec-regeneration:event", {
+ type: "spec_error",
+ error: error.message,
+ });
+ })
+ .finally(() => {
+ isRunning = false;
+ currentAbortController = null;
+ });
+
+ res.json({ success: true });
+ } catch (error) {
+ const message = error instanceof Error ? error.message : "Unknown error";
+ res.status(500).json({ success: false, error: message });
+ }
+ });
+
+ // Generate from project definition
+ router.post("/generate", async (req: Request, res: Response) => {
+ try {
+ const { projectPath, projectDefinition } = req.body as {
+ projectPath: string;
+ projectDefinition: string;
+ };
+
+ if (!projectPath || !projectDefinition) {
+ res.status(400).json({
+ success: false,
+ error: "projectPath and projectDefinition required",
+ });
+ return;
+ }
+
+ if (isRunning) {
+ res.json({ success: false, error: "Spec generation already running" });
+ return;
+ }
+
+ isRunning = true;
+ currentAbortController = new AbortController();
+
+ generateSpec(
+ projectPath,
+ projectDefinition,
+ events,
+ currentAbortController,
+ false
+ )
+ .catch((error) => {
+ console.error("[SpecRegeneration] Error:", error);
+ events.emit("spec-regeneration:event", {
+ type: "spec_error",
+ error: error.message,
+ });
+ })
+ .finally(() => {
+ isRunning = false;
+ currentAbortController = null;
+ });
+
+ res.json({ success: true });
+ } catch (error) {
+ const message = error instanceof Error ? error.message : "Unknown error";
+ res.status(500).json({ success: false, error: message });
+ }
+ });
+
+ // Generate features from existing spec
+ router.post("/generate-features", async (req: Request, res: Response) => {
+ try {
+ const { projectPath } = req.body as { projectPath: string };
+
+ if (!projectPath) {
+ res.status(400).json({ success: false, error: "projectPath required" });
+ return;
+ }
+
+ if (isRunning) {
+ res.json({ success: false, error: "Generation already running" });
+ return;
+ }
+
+ isRunning = true;
+ currentAbortController = new AbortController();
+
+ generateFeaturesFromSpec(projectPath, events, currentAbortController)
+ .catch((error) => {
+ console.error("[SpecRegeneration] Error:", error);
+ events.emit("spec-regeneration:event", {
+ type: "features_error",
+ error: error.message,
+ });
+ })
+ .finally(() => {
+ isRunning = false;
+ currentAbortController = null;
+ });
+
+ res.json({ success: true });
+ } catch (error) {
+ const message = error instanceof Error ? error.message : "Unknown error";
+ res.status(500).json({ success: false, error: message });
+ }
+ });
+
+ // Stop generation
+ router.post("/stop", async (_req: Request, res: Response) => {
+ try {
+ if (currentAbortController) {
+ currentAbortController.abort();
+ }
+ isRunning = false;
+ res.json({ success: true });
+ } catch (error) {
+ const message = error instanceof Error ? error.message : "Unknown error";
+ res.status(500).json({ success: false, error: message });
+ }
+ });
+
+ // Get status
+ router.get("/status", async (_req: Request, res: Response) => {
+ try {
+ res.json({ success: true, isRunning });
+ } catch (error) {
+ const message = error instanceof Error ? error.message : "Unknown error";
+ res.status(500).json({ success: false, error: message });
+ }
+ });
+
+ return router;
+}
+
+async function generateSpec(
+ projectPath: string,
+ projectOverview: string,
+ events: EventEmitter,
+ abortController: AbortController,
+ generateFeatures?: boolean
+) {
+ const prompt = `You are helping to define a software project specification.
+
+Project Overview:
+${projectOverview}
+
+Based on this overview, analyze the project and create a comprehensive specification that includes:
+
+1. **Project Summary** - Brief description of what the project does
+2. **Core Features** - Main functionality the project needs
+3. **Technical Stack** - Recommended technologies and frameworks
+4. **Architecture** - High-level system design
+5. **Data Models** - Key entities and their relationships
+6. **API Design** - Main endpoints/interfaces needed
+7. **User Experience** - Key user flows and interactions
+
+${generateFeatures ? `
+Also generate a list of features to implement. For each feature provide:
+- ID (lowercase-hyphenated)
+- Title
+- Description
+- Priority (1=high, 2=medium, 3=low)
+- Estimated complexity (simple, moderate, complex)
+` : ""}
+
+Format your response as markdown. Be specific and actionable.`;
+
+ events.emit("spec-regeneration:event", {
+ type: "spec_progress",
+ content: "Starting spec generation...\n",
+ });
+
+ const options: Options = {
+ model: "claude-opus-4-5-20251101",
+ maxTurns: 10,
+ cwd: projectPath,
+ allowedTools: ["Read", "Glob", "Grep"],
+ permissionMode: "acceptEdits",
+ abortController,
+ };
+
+ const stream = query({ prompt, options });
+ let responseText = "";
+
+ for await (const msg of stream) {
+ if (msg.type === "assistant" && msg.message.content) {
+ for (const block of msg.message.content) {
+ if (block.type === "text") {
+ responseText = block.text;
+ events.emit("spec-regeneration:event", {
+ type: "spec_progress",
+ content: block.text,
+ });
+ } else if (block.type === "tool_use") {
+ events.emit("spec-regeneration:event", {
+ type: "spec_tool",
+ tool: block.name,
+ input: block.input,
+ });
+ }
+ }
+ } else if (msg.type === "result" && msg.subtype === "success") {
+ responseText = msg.result || responseText;
+ }
+ }
+
+ // Save spec
+ const specDir = path.join(projectPath, ".automaker");
+ const specPath = path.join(specDir, "project-spec.md");
+
+ await fs.mkdir(specDir, { recursive: true });
+ await fs.writeFile(specPath, responseText);
+
+ events.emit("spec-regeneration:event", {
+ type: "spec_complete",
+ specPath,
+ content: responseText,
+ });
+
+ // If generate features was requested, parse and create them
+ if (generateFeatures) {
+ await parseAndCreateFeatures(projectPath, responseText, events);
+ }
+}
+
+async function generateFeaturesFromSpec(
+ projectPath: string,
+ events: EventEmitter,
+ abortController: AbortController
+) {
+ // Read existing spec
+ const specPath = path.join(projectPath, ".automaker", "project-spec.md");
+ let spec: string;
+
+ try {
+ spec = await fs.readFile(specPath, "utf-8");
+ } catch {
+ events.emit("spec-regeneration:event", {
+ type: "features_error",
+ error: "No project spec found. Generate spec first.",
+ });
+ return;
+ }
+
+ const prompt = `Based on this project specification:
+
+${spec}
+
+Generate a prioritized list of implementable features. For each feature provide:
+
+1. **id**: A unique lowercase-hyphenated identifier
+2. **title**: Short descriptive title
+3. **description**: What this feature does (2-3 sentences)
+4. **priority**: 1 (high), 2 (medium), or 3 (low)
+5. **complexity**: "simple", "moderate", or "complex"
+6. **dependencies**: Array of feature IDs this depends on (can be empty)
+
+Format as JSON:
+{
+ "features": [
+ {
+ "id": "feature-id",
+ "title": "Feature Title",
+ "description": "What it does",
+ "priority": 1,
+ "complexity": "moderate",
+ "dependencies": []
+ }
+ ]
+}
+
+Generate 5-15 features that build on each other logically.`;
+
+ events.emit("spec-regeneration:event", {
+ type: "features_progress",
+ content: "Analyzing spec and generating features...\n",
+ });
+
+ const options: Options = {
+ model: "claude-sonnet-4-20250514",
+ maxTurns: 5,
+ cwd: projectPath,
+ allowedTools: ["Read", "Glob"],
+ permissionMode: "acceptEdits",
+ abortController,
+ };
+
+ const stream = query({ prompt, options });
+ let responseText = "";
+
+ for await (const msg of stream) {
+ if (msg.type === "assistant" && msg.message.content) {
+ for (const block of msg.message.content) {
+ if (block.type === "text") {
+ responseText = block.text;
+ events.emit("spec-regeneration:event", {
+ type: "features_progress",
+ content: block.text,
+ });
+ }
+ }
+ } else if (msg.type === "result" && msg.subtype === "success") {
+ responseText = msg.result || responseText;
+ }
+ }
+
+ await parseAndCreateFeatures(projectPath, responseText, events);
+}
+
+async function parseAndCreateFeatures(
+ projectPath: string,
+ content: string,
+ events: EventEmitter
+) {
+ try {
+ // Extract JSON from response
+ const jsonMatch = content.match(/\{[\s\S]*"features"[\s\S]*\}/);
+ if (!jsonMatch) {
+ throw new Error("No valid JSON found in response");
+ }
+
+ const parsed = JSON.parse(jsonMatch[0]);
+ const featuresDir = path.join(projectPath, ".automaker", "features");
+ await fs.mkdir(featuresDir, { recursive: true });
+
+ const createdFeatures: Array<{ id: string; title: string }> = [];
+
+ for (const feature of parsed.features) {
+ const featureDir = path.join(featuresDir, feature.id);
+ await fs.mkdir(featureDir, { recursive: true });
+
+ const featureData = {
+ id: feature.id,
+ title: feature.title,
+ description: feature.description,
+ status: "pending",
+ priority: feature.priority || 2,
+ complexity: feature.complexity || "moderate",
+ dependencies: feature.dependencies || [],
+ createdAt: new Date().toISOString(),
+ updatedAt: new Date().toISOString(),
+ };
+
+ await fs.writeFile(
+ path.join(featureDir, "feature.json"),
+ JSON.stringify(featureData, null, 2)
+ );
+
+ createdFeatures.push({ id: feature.id, title: feature.title });
+ }
+
+ events.emit("spec-regeneration:event", {
+ type: "features_complete",
+ features: createdFeatures,
+ count: createdFeatures.length,
+ });
+ } catch (error) {
+ events.emit("spec-regeneration:event", {
+ type: "features_error",
+ error: (error as Error).message,
+ });
+ }
+}
diff --git a/apps/server/src/routes/suggestions.ts b/apps/server/src/routes/suggestions.ts
new file mode 100644
index 00000000..578d1328
--- /dev/null
+++ b/apps/server/src/routes/suggestions.ts
@@ -0,0 +1,192 @@
+/**
+ * Suggestions routes - HTTP API for AI-powered feature suggestions
+ */
+
+import { Router, type Request, type Response } from "express";
+import { query, type Options } from "@anthropic-ai/claude-agent-sdk";
+import type { EventEmitter } from "../lib/events.js";
+
+let isRunning = false;
+let currentAbortController: AbortController | null = null;
+
+export function createSuggestionsRoutes(events: EventEmitter): Router {
+ const router = Router();
+
+ // Generate suggestions
+ router.post("/generate", async (req: Request, res: Response) => {
+ try {
+ const { projectPath, suggestionType = "features" } = req.body as {
+ projectPath: string;
+ suggestionType?: string;
+ };
+
+ if (!projectPath) {
+ res.status(400).json({ success: false, error: "projectPath required" });
+ return;
+ }
+
+ if (isRunning) {
+ res.json({ success: false, error: "Suggestions generation is already running" });
+ return;
+ }
+
+ isRunning = true;
+ currentAbortController = new AbortController();
+
+ // Start generation in background
+ generateSuggestions(projectPath, suggestionType, events, currentAbortController)
+ .catch((error) => {
+ console.error("[Suggestions] Error:", error);
+ events.emit("suggestions:event", {
+ type: "suggestions_error",
+ error: error.message,
+ });
+ })
+ .finally(() => {
+ isRunning = false;
+ currentAbortController = null;
+ });
+
+ res.json({ success: true });
+ } catch (error) {
+ const message = error instanceof Error ? error.message : "Unknown error";
+ res.status(500).json({ success: false, error: message });
+ }
+ });
+
+ // Stop suggestions generation
+ router.post("/stop", async (_req: Request, res: Response) => {
+ try {
+ if (currentAbortController) {
+ currentAbortController.abort();
+ }
+ isRunning = false;
+ res.json({ success: true });
+ } catch (error) {
+ const message = error instanceof Error ? error.message : "Unknown error";
+ res.status(500).json({ success: false, error: message });
+ }
+ });
+
+ // Get status
+ router.get("/status", async (_req: Request, res: Response) => {
+ try {
+ res.json({ success: true, isRunning });
+ } catch (error) {
+ const message = error instanceof Error ? error.message : "Unknown error";
+ res.status(500).json({ success: false, error: message });
+ }
+ });
+
+ return router;
+}
+
+async function generateSuggestions(
+ projectPath: string,
+ suggestionType: string,
+ events: EventEmitter,
+ abortController: AbortController
+) {
+ const typePrompts: Record = {
+ features: "Analyze this project and suggest new features that would add value.",
+ refactoring: "Analyze this project and identify refactoring opportunities.",
+ security: "Analyze this project for security vulnerabilities and suggest fixes.",
+ performance: "Analyze this project for performance issues and suggest optimizations.",
+ };
+
+ const prompt = `${typePrompts[suggestionType] || typePrompts.features}
+
+Look at the codebase and provide 3-5 concrete suggestions.
+
+For each suggestion, provide:
+1. A category (e.g., "User Experience", "Security", "Performance")
+2. A clear description of what to implement
+3. Concrete steps to implement it
+4. Priority (1=high, 2=medium, 3=low)
+5. Brief reasoning for why this would help
+
+Format your response as JSON:
+{
+ "suggestions": [
+ {
+ "id": "suggestion-123",
+ "category": "Category",
+ "description": "What to implement",
+ "steps": ["Step 1", "Step 2"],
+ "priority": 1,
+ "reasoning": "Why this helps"
+ }
+ ]
+}`;
+
+ events.emit("suggestions:event", {
+ type: "suggestions_progress",
+ content: `Starting ${suggestionType} analysis...\n`,
+ });
+
+ const options: Options = {
+ model: "claude-opus-4-5-20251101",
+ maxTurns: 5,
+ cwd: projectPath,
+ allowedTools: ["Read", "Glob", "Grep"],
+ permissionMode: "acceptEdits",
+ abortController,
+ };
+
+ const stream = query({ prompt, options });
+ let responseText = "";
+
+ for await (const msg of stream) {
+ if (msg.type === "assistant" && msg.message.content) {
+ for (const block of msg.message.content) {
+ if (block.type === "text") {
+ responseText = block.text;
+ events.emit("suggestions:event", {
+ type: "suggestions_progress",
+ content: block.text,
+ });
+ } else if (block.type === "tool_use") {
+ events.emit("suggestions:event", {
+ type: "suggestions_tool",
+ tool: block.name,
+ input: block.input,
+ });
+ }
+ }
+ } else if (msg.type === "result" && msg.subtype === "success") {
+ responseText = msg.result || responseText;
+ }
+ }
+
+ // Parse suggestions from response
+ try {
+ const jsonMatch = responseText.match(/\{[\s\S]*"suggestions"[\s\S]*\}/);
+ if (jsonMatch) {
+ const parsed = JSON.parse(jsonMatch[0]);
+ events.emit("suggestions:event", {
+ type: "suggestions_complete",
+ suggestions: parsed.suggestions.map((s: Record, i: number) => ({
+ ...s,
+ id: s.id || `suggestion-${Date.now()}-${i}`,
+ })),
+ });
+ } else {
+ throw new Error("No valid JSON found in response");
+ }
+ } catch (error) {
+ // Return generic suggestions if parsing fails
+ events.emit("suggestions:event", {
+ type: "suggestions_complete",
+ suggestions: [
+ {
+ id: `suggestion-${Date.now()}-0`,
+ category: "Analysis",
+ description: "Review the AI analysis output for insights",
+ steps: ["Review the generated analysis"],
+ priority: 1,
+ reasoning: "The AI provided analysis but suggestions need manual review",
+ },
+ ],
+ });
+ }
+}
diff --git a/apps/server/src/routes/worktree.ts b/apps/server/src/routes/worktree.ts
new file mode 100644
index 00000000..9d57e3d3
--- /dev/null
+++ b/apps/server/src/routes/worktree.ts
@@ -0,0 +1,355 @@
+/**
+ * Worktree routes - HTTP API for git worktree operations
+ */
+
+import { Router, type Request, type Response } from "express";
+import { exec } from "child_process";
+import { promisify } from "util";
+import path from "path";
+import fs from "fs/promises";
+
+const execAsync = promisify(exec);
+
+export function createWorktreeRoutes(): Router {
+ const router = Router();
+
+ // Check if a path is a git repo
+ async function isGitRepo(repoPath: string): Promise {
+ try {
+ await execAsync("git rev-parse --is-inside-work-tree", { cwd: repoPath });
+ return true;
+ } catch {
+ return false;
+ }
+ }
+
+ // Get worktree info
+ router.post("/info", async (req: Request, res: Response) => {
+ try {
+ const { projectPath, featureId } = req.body as {
+ projectPath: string;
+ featureId: string;
+ };
+
+ if (!projectPath || !featureId) {
+ res
+ .status(400)
+ .json({ success: false, error: "projectPath and featureId required" });
+ return;
+ }
+
+ // Check if worktree exists
+ const worktreePath = path.join(projectPath, ".automaker", "worktrees", featureId);
+ try {
+ await fs.access(worktreePath);
+ const { stdout } = await execAsync("git rev-parse --abbrev-ref HEAD", {
+ cwd: worktreePath,
+ });
+ res.json({
+ success: true,
+ worktreePath,
+ branchName: stdout.trim(),
+ });
+ } catch {
+ res.json({ success: true, worktreePath: null, branchName: null });
+ }
+ } catch (error) {
+ const message = error instanceof Error ? error.message : "Unknown error";
+ res.status(500).json({ success: false, error: message });
+ }
+ });
+
+ // Get worktree status
+ router.post("/status", async (req: Request, res: Response) => {
+ try {
+ const { projectPath, featureId } = req.body as {
+ projectPath: string;
+ featureId: string;
+ };
+
+ if (!projectPath || !featureId) {
+ res
+ .status(400)
+ .json({ success: false, error: "projectPath and featureId required" });
+ return;
+ }
+
+ const worktreePath = path.join(projectPath, ".automaker", "worktrees", featureId);
+
+ try {
+ await fs.access(worktreePath);
+ const { stdout: status } = await execAsync("git status --porcelain", {
+ cwd: worktreePath,
+ });
+ const files = status
+ .split("\n")
+ .filter(Boolean)
+ .map((line) => line.slice(3));
+ const { stdout: diffStat } = await execAsync("git diff --stat", {
+ cwd: worktreePath,
+ });
+ const { stdout: logOutput } = await execAsync(
+ 'git log --oneline -5 --format="%h %s"',
+ { cwd: worktreePath }
+ );
+
+ res.json({
+ success: true,
+ modifiedFiles: files.length,
+ files,
+ diffStat: diffStat.trim(),
+ recentCommits: logOutput.trim().split("\n").filter(Boolean),
+ });
+ } catch {
+ res.json({
+ success: true,
+ modifiedFiles: 0,
+ files: [],
+ diffStat: "",
+ recentCommits: [],
+ });
+ }
+ } catch (error) {
+ const message = error instanceof Error ? error.message : "Unknown error";
+ res.status(500).json({ success: false, error: message });
+ }
+ });
+
+ // List all worktrees
+ router.post("/list", async (req: Request, res: Response) => {
+ try {
+ const { projectPath } = req.body as { projectPath: string };
+
+ if (!projectPath) {
+ res.status(400).json({ success: false, error: "projectPath required" });
+ return;
+ }
+
+ if (!(await isGitRepo(projectPath))) {
+ res.json({ success: true, worktrees: [] });
+ return;
+ }
+
+ const { stdout } = await execAsync("git worktree list --porcelain", {
+ cwd: projectPath,
+ });
+
+ const worktrees: Array<{ path: string; branch: string }> = [];
+ const lines = stdout.split("\n");
+ let current: { path?: string; branch?: string } = {};
+
+ for (const line of lines) {
+ if (line.startsWith("worktree ")) {
+ current.path = line.slice(9);
+ } else if (line.startsWith("branch ")) {
+ current.branch = line.slice(7).replace("refs/heads/", "");
+ } else if (line === "") {
+ if (current.path && current.branch) {
+ worktrees.push({ path: current.path, branch: current.branch });
+ }
+ current = {};
+ }
+ }
+
+ res.json({ success: true, worktrees });
+ } catch (error) {
+ const message = error instanceof Error ? error.message : "Unknown error";
+ res.status(500).json({ success: false, error: message });
+ }
+ });
+
+ // Get diffs for a worktree
+ router.post("/diffs", async (req: Request, res: Response) => {
+ try {
+ const { projectPath, featureId } = req.body as {
+ projectPath: string;
+ featureId: string;
+ };
+
+ if (!projectPath || !featureId) {
+ res
+ .status(400)
+ .json({ success: false, error: "projectPath and featureId required" });
+ return;
+ }
+
+ const worktreePath = path.join(projectPath, ".automaker", "worktrees", featureId);
+
+ try {
+ await fs.access(worktreePath);
+ const { stdout: diff } = await execAsync("git diff HEAD", {
+ cwd: worktreePath,
+ maxBuffer: 10 * 1024 * 1024,
+ });
+ const { stdout: status } = await execAsync("git status --porcelain", {
+ cwd: worktreePath,
+ });
+
+ const files = status
+ .split("\n")
+ .filter(Boolean)
+ .map((line) => {
+ const statusChar = line[0];
+ const filePath = line.slice(3);
+ const statusMap: Record = {
+ M: "Modified",
+ A: "Added",
+ D: "Deleted",
+ R: "Renamed",
+ C: "Copied",
+ U: "Updated",
+ "?": "Untracked",
+ };
+ return {
+ status: statusChar,
+ path: filePath,
+ statusText: statusMap[statusChar] || "Unknown",
+ };
+ });
+
+ res.json({
+ success: true,
+ diff,
+ files,
+ hasChanges: files.length > 0,
+ });
+ } catch {
+ res.json({ success: true, diff: "", files: [], hasChanges: false });
+ }
+ } catch (error) {
+ const message = error instanceof Error ? error.message : "Unknown error";
+ res.status(500).json({ success: false, error: message });
+ }
+ });
+
+ // Get diff for a specific file
+ router.post("/file-diff", async (req: Request, res: Response) => {
+ try {
+ const { projectPath, featureId, filePath } = req.body as {
+ projectPath: string;
+ featureId: string;
+ filePath: string;
+ };
+
+ if (!projectPath || !featureId || !filePath) {
+ res.status(400).json({
+ success: false,
+ error: "projectPath, featureId, and filePath required",
+ });
+ return;
+ }
+
+ const worktreePath = path.join(projectPath, ".automaker", "worktrees", featureId);
+
+ try {
+ await fs.access(worktreePath);
+ const { stdout: diff } = await execAsync(`git diff HEAD -- "${filePath}"`, {
+ cwd: worktreePath,
+ maxBuffer: 10 * 1024 * 1024,
+ });
+
+ res.json({ success: true, diff, filePath });
+ } catch {
+ res.json({ success: true, diff: "", filePath });
+ }
+ } catch (error) {
+ const message = error instanceof Error ? error.message : "Unknown error";
+ res.status(500).json({ success: false, error: message });
+ }
+ });
+
+ // Revert feature (remove worktree)
+ router.post("/revert", async (req: Request, res: Response) => {
+ try {
+ const { projectPath, featureId } = req.body as {
+ projectPath: string;
+ featureId: string;
+ };
+
+ if (!projectPath || !featureId) {
+ res
+ .status(400)
+ .json({ success: false, error: "projectPath and featureId required" });
+ return;
+ }
+
+ const worktreePath = path.join(projectPath, ".automaker", "worktrees", featureId);
+
+ try {
+ // Remove worktree
+ await execAsync(`git worktree remove "${worktreePath}" --force`, {
+ cwd: projectPath,
+ });
+ // Delete branch
+ await execAsync(`git branch -D feature/${featureId}`, { cwd: projectPath });
+
+ res.json({ success: true, removedPath: worktreePath });
+ } catch (error) {
+ // Worktree might not exist
+ res.json({ success: true, removedPath: null });
+ }
+ } catch (error) {
+ const message = error instanceof Error ? error.message : "Unknown error";
+ res.status(500).json({ success: false, error: message });
+ }
+ });
+
+ // Merge feature (merge worktree branch into main)
+ router.post("/merge", async (req: Request, res: Response) => {
+ try {
+ const { projectPath, featureId, options } = req.body as {
+ projectPath: string;
+ featureId: string;
+ options?: { squash?: boolean; message?: string };
+ };
+
+ if (!projectPath || !featureId) {
+ res
+ .status(400)
+ .json({ success: false, error: "projectPath and featureId required" });
+ return;
+ }
+
+ const branchName = `feature/${featureId}`;
+ const worktreePath = path.join(projectPath, ".automaker", "worktrees", featureId);
+
+ // Get current branch
+ const { stdout: currentBranch } = await execAsync(
+ "git rev-parse --abbrev-ref HEAD",
+ { cwd: projectPath }
+ );
+
+ // Merge the feature branch
+ const mergeCmd = options?.squash
+ ? `git merge --squash ${branchName}`
+ : `git merge ${branchName} -m "${options?.message || `Merge ${branchName}`}"`;
+
+ await execAsync(mergeCmd, { cwd: projectPath });
+
+ // If squash merge, need to commit
+ if (options?.squash) {
+ await execAsync(
+ `git commit -m "${options?.message || `Merge ${branchName} (squash)`}"`,
+ { cwd: projectPath }
+ );
+ }
+
+ // Clean up worktree and branch
+ try {
+ await execAsync(`git worktree remove "${worktreePath}" --force`, {
+ cwd: projectPath,
+ });
+ await execAsync(`git branch -D ${branchName}`, { cwd: projectPath });
+ } catch {
+ // Cleanup errors are non-fatal
+ }
+
+ res.json({ success: true, mergedBranch: branchName });
+ } catch (error) {
+ const message = error instanceof Error ? error.message : "Unknown error";
+ res.status(500).json({ success: false, error: message });
+ }
+ });
+
+ return router;
+}
diff --git a/apps/server/src/services/agent-service.ts b/apps/server/src/services/agent-service.ts
new file mode 100644
index 00000000..09119be6
--- /dev/null
+++ b/apps/server/src/services/agent-service.ts
@@ -0,0 +1,562 @@
+/**
+ * Agent Service - Runs Claude agents via the Claude Agent SDK
+ * Manages conversation sessions and streams responses via WebSocket
+ */
+
+import { query, AbortError, type Options } from "@anthropic-ai/claude-agent-sdk";
+import path from "path";
+import fs from "fs/promises";
+import type { EventEmitter } from "../lib/events.js";
+
+interface Message {
+ id: string;
+ role: "user" | "assistant";
+ content: string;
+ images?: Array<{
+ data: string;
+ mimeType: string;
+ filename: string;
+ }>;
+ timestamp: string;
+ isError?: boolean;
+}
+
+interface Session {
+ messages: Message[];
+ isRunning: boolean;
+ abortController: AbortController | null;
+ workingDirectory: string;
+}
+
+interface SessionMetadata {
+ id: string;
+ name: string;
+ projectPath?: string;
+ workingDirectory: string;
+ createdAt: string;
+ updatedAt: string;
+ archived?: boolean;
+ tags?: string[];
+}
+
+export class AgentService {
+ private sessions = new Map();
+ private stateDir: string;
+ private metadataFile: string;
+ private events: EventEmitter;
+
+ constructor(dataDir: string, events: EventEmitter) {
+ this.stateDir = path.join(dataDir, "agent-sessions");
+ this.metadataFile = path.join(dataDir, "sessions-metadata.json");
+ this.events = events;
+ }
+
+ async initialize(): Promise {
+ await fs.mkdir(this.stateDir, { recursive: true });
+ }
+
+ /**
+ * Start or resume a conversation
+ */
+ async startConversation({
+ sessionId,
+ workingDirectory,
+ }: {
+ sessionId: string;
+ workingDirectory?: string;
+ }) {
+ if (!this.sessions.has(sessionId)) {
+ const messages = await this.loadSession(sessionId);
+ this.sessions.set(sessionId, {
+ messages,
+ isRunning: false,
+ abortController: null,
+ workingDirectory: workingDirectory || process.cwd(),
+ });
+ }
+
+ const session = this.sessions.get(sessionId)!;
+ return {
+ success: true,
+ messages: session.messages,
+ sessionId,
+ };
+ }
+
+ /**
+ * Send a message to the agent and stream responses
+ */
+ async sendMessage({
+ sessionId,
+ message,
+ workingDirectory,
+ imagePaths,
+ }: {
+ sessionId: string;
+ message: string;
+ workingDirectory?: string;
+ imagePaths?: string[];
+ }) {
+ const session = this.sessions.get(sessionId);
+ if (!session) {
+ throw new Error(`Session ${sessionId} not found`);
+ }
+
+ if (session.isRunning) {
+ throw new Error("Agent is already processing a message");
+ }
+
+ // Read images and convert to base64
+ const images: Message["images"] = [];
+ if (imagePaths && imagePaths.length > 0) {
+ for (const imagePath of imagePaths) {
+ try {
+ const imageBuffer = await fs.readFile(imagePath);
+ const base64Data = imageBuffer.toString("base64");
+ const ext = path.extname(imagePath).toLowerCase();
+ const mimeTypeMap: Record = {
+ ".jpg": "image/jpeg",
+ ".jpeg": "image/jpeg",
+ ".png": "image/png",
+ ".gif": "image/gif",
+ ".webp": "image/webp",
+ };
+ const mediaType = mimeTypeMap[ext] || "image/png";
+
+ images.push({
+ data: base64Data,
+ mimeType: mediaType,
+ filename: path.basename(imagePath),
+ });
+ } catch (error) {
+ console.error(`[AgentService] Failed to load image ${imagePath}:`, error);
+ }
+ }
+ }
+
+ // Add user message
+ const userMessage: Message = {
+ id: this.generateId(),
+ role: "user",
+ content: message,
+ images: images.length > 0 ? images : undefined,
+ timestamp: new Date().toISOString(),
+ };
+
+ session.messages.push(userMessage);
+ session.isRunning = true;
+ session.abortController = new AbortController();
+
+ // Emit user message event
+ this.emitAgentEvent(sessionId, {
+ type: "message",
+ message: userMessage,
+ });
+
+ await this.saveSession(sessionId, session.messages);
+
+ try {
+ const options: Options = {
+ model: "claude-opus-4-5-20251101",
+ systemPrompt: this.getSystemPrompt(),
+ maxTurns: 20,
+ cwd: workingDirectory || session.workingDirectory,
+ allowedTools: [
+ "Read",
+ "Write",
+ "Edit",
+ "Glob",
+ "Grep",
+ "Bash",
+ "WebSearch",
+ "WebFetch",
+ ],
+ permissionMode: "acceptEdits",
+ sandbox: {
+ enabled: true,
+ autoAllowBashIfSandboxed: true,
+ },
+ abortController: session.abortController!,
+ };
+
+ // Build prompt content
+ let promptContent: string | Array<{ type: string; text?: string; source?: object }> =
+ message;
+
+ if (imagePaths && imagePaths.length > 0) {
+ const contentBlocks: Array<{ type: string; text?: string; source?: object }> = [];
+
+ if (message && message.trim()) {
+ contentBlocks.push({ type: "text", text: message });
+ }
+
+ for (const imagePath of imagePaths) {
+ try {
+ const imageBuffer = await fs.readFile(imagePath);
+ const base64Data = imageBuffer.toString("base64");
+ const ext = path.extname(imagePath).toLowerCase();
+ const mimeTypeMap: Record = {
+ ".jpg": "image/jpeg",
+ ".jpeg": "image/jpeg",
+ ".png": "image/png",
+ ".gif": "image/gif",
+ ".webp": "image/webp",
+ };
+ const mediaType = mimeTypeMap[ext] || "image/png";
+
+ contentBlocks.push({
+ type: "image",
+ source: {
+ type: "base64",
+ media_type: mediaType,
+ data: base64Data,
+ },
+ });
+ } catch (error) {
+ console.error(`[AgentService] Failed to load image ${imagePath}:`, error);
+ }
+ }
+
+ if (contentBlocks.length > 1 || contentBlocks[0]?.type === "image") {
+ promptContent = contentBlocks;
+ }
+ }
+
+ // Build payload
+ const promptPayload = Array.isArray(promptContent)
+ ? (async function* () {
+ yield {
+ type: "user" as const,
+ session_id: "",
+ message: {
+ role: "user" as const,
+ content: promptContent,
+ },
+ parent_tool_use_id: null,
+ };
+ })()
+ : promptContent;
+
+ const stream = query({ prompt: promptPayload, options });
+
+ let currentAssistantMessage: Message | null = null;
+ let responseText = "";
+ const toolUses: Array<{ name: string; input: unknown }> = [];
+
+ for await (const msg of stream) {
+ if (msg.type === "assistant") {
+ if (msg.message.content) {
+ for (const block of msg.message.content) {
+ if (block.type === "text") {
+ responseText += block.text;
+
+ if (!currentAssistantMessage) {
+ currentAssistantMessage = {
+ id: this.generateId(),
+ role: "assistant",
+ content: responseText,
+ timestamp: new Date().toISOString(),
+ };
+ session.messages.push(currentAssistantMessage);
+ } else {
+ currentAssistantMessage.content = responseText;
+ }
+
+ this.emitAgentEvent(sessionId, {
+ type: "stream",
+ messageId: currentAssistantMessage.id,
+ content: responseText,
+ isComplete: false,
+ });
+ } else if (block.type === "tool_use") {
+ const toolUse = {
+ name: block.name,
+ input: block.input,
+ };
+ toolUses.push(toolUse);
+
+ this.emitAgentEvent(sessionId, {
+ type: "tool_use",
+ tool: toolUse,
+ });
+ }
+ }
+ }
+ } else if (msg.type === "result") {
+ if (msg.subtype === "success" && msg.result) {
+ if (currentAssistantMessage) {
+ currentAssistantMessage.content = msg.result;
+ responseText = msg.result;
+ }
+ }
+
+ this.emitAgentEvent(sessionId, {
+ type: "complete",
+ messageId: currentAssistantMessage?.id,
+ content: responseText,
+ toolUses,
+ });
+ }
+ }
+
+ await this.saveSession(sessionId, session.messages);
+
+ session.isRunning = false;
+ session.abortController = null;
+
+ return {
+ success: true,
+ message: currentAssistantMessage,
+ };
+ } catch (error) {
+ if (error instanceof AbortError || (error as Error)?.name === "AbortError") {
+ session.isRunning = false;
+ session.abortController = null;
+ return { success: false, aborted: true };
+ }
+
+ console.error("[AgentService] Error:", error);
+
+ session.isRunning = false;
+ session.abortController = null;
+
+ const errorMessage: Message = {
+ id: this.generateId(),
+ role: "assistant",
+ content: `Error: ${(error as Error).message}`,
+ timestamp: new Date().toISOString(),
+ isError: true,
+ };
+
+ session.messages.push(errorMessage);
+ await this.saveSession(sessionId, session.messages);
+
+ this.emitAgentEvent(sessionId, {
+ type: "error",
+ error: (error as Error).message,
+ message: errorMessage,
+ });
+
+ throw error;
+ }
+ }
+
+ /**
+ * Get conversation history
+ */
+ getHistory(sessionId: string) {
+ const session = this.sessions.get(sessionId);
+ if (!session) {
+ return { success: false, error: "Session not found" };
+ }
+
+ return {
+ success: true,
+ messages: session.messages,
+ isRunning: session.isRunning,
+ };
+ }
+
+ /**
+ * Stop current agent execution
+ */
+ async stopExecution(sessionId: string) {
+ const session = this.sessions.get(sessionId);
+ if (!session) {
+ return { success: false, error: "Session not found" };
+ }
+
+ if (session.abortController) {
+ session.abortController.abort();
+ session.isRunning = false;
+ session.abortController = null;
+ }
+
+ return { success: true };
+ }
+
+ /**
+ * Clear conversation history
+ */
+ async clearSession(sessionId: string) {
+ const session = this.sessions.get(sessionId);
+ if (session) {
+ session.messages = [];
+ session.isRunning = false;
+ await this.saveSession(sessionId, []);
+ }
+
+ return { success: true };
+ }
+
+ // Session management
+
+ async loadSession(sessionId: string): Promise {
+ const sessionFile = path.join(this.stateDir, `${sessionId}.json`);
+
+ try {
+ const data = await fs.readFile(sessionFile, "utf-8");
+ return JSON.parse(data);
+ } catch {
+ return [];
+ }
+ }
+
+ async saveSession(sessionId: string, messages: Message[]): Promise {
+ const sessionFile = path.join(this.stateDir, `${sessionId}.json`);
+
+ try {
+ await fs.writeFile(sessionFile, JSON.stringify(messages, null, 2), "utf-8");
+ await this.updateSessionTimestamp(sessionId);
+ } catch (error) {
+ console.error("[AgentService] Failed to save session:", error);
+ }
+ }
+
+ async loadMetadata(): Promise> {
+ try {
+ const data = await fs.readFile(this.metadataFile, "utf-8");
+ return JSON.parse(data);
+ } catch {
+ return {};
+ }
+ }
+
+ async saveMetadata(metadata: Record): Promise {
+ await fs.writeFile(this.metadataFile, JSON.stringify(metadata, null, 2), "utf-8");
+ }
+
+ async updateSessionTimestamp(sessionId: string): Promise {
+ const metadata = await this.loadMetadata();
+ if (metadata[sessionId]) {
+ metadata[sessionId].updatedAt = new Date().toISOString();
+ await this.saveMetadata(metadata);
+ }
+ }
+
+ async listSessions(includeArchived = false): Promise {
+ const metadata = await this.loadMetadata();
+ let sessions = Object.values(metadata);
+
+ if (!includeArchived) {
+ sessions = sessions.filter((s) => !s.archived);
+ }
+
+ return sessions.sort(
+ (a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
+ );
+ }
+
+ async createSession(
+ name: string,
+ projectPath?: string,
+ workingDirectory?: string
+ ): Promise {
+ const sessionId = this.generateId();
+ const metadata = await this.loadMetadata();
+
+ const session: SessionMetadata = {
+ id: sessionId,
+ name,
+ projectPath,
+ workingDirectory: workingDirectory || projectPath || process.cwd(),
+ createdAt: new Date().toISOString(),
+ updatedAt: new Date().toISOString(),
+ };
+
+ metadata[sessionId] = session;
+ await this.saveMetadata(metadata);
+
+ return session;
+ }
+
+ async updateSession(
+ sessionId: string,
+ updates: Partial
+ ): Promise {
+ const metadata = await this.loadMetadata();
+ if (!metadata[sessionId]) return null;
+
+ metadata[sessionId] = {
+ ...metadata[sessionId],
+ ...updates,
+ updatedAt: new Date().toISOString(),
+ };
+
+ await this.saveMetadata(metadata);
+ return metadata[sessionId];
+ }
+
+ async archiveSession(sessionId: string): Promise {
+ const result = await this.updateSession(sessionId, { archived: true });
+ return result !== null;
+ }
+
+ async unarchiveSession(sessionId: string): Promise {
+ const result = await this.updateSession(sessionId, { archived: false });
+ return result !== null;
+ }
+
+ async deleteSession(sessionId: string): Promise {
+ const metadata = await this.loadMetadata();
+ if (!metadata[sessionId]) return false;
+
+ delete metadata[sessionId];
+ await this.saveMetadata(metadata);
+
+ // Delete session file
+ try {
+ const sessionFile = path.join(this.stateDir, `${sessionId}.json`);
+ await fs.unlink(sessionFile);
+ } catch {
+ // File may not exist
+ }
+
+ // Clear from memory
+ this.sessions.delete(sessionId);
+
+ return true;
+ }
+
+ private emitAgentEvent(sessionId: string, data: Record): void {
+ this.events.emit("agent:stream", { sessionId, ...data });
+ }
+
+ private getSystemPrompt(): string {
+ return `You are an AI assistant helping users build software. You are part of the Automaker application,
+which is designed to help developers plan, design, and implement software projects autonomously.
+
+**Feature Storage:**
+Features are stored in .automaker/features/{id}/feature.json - each feature has its own folder.
+Use the UpdateFeatureStatus tool to manage features, not direct file edits.
+
+Your role is to:
+- Help users define their project requirements and specifications
+- Ask clarifying questions to better understand their needs
+- Suggest technical approaches and architectures
+- Guide them through the development process
+- Be conversational and helpful
+- Write, edit, and modify code files as requested
+- Execute commands and tests
+- Search and analyze the codebase
+
+When discussing projects, help users think through:
+- Core functionality and features
+- Technical stack choices
+- Data models and architecture
+- User experience considerations
+- Testing strategies
+
+You have full access to the codebase and can:
+- Read files to understand existing code
+- Write new files
+- Edit existing files
+- Run bash commands
+- Search for code patterns
+- Execute tests and builds`;
+ }
+
+ private generateId(): string {
+ return `msg_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
+ }
+}
diff --git a/apps/server/src/services/auto-mode-service.ts b/apps/server/src/services/auto-mode-service.ts
new file mode 100644
index 00000000..bcc22625
--- /dev/null
+++ b/apps/server/src/services/auto-mode-service.ts
@@ -0,0 +1,815 @@
+/**
+ * Auto Mode Service - Autonomous feature implementation using Claude Agent SDK
+ *
+ * Manages:
+ * - Worktree creation for isolated development
+ * - Feature execution with Claude
+ * - Concurrent execution with max concurrency limits
+ * - Progress streaming via events
+ * - Verification and merge workflows
+ */
+
+import { query, AbortError, type Options } from "@anthropic-ai/claude-agent-sdk";
+import { exec } from "child_process";
+import { promisify } from "util";
+import path from "path";
+import fs from "fs/promises";
+import type { EventEmitter, EventType } from "../lib/events.js";
+
+const execAsync = promisify(exec);
+
+interface Feature {
+ id: string;
+ title: string;
+ description: string;
+ status: string;
+ priority?: number;
+ spec?: string;
+}
+
+interface RunningFeature {
+ featureId: string;
+ projectPath: string;
+ worktreePath: string | null;
+ branchName: string | null;
+ abortController: AbortController;
+ isAutoMode: boolean;
+ startTime: number;
+}
+
+interface AutoModeConfig {
+ maxConcurrency: number;
+ useWorktrees: boolean;
+ projectPath: string;
+}
+
+export class AutoModeService {
+ private events: EventEmitter;
+ private runningFeatures = new Map();
+ private autoLoopRunning = false;
+ private autoLoopAbortController: AbortController | null = null;
+ private config: AutoModeConfig | null = null;
+
+ constructor(events: EventEmitter) {
+ this.events = events;
+ }
+
+ /**
+ * Start the auto mode loop - continuously picks and executes pending features
+ */
+ async startAutoLoop(projectPath: string, maxConcurrency = 3): Promise {
+ if (this.autoLoopRunning) {
+ throw new Error("Auto mode is already running");
+ }
+
+ this.autoLoopRunning = true;
+ this.autoLoopAbortController = new AbortController();
+ this.config = {
+ maxConcurrency,
+ useWorktrees: true,
+ projectPath,
+ };
+
+ this.emitAutoModeEvent("auto_mode_complete", {
+ message: `Auto mode started with max ${maxConcurrency} concurrent features`,
+ projectPath,
+ });
+
+ // Run the loop in the background
+ this.runAutoLoop().catch((error) => {
+ console.error("[AutoMode] Loop error:", error);
+ this.emitAutoModeEvent("auto_mode_error", {
+ error: error.message,
+ });
+ });
+ }
+
+ private async runAutoLoop(): Promise {
+ while (this.autoLoopRunning && this.autoLoopAbortController && !this.autoLoopAbortController.signal.aborted) {
+ try {
+ // Check if we have capacity
+ if (this.runningFeatures.size >= (this.config?.maxConcurrency || 3)) {
+ await this.sleep(5000);
+ continue;
+ }
+
+ // Load pending features
+ const pendingFeatures = await this.loadPendingFeatures(this.config!.projectPath);
+
+ if (pendingFeatures.length === 0) {
+ this.emitAutoModeEvent("auto_mode_complete", {
+ message: "No pending features - auto mode idle",
+ });
+ await this.sleep(10000);
+ continue;
+ }
+
+ // Find a feature not currently running
+ const nextFeature = pendingFeatures.find((f) => !this.runningFeatures.has(f.id));
+
+ if (nextFeature) {
+ // Start feature execution in background
+ this.executeFeature(
+ this.config!.projectPath,
+ nextFeature.id,
+ this.config!.useWorktrees,
+ true
+ ).catch((error) => {
+ console.error(`[AutoMode] Feature ${nextFeature.id} error:`, error);
+ });
+ }
+
+ await this.sleep(2000);
+ } catch (error) {
+ console.error("[AutoMode] Loop iteration error:", error);
+ await this.sleep(5000);
+ }
+ }
+
+ this.autoLoopRunning = false;
+ this.emitAutoModeEvent("auto_mode_complete", {
+ message: "Auto mode stopped",
+ });
+ }
+
+ /**
+ * Stop the auto mode loop
+ */
+ async stopAutoLoop(): Promise {
+ this.autoLoopRunning = false;
+ if (this.autoLoopAbortController) {
+ this.autoLoopAbortController.abort();
+ this.autoLoopAbortController = null;
+ }
+
+ return this.runningFeatures.size;
+ }
+
+ /**
+ * Execute a single feature
+ */
+ async executeFeature(
+ projectPath: string,
+ featureId: string,
+ useWorktrees = true,
+ isAutoMode = false
+ ): Promise {
+ if (this.runningFeatures.has(featureId)) {
+ throw new Error(`Feature ${featureId} is already running`);
+ }
+
+ const abortController = new AbortController();
+ const branchName = `feature/${featureId}`;
+ let worktreePath: string | null = null;
+
+ // Setup worktree if enabled
+ if (useWorktrees) {
+ worktreePath = await this.setupWorktree(projectPath, featureId, branchName);
+ }
+
+ const workDir = worktreePath || projectPath;
+
+ this.runningFeatures.set(featureId, {
+ featureId,
+ projectPath,
+ worktreePath,
+ branchName,
+ abortController,
+ isAutoMode,
+ startTime: Date.now(),
+ });
+
+ // Emit feature start event
+ this.emitAutoModeEvent("auto_mode_feature_start", {
+ featureId,
+ projectPath,
+ feature: { id: featureId, title: "Loading...", description: "Feature is starting" },
+ });
+
+ try {
+ // Load feature details
+ const feature = await this.loadFeature(projectPath, featureId);
+ if (!feature) {
+ throw new Error(`Feature ${featureId} not found`);
+ }
+
+ // Update feature status to in-progress
+ await this.updateFeatureStatus(projectPath, featureId, "in-progress");
+
+ // Build the prompt
+ const prompt = this.buildFeaturePrompt(feature);
+
+ // Run the agent
+ await this.runAgent(workDir, featureId, prompt, abortController);
+
+ // Mark as completed
+ await this.updateFeatureStatus(projectPath, featureId, "completed");
+
+ this.emitAutoModeEvent("auto_mode_feature_complete", {
+ featureId,
+ passes: true,
+ message: `Feature completed in ${Math.round((Date.now() - this.runningFeatures.get(featureId)!.startTime) / 1000)}s`,
+ projectPath,
+ });
+ } catch (error) {
+ if (error instanceof AbortError || (error as Error)?.name === "AbortError") {
+ this.emitAutoModeEvent("auto_mode_feature_complete", {
+ featureId,
+ passes: false,
+ message: "Feature stopped by user",
+ projectPath,
+ });
+ } else {
+ console.error(`[AutoMode] Feature ${featureId} failed:`, error);
+ await this.updateFeatureStatus(projectPath, featureId, "failed");
+ this.emitAutoModeEvent("auto_mode_error", {
+ featureId,
+ error: (error as Error).message,
+ projectPath,
+ });
+ }
+ } finally {
+ this.runningFeatures.delete(featureId);
+ }
+ }
+
+ /**
+ * Stop a specific feature
+ */
+ async stopFeature(featureId: string): Promise {
+ const running = this.runningFeatures.get(featureId);
+ if (!running) {
+ return false;
+ }
+
+ running.abortController.abort();
+ return true;
+ }
+
+ /**
+ * Resume a feature (continues from saved context)
+ */
+ async resumeFeature(
+ projectPath: string,
+ featureId: string,
+ useWorktrees = true
+ ): Promise {
+ // Check if context exists
+ const contextPath = path.join(
+ projectPath,
+ ".automaker",
+ "features",
+ featureId,
+ "agent-output.md"
+ );
+
+ let hasContext = false;
+ try {
+ await fs.access(contextPath);
+ hasContext = true;
+ } catch {
+ // No context
+ }
+
+ if (hasContext) {
+ // Load previous context and continue
+ const context = await fs.readFile(contextPath, "utf-8");
+ return this.executeFeatureWithContext(projectPath, featureId, context, useWorktrees);
+ }
+
+ // No context, start fresh
+ return this.executeFeature(projectPath, featureId, useWorktrees, false);
+ }
+
+ /**
+ * Follow up on a feature with additional instructions
+ */
+ async followUpFeature(
+ projectPath: string,
+ featureId: string,
+ prompt: string,
+ imagePaths?: string[]
+ ): Promise {
+ if (this.runningFeatures.has(featureId)) {
+ throw new Error(`Feature ${featureId} is already running`);
+ }
+
+ const abortController = new AbortController();
+
+ // Check if worktree exists
+ const worktreePath = path.join(projectPath, ".automaker", "worktrees", featureId);
+ let workDir = projectPath;
+
+ try {
+ await fs.access(worktreePath);
+ workDir = worktreePath;
+ } catch {
+ // No worktree, use project path
+ }
+
+ this.runningFeatures.set(featureId, {
+ featureId,
+ projectPath,
+ worktreePath: workDir !== projectPath ? worktreePath : null,
+ branchName: `feature/${featureId}`,
+ abortController,
+ isAutoMode: false,
+ startTime: Date.now(),
+ });
+
+ this.emitAutoModeEvent("auto_mode_feature_start", {
+ featureId,
+ projectPath,
+ feature: { id: featureId, title: "Follow-up", description: prompt.substring(0, 100) },
+ });
+
+ try {
+ await this.runAgent(workDir, featureId, prompt, abortController, imagePaths);
+
+ this.emitAutoModeEvent("auto_mode_feature_complete", {
+ featureId,
+ passes: true,
+ message: "Follow-up completed successfully",
+ projectPath,
+ });
+ } catch (error) {
+ if (!(error instanceof AbortError)) {
+ this.emitAutoModeEvent("auto_mode_error", {
+ featureId,
+ error: (error as Error).message,
+ projectPath,
+ });
+ }
+ } finally {
+ this.runningFeatures.delete(featureId);
+ }
+ }
+
+ /**
+ * Verify a feature's implementation
+ */
+ async verifyFeature(projectPath: string, featureId: string): Promise {
+ const worktreePath = path.join(projectPath, ".automaker", "worktrees", featureId);
+ let workDir = projectPath;
+
+ try {
+ await fs.access(worktreePath);
+ workDir = worktreePath;
+ } catch {
+ // No worktree
+ }
+
+ // Run verification - check if tests pass, build works, etc.
+ const verificationChecks = [
+ { cmd: "npm run lint", name: "Lint" },
+ { cmd: "npm run typecheck", name: "Type check" },
+ { cmd: "npm test", name: "Tests" },
+ { cmd: "npm run build", name: "Build" },
+ ];
+
+ let allPassed = true;
+ const results: Array<{ check: string; passed: boolean; output?: string }> = [];
+
+ for (const check of verificationChecks) {
+ try {
+ const { stdout, stderr } = await execAsync(check.cmd, {
+ cwd: workDir,
+ timeout: 120000,
+ });
+ results.push({ check: check.name, passed: true, output: stdout || stderr });
+ } catch (error) {
+ allPassed = false;
+ results.push({
+ check: check.name,
+ passed: false,
+ output: (error as Error).message,
+ });
+ break; // Stop on first failure
+ }
+ }
+
+ this.emitAutoModeEvent("auto_mode_feature_complete", {
+ featureId,
+ passes: allPassed,
+ message: allPassed
+ ? "All verification checks passed"
+ : `Verification failed: ${results.find(r => !r.passed)?.check || "Unknown"}`,
+ });
+
+ return allPassed;
+ }
+
+ /**
+ * Commit feature changes
+ */
+ async commitFeature(projectPath: string, featureId: string): Promise {
+ const worktreePath = path.join(projectPath, ".automaker", "worktrees", featureId);
+ let workDir = projectPath;
+
+ try {
+ await fs.access(worktreePath);
+ workDir = worktreePath;
+ } catch {
+ // No worktree
+ }
+
+ try {
+ // Check for changes
+ const { stdout: status } = await execAsync("git status --porcelain", { cwd: workDir });
+ if (!status.trim()) {
+ return null; // No changes
+ }
+
+ // Load feature for commit message
+ const feature = await this.loadFeature(projectPath, featureId);
+ const commitMessage = feature
+ ? `feat: ${feature.title}\n\nImplemented by Automaker auto-mode`
+ : `feat: Feature ${featureId}`;
+
+ // Stage and commit
+ await execAsync("git add -A", { cwd: workDir });
+ await execAsync(`git commit -m "${commitMessage.replace(/"/g, '\\"')}"`, {
+ cwd: workDir,
+ });
+
+ // Get commit hash
+ const { stdout: hash } = await execAsync("git rev-parse HEAD", { cwd: workDir });
+
+ this.emitAutoModeEvent("auto_mode_feature_complete", {
+ featureId,
+ passes: true,
+ message: `Changes committed: ${hash.trim().substring(0, 8)}`,
+ });
+
+ return hash.trim();
+ } catch (error) {
+ console.error(`[AutoMode] Commit failed for ${featureId}:`, error);
+ return null;
+ }
+ }
+
+ /**
+ * Check if context exists for a feature
+ */
+ async contextExists(projectPath: string, featureId: string): Promise {
+ const contextPath = path.join(
+ projectPath,
+ ".automaker",
+ "features",
+ featureId,
+ "agent-output.md"
+ );
+
+ try {
+ await fs.access(contextPath);
+ return true;
+ } catch {
+ return false;
+ }
+ }
+
+ /**
+ * Analyze project to gather context
+ */
+ async analyzeProject(projectPath: string): Promise {
+ const abortController = new AbortController();
+
+ const analysisFeatureId = `analysis-${Date.now()}`;
+ this.emitAutoModeEvent("auto_mode_feature_start", {
+ featureId: analysisFeatureId,
+ projectPath,
+ feature: { id: analysisFeatureId, title: "Project Analysis", description: "Analyzing project structure" },
+ });
+
+ const prompt = `Analyze this project and provide a summary of:
+1. Project structure and architecture
+2. Main technologies and frameworks used
+3. Key components and their responsibilities
+4. Build and test commands
+5. Any existing conventions or patterns
+
+Format your response as a structured markdown document.`;
+
+ try {
+ const options: Options = {
+ model: "claude-sonnet-4-20250514",
+ maxTurns: 5,
+ cwd: projectPath,
+ allowedTools: ["Read", "Glob", "Grep"],
+ permissionMode: "acceptEdits",
+ abortController,
+ };
+
+ const stream = query({ prompt, options });
+ let analysisResult = "";
+
+ for await (const msg of stream) {
+ if (msg.type === "assistant" && msg.message.content) {
+ for (const block of msg.message.content) {
+ if (block.type === "text") {
+ analysisResult = block.text;
+ this.emitAutoModeEvent("auto_mode_progress", {
+ featureId: analysisFeatureId,
+ content: block.text,
+ projectPath,
+ });
+ }
+ }
+ } else if (msg.type === "result" && msg.subtype === "success") {
+ analysisResult = msg.result || analysisResult;
+ }
+ }
+
+ // Save analysis
+ const analysisPath = path.join(projectPath, ".automaker", "project-analysis.md");
+ await fs.mkdir(path.dirname(analysisPath), { recursive: true });
+ await fs.writeFile(analysisPath, analysisResult);
+
+ this.emitAutoModeEvent("auto_mode_feature_complete", {
+ featureId: analysisFeatureId,
+ passes: true,
+ message: "Project analysis completed",
+ projectPath,
+ });
+ } catch (error) {
+ this.emitAutoModeEvent("auto_mode_error", {
+ featureId: analysisFeatureId,
+ error: (error as Error).message,
+ projectPath,
+ });
+ }
+ }
+
+ /**
+ * Get current status
+ */
+ getStatus(): {
+ isRunning: boolean;
+ autoLoopRunning: boolean;
+ runningFeatures: string[];
+ runningCount: number;
+ } {
+ return {
+ isRunning: this.autoLoopRunning || this.runningFeatures.size > 0,
+ autoLoopRunning: this.autoLoopRunning,
+ runningFeatures: Array.from(this.runningFeatures.keys()),
+ runningCount: this.runningFeatures.size,
+ };
+ }
+
+ // Private helpers
+
+ private async setupWorktree(
+ projectPath: string,
+ featureId: string,
+ branchName: string
+ ): Promise {
+ const worktreesDir = path.join(projectPath, ".automaker", "worktrees");
+ const worktreePath = path.join(worktreesDir, featureId);
+
+ await fs.mkdir(worktreesDir, { recursive: true });
+
+ // Check if worktree already exists
+ try {
+ await fs.access(worktreePath);
+ return worktreePath;
+ } catch {
+ // Create new worktree
+ }
+
+ // Create branch if it doesn't exist
+ try {
+ await execAsync(`git branch ${branchName}`, { cwd: projectPath });
+ } catch {
+ // Branch may already exist
+ }
+
+ // Create worktree
+ try {
+ await execAsync(`git worktree add "${worktreePath}" ${branchName}`, {
+ cwd: projectPath,
+ });
+ } catch (error) {
+ // Worktree creation failed, fall back to direct execution
+ console.error(`[AutoMode] Worktree creation failed:`, error);
+ return projectPath;
+ }
+
+ return worktreePath;
+ }
+
+ private async loadFeature(projectPath: string, featureId: string): Promise {
+ const featurePath = path.join(
+ projectPath,
+ ".automaker",
+ "features",
+ featureId,
+ "feature.json"
+ );
+
+ try {
+ const data = await fs.readFile(featurePath, "utf-8");
+ return JSON.parse(data);
+ } catch {
+ return null;
+ }
+ }
+
+ private async updateFeatureStatus(
+ projectPath: string,
+ featureId: string,
+ status: string
+ ): Promise {
+ const featurePath = path.join(
+ projectPath,
+ ".automaker",
+ "features",
+ featureId,
+ "feature.json"
+ );
+
+ try {
+ const data = await fs.readFile(featurePath, "utf-8");
+ const feature = JSON.parse(data);
+ feature.status = status;
+ feature.updatedAt = new Date().toISOString();
+ await fs.writeFile(featurePath, JSON.stringify(feature, null, 2));
+ } catch {
+ // Feature file may not exist
+ }
+ }
+
+ private async loadPendingFeatures(projectPath: string): Promise {
+ const featuresDir = path.join(projectPath, ".automaker", "features");
+
+ try {
+ const entries = await fs.readdir(featuresDir, { withFileTypes: true });
+ const features: Feature[] = [];
+
+ for (const entry of entries) {
+ if (entry.isDirectory()) {
+ const featurePath = path.join(featuresDir, entry.name, "feature.json");
+ try {
+ const data = await fs.readFile(featurePath, "utf-8");
+ const feature = JSON.parse(data);
+ if (feature.status === "pending" || feature.status === "ready") {
+ features.push(feature);
+ }
+ } catch {
+ // Skip invalid features
+ }
+ }
+ }
+
+ // Sort by priority
+ return features.sort((a, b) => (a.priority || 999) - (b.priority || 999));
+ } catch {
+ return [];
+ }
+ }
+
+ private buildFeaturePrompt(feature: Feature): string {
+ let prompt = `## Feature Implementation Task
+
+**Feature ID:** ${feature.id}
+**Title:** ${feature.title}
+**Description:** ${feature.description}
+`;
+
+ if (feature.spec) {
+ prompt += `
+**Specification:**
+${feature.spec}
+`;
+ }
+
+ prompt += `
+## Instructions
+
+Implement this feature by:
+1. First, explore the codebase to understand the existing structure
+2. Plan your implementation approach
+3. Write the necessary code changes
+4. Add or update tests as needed
+5. Ensure the code follows existing patterns and conventions
+
+When done, summarize what you implemented and any notes for the developer.`;
+
+ return prompt;
+ }
+
+ private async runAgent(
+ workDir: string,
+ featureId: string,
+ prompt: string,
+ abortController: AbortController,
+ imagePaths?: string[]
+ ): Promise {
+ const options: Options = {
+ model: "claude-opus-4-5-20251101",
+ maxTurns: 50,
+ cwd: workDir,
+ allowedTools: [
+ "Read",
+ "Write",
+ "Edit",
+ "Glob",
+ "Grep",
+ "Bash",
+ ],
+ permissionMode: "acceptEdits",
+ sandbox: {
+ enabled: true,
+ autoAllowBashIfSandboxed: true,
+ },
+ abortController,
+ };
+
+ // Build prompt - include image paths for the agent to read
+ let finalPrompt = prompt;
+
+ if (imagePaths && imagePaths.length > 0) {
+ finalPrompt = `${prompt}\n\n## Reference Images\nThe following images are available for reference. Use the Read tool to view them:\n${imagePaths.map((p) => `- ${p}`).join("\n")}`;
+ }
+
+ const stream = query({ prompt: finalPrompt, options });
+ let responseText = "";
+ const outputPath = path.join(workDir, ".automaker", "features", featureId, "agent-output.md");
+
+ for await (const msg of stream) {
+ if (msg.type === "assistant" && msg.message.content) {
+ for (const block of msg.message.content) {
+ if (block.type === "text") {
+ responseText = block.text;
+ this.emitAutoModeEvent("auto_mode_progress", {
+ featureId,
+ content: block.text,
+ });
+ } else if (block.type === "tool_use") {
+ this.emitAutoModeEvent("auto_mode_tool", {
+ featureId,
+ tool: block.name,
+ input: block.input,
+ });
+ }
+ }
+ } else if (msg.type === "result" && msg.subtype === "success") {
+ responseText = msg.result || responseText;
+ }
+ }
+
+ // Save agent output
+ try {
+ await fs.mkdir(path.dirname(outputPath), { recursive: true });
+ await fs.writeFile(outputPath, responseText);
+ } catch {
+ // May fail if directory doesn't exist
+ }
+ }
+
+ private async executeFeatureWithContext(
+ projectPath: string,
+ featureId: string,
+ context: string,
+ useWorktrees: boolean
+ ): Promise {
+ const feature = await this.loadFeature(projectPath, featureId);
+ if (!feature) {
+ throw new Error(`Feature ${featureId} not found`);
+ }
+
+ const prompt = `## Continuing Feature Implementation
+
+${this.buildFeaturePrompt(feature)}
+
+## Previous Context
+The following is the output from a previous implementation attempt. Continue from where you left off:
+
+${context}
+
+## Instructions
+Review the previous work and continue the implementation. If the feature appears complete, verify it works correctly.`;
+
+ return this.executeFeature(projectPath, featureId, useWorktrees, false);
+ }
+
+ /**
+ * Emit an auto-mode event wrapped in the correct format for the client.
+ * All auto-mode events are sent as type "auto-mode:event" with the actual
+ * event type and data in the payload.
+ */
+ private emitAutoModeEvent(
+ eventType: string,
+ data: Record
+ ): void {
+ // Wrap the event in auto-mode:event format expected by the client
+ this.events.emit("auto-mode:event", {
+ type: eventType,
+ ...data,
+ });
+ }
+
+ private sleep(ms: number): Promise {
+ return new Promise((resolve) => setTimeout(resolve, ms));
+ }
+}
diff --git a/apps/server/src/services/feature-loader.ts b/apps/server/src/services/feature-loader.ts
new file mode 100644
index 00000000..04084eb4
--- /dev/null
+++ b/apps/server/src/services/feature-loader.ts
@@ -0,0 +1,263 @@
+/**
+ * Feature Loader - Handles loading and managing features from individual feature folders
+ * Each feature is stored in .automaker/features/{featureId}/feature.json
+ */
+
+import path from "path";
+import fs from "fs/promises";
+
+export interface Feature {
+ id: string;
+ category: string;
+ description: string;
+ steps?: string[];
+ passes?: boolean;
+ priority?: number;
+ imagePaths?: Array;
+ [key: string]: unknown;
+}
+
+export class FeatureLoader {
+ /**
+ * Get the features directory path
+ */
+ getFeaturesDir(projectPath: string): string {
+ return path.join(projectPath, ".automaker", "features");
+ }
+
+ /**
+ * Get the path to a specific feature folder
+ */
+ getFeatureDir(projectPath: string, featureId: string): string {
+ return path.join(this.getFeaturesDir(projectPath), featureId);
+ }
+
+ /**
+ * Get the path to a feature's feature.json file
+ */
+ getFeatureJsonPath(projectPath: string, featureId: string): string {
+ return path.join(this.getFeatureDir(projectPath, featureId), "feature.json");
+ }
+
+ /**
+ * Get the path to a feature's agent-output.md file
+ */
+ getAgentOutputPath(projectPath: string, featureId: string): string {
+ return path.join(this.getFeatureDir(projectPath, featureId), "agent-output.md");
+ }
+
+ /**
+ * Generate a new feature ID
+ */
+ generateFeatureId(): string {
+ return `feature-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
+ }
+
+ /**
+ * Get all features for a project
+ */
+ async getAll(projectPath: string): Promise {
+ try {
+ const featuresDir = this.getFeaturesDir(projectPath);
+
+ // Check if features directory exists
+ try {
+ await fs.access(featuresDir);
+ } catch {
+ return [];
+ }
+
+ // Read all feature directories
+ const entries = await fs.readdir(featuresDir, { withFileTypes: true });
+ const featureDirs = entries.filter((entry) => entry.isDirectory());
+
+ // Load each feature
+ const features: Feature[] = [];
+ for (const dir of featureDirs) {
+ const featureId = dir.name;
+ const featureJsonPath = this.getFeatureJsonPath(projectPath, featureId);
+
+ try {
+ const content = await fs.readFile(featureJsonPath, "utf-8");
+ const feature = JSON.parse(content);
+
+ if (!feature.id) {
+ console.warn(
+ `[FeatureLoader] Feature ${featureId} missing required 'id' field, skipping`
+ );
+ continue;
+ }
+
+ features.push(feature);
+ } catch (error) {
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") {
+ continue;
+ } else if (error instanceof SyntaxError) {
+ console.warn(
+ `[FeatureLoader] Failed to parse feature.json for ${featureId}: ${error.message}`
+ );
+ } else {
+ console.error(
+ `[FeatureLoader] Failed to load feature ${featureId}:`,
+ (error as Error).message
+ );
+ }
+ }
+ }
+
+ // Sort by creation order (feature IDs contain timestamp)
+ features.sort((a, b) => {
+ const aTime = a.id ? parseInt(a.id.split("-")[1] || "0") : 0;
+ const bTime = b.id ? parseInt(b.id.split("-")[1] || "0") : 0;
+ return aTime - bTime;
+ });
+
+ return features;
+ } catch (error) {
+ console.error("[FeatureLoader] Failed to get all features:", error);
+ return [];
+ }
+ }
+
+ /**
+ * Get a single feature by ID
+ */
+ async get(projectPath: string, featureId: string): Promise {
+ try {
+ const featureJsonPath = this.getFeatureJsonPath(projectPath, featureId);
+ const content = await fs.readFile(featureJsonPath, "utf-8");
+ return JSON.parse(content);
+ } catch (error) {
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") {
+ return null;
+ }
+ console.error(`[FeatureLoader] Failed to get feature ${featureId}:`, error);
+ throw error;
+ }
+ }
+
+ /**
+ * Create a new feature
+ */
+ async create(projectPath: string, featureData: Partial): Promise {
+ const featureId = featureData.id || this.generateFeatureId();
+ const featureDir = this.getFeatureDir(projectPath, featureId);
+ const featureJsonPath = this.getFeatureJsonPath(projectPath, featureId);
+
+ // Ensure features directory exists
+ const featuresDir = this.getFeaturesDir(projectPath);
+ await fs.mkdir(featuresDir, { recursive: true });
+
+ // Create feature directory
+ await fs.mkdir(featureDir, { recursive: true });
+
+ // Ensure feature has required fields
+ const feature: Feature = {
+ category: featureData.category || "Uncategorized",
+ description: featureData.description || "",
+ ...featureData,
+ id: featureId,
+ };
+
+ // Write feature.json
+ await fs.writeFile(featureJsonPath, JSON.stringify(feature, null, 2), "utf-8");
+
+ console.log(`[FeatureLoader] Created feature ${featureId}`);
+ return feature;
+ }
+
+ /**
+ * Update a feature (partial updates supported)
+ */
+ async update(
+ projectPath: string,
+ featureId: string,
+ updates: Partial
+ ): Promise {
+ const feature = await this.get(projectPath, featureId);
+ if (!feature) {
+ throw new Error(`Feature ${featureId} not found`);
+ }
+
+ // Merge updates
+ const updatedFeature: Feature = { ...feature, ...updates };
+
+ // Write back to file
+ const featureJsonPath = this.getFeatureJsonPath(projectPath, featureId);
+ await fs.writeFile(
+ featureJsonPath,
+ JSON.stringify(updatedFeature, null, 2),
+ "utf-8"
+ );
+
+ console.log(`[FeatureLoader] Updated feature ${featureId}`);
+ return updatedFeature;
+ }
+
+ /**
+ * Delete a feature
+ */
+ async delete(projectPath: string, featureId: string): Promise {
+ try {
+ const featureDir = this.getFeatureDir(projectPath, featureId);
+ await fs.rm(featureDir, { recursive: true, force: true });
+ console.log(`[FeatureLoader] Deleted feature ${featureId}`);
+ return true;
+ } catch (error) {
+ console.error(`[FeatureLoader] Failed to delete feature ${featureId}:`, error);
+ return false;
+ }
+ }
+
+ /**
+ * Get agent output for a feature
+ */
+ async getAgentOutput(
+ projectPath: string,
+ featureId: string
+ ): Promise {
+ try {
+ const agentOutputPath = this.getAgentOutputPath(projectPath, featureId);
+ const content = await fs.readFile(agentOutputPath, "utf-8");
+ return content;
+ } catch (error) {
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") {
+ return null;
+ }
+ console.error(
+ `[FeatureLoader] Failed to get agent output for ${featureId}:`,
+ error
+ );
+ throw error;
+ }
+ }
+
+ /**
+ * Save agent output for a feature
+ */
+ async saveAgentOutput(
+ projectPath: string,
+ featureId: string,
+ content: string
+ ): Promise {
+ const featureDir = this.getFeatureDir(projectPath, featureId);
+ await fs.mkdir(featureDir, { recursive: true });
+
+ const agentOutputPath = this.getAgentOutputPath(projectPath, featureId);
+ await fs.writeFile(agentOutputPath, content, "utf-8");
+ }
+
+ /**
+ * Delete agent output for a feature
+ */
+ async deleteAgentOutput(projectPath: string, featureId: string): Promise {
+ try {
+ const agentOutputPath = this.getAgentOutputPath(projectPath, featureId);
+ await fs.unlink(agentOutputPath);
+ } catch (error) {
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
+ throw error;
+ }
+ }
+ }
+}
diff --git a/apps/server/tsconfig.json b/apps/server/tsconfig.json
new file mode 100644
index 00000000..c83c5333
--- /dev/null
+++ b/apps/server/tsconfig.json
@@ -0,0 +1,20 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "module": "NodeNext",
+ "moduleResolution": "NodeNext",
+ "lib": ["ES2022"],
+ "outDir": "./dist",
+ "rootDir": "./src",
+ "strict": true,
+ "esModuleInterop": true,
+ "skipLibCheck": true,
+ "forceConsistentCasingInFileNames": true,
+ "resolveJsonModule": true,
+ "declaration": true,
+ "declarationMap": true,
+ "sourceMap": true
+ },
+ "include": ["src/**/*"],
+ "exclude": ["node_modules", "dist"]
+}
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 00000000..5a82f599
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,40 @@
+# Automaker Docker Compose
+# For self-hosting the Automaker backend server
+
+services:
+ server:
+ build:
+ context: .
+ dockerfile: apps/server/Dockerfile
+ container_name: automaker-server
+ restart: unless-stopped
+ ports:
+ - "3008:3008"
+ environment:
+ # Required
+ - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
+
+ # Optional - authentication (leave empty to disable)
+ - AUTOMAKER_API_KEY=${AUTOMAKER_API_KEY:-}
+
+ # Optional - restrict to specific directories (comma-separated)
+ - ALLOWED_PROJECT_DIRS=${ALLOWED_PROJECT_DIRS:-/projects}
+
+ # Optional - data directory for sessions, etc.
+ - DATA_DIR=/data
+
+ # Optional - CORS origin (default allows all)
+ - CORS_ORIGIN=${CORS_ORIGIN:-*}
+
+ # Optional - additional API keys
+ - OPENAI_API_KEY=${OPENAI_API_KEY:-}
+ - GOOGLE_API_KEY=${GOOGLE_API_KEY:-}
+ volumes:
+ # Persist data between restarts
+ - automaker-data:/data
+
+ # Mount your projects directory (read-write access)
+ - ${PROJECTS_DIR:-./projects}:/projects
+
+volumes:
+ automaker-data:
diff --git a/package-lock.json b/package-lock.json
index d7d208f3..c9820c7c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -75,60 +75,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "apps/app/node_modules/@anthropic-ai/claude-agent-sdk": {
- "version": "0.1.61",
- "license": "SEE LICENSE IN README.md",
- "engines": {
- "node": ">=18.0.0"
- },
- "optionalDependencies": {
- "@img/sharp-darwin-arm64": "^0.33.5",
- "@img/sharp-darwin-x64": "^0.33.5",
- "@img/sharp-linux-arm": "^0.33.5",
- "@img/sharp-linux-arm64": "^0.33.5",
- "@img/sharp-linux-x64": "^0.33.5",
- "@img/sharp-linuxmusl-arm64": "^0.33.5",
- "@img/sharp-linuxmusl-x64": "^0.33.5",
- "@img/sharp-win32-x64": "^0.33.5"
- },
- "peerDependencies": {
- "zod": "^3.24.1"
- }
- },
- "apps/app/node_modules/@anthropic-ai/claude-agent-sdk/node_modules/@img/sharp-darwin-arm64": {
- "version": "0.33.5",
- "cpu": [
- "arm64"
- ],
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/libvips"
- },
- "optionalDependencies": {
- "@img/sharp-libvips-darwin-arm64": "1.0.4"
- }
- },
- "apps/app/node_modules/@anthropic-ai/claude-agent-sdk/node_modules/@img/sharp-libvips-darwin-arm64": {
- "version": "1.0.4",
- "cpu": [
- "arm64"
- ],
- "license": "LGPL-3.0-or-later",
- "optional": true,
- "os": [
- "darwin"
- ],
- "funding": {
- "url": "https://opencollective.com/libvips"
- }
- },
"apps/app/node_modules/@babel/code-frame": {
"version": "7.27.1",
"dev": true,
@@ -1319,253 +1265,6 @@
"url": "https://github.com/sponsors/nzakas"
}
},
- "apps/app/node_modules/@img/sharp-darwin-x64": {
- "version": "0.33.5",
- "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz",
- "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==",
- "cpu": [
- "x64"
- ],
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/libvips"
- },
- "optionalDependencies": {
- "@img/sharp-libvips-darwin-x64": "1.0.4"
- }
- },
- "apps/app/node_modules/@img/sharp-libvips-darwin-x64": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz",
- "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==",
- "cpu": [
- "x64"
- ],
- "license": "LGPL-3.0-or-later",
- "optional": true,
- "os": [
- "darwin"
- ],
- "funding": {
- "url": "https://opencollective.com/libvips"
- }
- },
- "apps/app/node_modules/@img/sharp-libvips-linux-arm": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz",
- "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==",
- "cpu": [
- "arm"
- ],
- "license": "LGPL-3.0-or-later",
- "optional": true,
- "os": [
- "linux"
- ],
- "funding": {
- "url": "https://opencollective.com/libvips"
- }
- },
- "apps/app/node_modules/@img/sharp-libvips-linux-arm64": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz",
- "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==",
- "cpu": [
- "arm64"
- ],
- "license": "LGPL-3.0-or-later",
- "optional": true,
- "os": [
- "linux"
- ],
- "funding": {
- "url": "https://opencollective.com/libvips"
- }
- },
- "apps/app/node_modules/@img/sharp-libvips-linux-x64": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz",
- "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==",
- "cpu": [
- "x64"
- ],
- "license": "LGPL-3.0-or-later",
- "optional": true,
- "os": [
- "linux"
- ],
- "funding": {
- "url": "https://opencollective.com/libvips"
- }
- },
- "apps/app/node_modules/@img/sharp-libvips-linuxmusl-arm64": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz",
- "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==",
- "cpu": [
- "arm64"
- ],
- "license": "LGPL-3.0-or-later",
- "optional": true,
- "os": [
- "linux"
- ],
- "funding": {
- "url": "https://opencollective.com/libvips"
- }
- },
- "apps/app/node_modules/@img/sharp-libvips-linuxmusl-x64": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz",
- "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==",
- "cpu": [
- "x64"
- ],
- "license": "LGPL-3.0-or-later",
- "optional": true,
- "os": [
- "linux"
- ],
- "funding": {
- "url": "https://opencollective.com/libvips"
- }
- },
- "apps/app/node_modules/@img/sharp-linux-arm": {
- "version": "0.33.5",
- "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz",
- "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==",
- "cpu": [
- "arm"
- ],
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/libvips"
- },
- "optionalDependencies": {
- "@img/sharp-libvips-linux-arm": "1.0.5"
- }
- },
- "apps/app/node_modules/@img/sharp-linux-arm64": {
- "version": "0.33.5",
- "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz",
- "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==",
- "cpu": [
- "arm64"
- ],
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/libvips"
- },
- "optionalDependencies": {
- "@img/sharp-libvips-linux-arm64": "1.0.4"
- }
- },
- "apps/app/node_modules/@img/sharp-linux-x64": {
- "version": "0.33.5",
- "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz",
- "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==",
- "cpu": [
- "x64"
- ],
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/libvips"
- },
- "optionalDependencies": {
- "@img/sharp-libvips-linux-x64": "1.0.4"
- }
- },
- "apps/app/node_modules/@img/sharp-linuxmusl-arm64": {
- "version": "0.33.5",
- "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz",
- "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==",
- "cpu": [
- "arm64"
- ],
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/libvips"
- },
- "optionalDependencies": {
- "@img/sharp-libvips-linuxmusl-arm64": "1.0.4"
- }
- },
- "apps/app/node_modules/@img/sharp-linuxmusl-x64": {
- "version": "0.33.5",
- "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz",
- "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==",
- "cpu": [
- "x64"
- ],
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/libvips"
- },
- "optionalDependencies": {
- "@img/sharp-libvips-linuxmusl-x64": "1.0.4"
- }
- },
- "apps/app/node_modules/@img/sharp-win32-x64": {
- "version": "0.33.5",
- "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz",
- "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==",
- "cpu": [
- "x64"
- ],
- "license": "Apache-2.0 AND LGPL-3.0-or-later",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/libvips"
- }
- },
"apps/app/node_modules/@isaacs/balanced-match": {
"version": "4.0.1",
"dev": true,
@@ -2986,14 +2685,6 @@
"version": "2.1.0",
"license": "MIT"
},
- "apps/app/node_modules/@types/node": {
- "version": "20.19.25",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "undici-types": "~6.21.0"
- }
- },
"apps/app/node_modules/@types/plist": {
"version": "3.0.5",
"dev": true,
@@ -4208,33 +3899,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "apps/app/node_modules/call-bind-apply-helpers": {
- "version": "1.0.2",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "es-errors": "^1.3.0",
- "function-bind": "^1.1.2"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
- "apps/app/node_modules/call-bound": {
- "version": "1.0.4",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "call-bind-apply-helpers": "^1.0.2",
- "get-intrinsic": "^1.3.0"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
"apps/app/node_modules/callsites": {
"version": "3.1.0",
"dev": true,
@@ -4882,16 +4546,6 @@
"node": ">=0.10.0"
}
},
- "apps/app/node_modules/dotenv": {
- "version": "17.2.3",
- "license": "BSD-2-Clause",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://dotenvx.com"
- }
- },
"apps/app/node_modules/dotenv-expand": {
"version": "11.0.7",
"dev": true,
@@ -4917,19 +4571,6 @@
"url": "https://dotenvx.com"
}
},
- "apps/app/node_modules/dunder-proto": {
- "version": "1.0.1",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "call-bind-apply-helpers": "^1.0.1",
- "es-errors": "^1.3.0",
- "gopd": "^1.2.0"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
"apps/app/node_modules/ejs": {
"version": "3.1.10",
"dev": true,
@@ -5230,22 +4871,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "apps/app/node_modules/es-define-property": {
- "version": "1.0.1",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 0.4"
- }
- },
- "apps/app/node_modules/es-errors": {
- "version": "1.3.0",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 0.4"
- }
- },
"apps/app/node_modules/es-iterator-helpers": {
"version": "1.2.1",
"dev": true,
@@ -5272,17 +4897,6 @@
"node": ">= 0.4"
}
},
- "apps/app/node_modules/es-object-atoms": {
- "version": "1.1.1",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "es-errors": "^1.3.0"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
"apps/app/node_modules/es-set-tostringtag": {
"version": "2.1.0",
"dev": true,
@@ -6035,14 +5649,6 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
- "apps/app/node_modules/function-bind": {
- "version": "1.1.2",
- "dev": true,
- "license": "MIT",
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
"apps/app/node_modules/function.prototype.name": {
"version": "1.1.8",
"dev": true,
@@ -6094,29 +5700,6 @@
"node": "6.* || 8.* || >= 10.*"
}
},
- "apps/app/node_modules/get-intrinsic": {
- "version": "1.3.0",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "call-bind-apply-helpers": "^1.0.2",
- "es-define-property": "^1.0.1",
- "es-errors": "^1.3.0",
- "es-object-atoms": "^1.1.1",
- "function-bind": "^1.1.2",
- "get-proto": "^1.0.1",
- "gopd": "^1.2.0",
- "has-symbols": "^1.1.0",
- "hasown": "^2.0.2",
- "math-intrinsics": "^1.1.0"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
"apps/app/node_modules/get-nonce": {
"version": "1.0.1",
"license": "MIT",
@@ -6124,18 +5707,6 @@
"node": ">=6"
}
},
- "apps/app/node_modules/get-proto": {
- "version": "1.0.1",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "dunder-proto": "^1.0.1",
- "es-object-atoms": "^1.0.0"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
"apps/app/node_modules/get-stream": {
"version": "5.2.0",
"dev": true,
@@ -6166,17 +5737,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "apps/app/node_modules/get-tsconfig": {
- "version": "4.13.0",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "resolve-pkg-maps": "^1.0.0"
- },
- "funding": {
- "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
- }
- },
"apps/app/node_modules/github-from-package": {
"version": "0.0.0",
"license": "MIT"
@@ -6266,17 +5826,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "apps/app/node_modules/gopd": {
- "version": "1.2.0",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
"apps/app/node_modules/got": {
"version": "11.8.6",
"dev": true,
@@ -6347,17 +5896,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "apps/app/node_modules/has-symbols": {
- "version": "1.1.0",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
"apps/app/node_modules/has-tostringtag": {
"version": "1.0.2",
"dev": true,
@@ -6372,17 +5910,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "apps/app/node_modules/hasown": {
- "version": "2.0.2",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "function-bind": "^1.1.2"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
"apps/app/node_modules/hast-util-to-jsx-runtime": {
"version": "2.3.6",
"license": "MIT",
@@ -6614,10 +6141,6 @@
"wrappy": "1"
}
},
- "apps/app/node_modules/inherits": {
- "version": "2.0.4",
- "license": "ISC"
- },
"apps/app/node_modules/inline-style-parser": {
"version": "0.2.7",
"license": "MIT"
@@ -7471,14 +6994,6 @@
"node": ">=10"
}
},
- "apps/app/node_modules/math-intrinsics": {
- "version": "1.1.0",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 0.4"
- }
- },
"apps/app/node_modules/mdast-util-from-markdown": {
"version": "2.0.2",
"license": "MIT",
@@ -8427,25 +7942,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "apps/app/node_modules/object-assign": {
- "version": "4.1.1",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "apps/app/node_modules/object-inspect": {
- "version": "1.13.4",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
"apps/app/node_modules/object-keys": {
"version": "1.1.1",
"dev": true,
@@ -8534,13 +8030,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "apps/app/node_modules/once": {
- "version": "1.4.0",
- "license": "ISC",
- "dependencies": {
- "wrappy": "1"
- }
- },
"apps/app/node_modules/optionator": {
"version": "0.9.4",
"dev": true,
@@ -9246,14 +8735,6 @@
"node": ">=4"
}
},
- "apps/app/node_modules/resolve-pkg-maps": {
- "version": "1.0.0",
- "dev": true,
- "license": "MIT",
- "funding": {
- "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
- }
- },
"apps/app/node_modules/responselike": {
"version": "2.0.1",
"dev": true,
@@ -9404,11 +8885,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "apps/app/node_modules/safer-buffer": {
- "version": "2.1.2",
- "dev": true,
- "license": "MIT"
- },
"apps/app/node_modules/sanitize-filename": {
"version": "1.6.3",
"dev": true,
@@ -9505,74 +8981,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "apps/app/node_modules/side-channel": {
- "version": "1.1.0",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "es-errors": "^1.3.0",
- "object-inspect": "^1.13.3",
- "side-channel-list": "^1.0.0",
- "side-channel-map": "^1.0.1",
- "side-channel-weakmap": "^1.0.2"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "apps/app/node_modules/side-channel-list": {
- "version": "1.0.0",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "es-errors": "^1.3.0",
- "object-inspect": "^1.13.3"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "apps/app/node_modules/side-channel-map": {
- "version": "1.0.1",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "call-bound": "^1.0.2",
- "es-errors": "^1.3.0",
- "get-intrinsic": "^1.2.5",
- "object-inspect": "^1.13.3"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "apps/app/node_modules/side-channel-weakmap": {
- "version": "1.0.2",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "call-bound": "^1.0.2",
- "es-errors": "^1.3.0",
- "get-intrinsic": "^1.2.5",
- "object-inspect": "^1.13.3",
- "side-channel-map": "^1.0.1"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
"apps/app/node_modules/simple-concat": {
"version": "1.0.1",
"funding": [
@@ -10424,18 +9832,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "apps/app/node_modules/typescript": {
- "version": "5.9.3",
- "dev": true,
- "license": "Apache-2.0",
- "bin": {
- "tsc": "bin/tsc",
- "tsserver": "bin/tsserver"
- },
- "engines": {
- "node": ">=14.17"
- }
- },
"apps/app/node_modules/typescript-eslint": {
"version": "8.48.1",
"dev": true,
@@ -10475,11 +9871,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "apps/app/node_modules/undici-types": {
- "version": "6.21.0",
- "dev": true,
- "license": "MIT"
- },
"apps/app/node_modules/unified": {
"version": "11.0.5",
"license": "MIT",
@@ -10881,10 +10272,6 @@
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
- "apps/app/node_modules/wrappy": {
- "version": "1.0.2",
- "license": "ISC"
- },
"apps/app/node_modules/xmlbuilder": {
"version": "15.1.1",
"dev": true,
@@ -10951,13 +10338,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "apps/app/node_modules/zod": {
- "version": "3.25.76",
- "license": "MIT",
- "funding": {
- "url": "https://github.com/sponsors/colinhacks"
- }
- },
"apps/app/node_modules/zod-validation-error": {
"version": "4.0.2",
"dev": true,
@@ -11011,6 +10391,332 @@
"serve": "^14.2.4"
}
},
+ "apps/server": {
+ "name": "@automaker/server",
+ "version": "0.1.0",
+ "dependencies": {
+ "@anthropic-ai/claude-agent-sdk": "^0.1.61",
+ "cors": "^2.8.5",
+ "dotenv": "^17.2.3",
+ "express": "^5.1.0",
+ "ws": "^8.18.0"
+ },
+ "devDependencies": {
+ "@types/cors": "^2.8.18",
+ "@types/express": "^5.0.1",
+ "@types/node": "^20",
+ "@types/ws": "^8.18.1",
+ "tsx": "^4.19.4",
+ "typescript": "^5"
+ }
+ },
+ "node_modules/@anthropic-ai/claude-agent-sdk": {
+ "version": "0.1.67",
+ "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.1.67.tgz",
+ "integrity": "sha512-SPeMOfBeQ4Q6BcTRGRyMzaSEzKja3w8giZn6xboab02rPly5KQmgDK0wNerUntPe+xyw7c01xdu5K/pjZXq0dw==",
+ "license": "SEE LICENSE IN README.md",
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "optionalDependencies": {
+ "@img/sharp-darwin-arm64": "^0.33.5",
+ "@img/sharp-darwin-x64": "^0.33.5",
+ "@img/sharp-linux-arm": "^0.33.5",
+ "@img/sharp-linux-arm64": "^0.33.5",
+ "@img/sharp-linux-x64": "^0.33.5",
+ "@img/sharp-linuxmusl-arm64": "^0.33.5",
+ "@img/sharp-linuxmusl-x64": "^0.33.5",
+ "@img/sharp-win32-x64": "^0.33.5"
+ },
+ "peerDependencies": {
+ "zod": "^3.24.1"
+ }
+ },
+ "node_modules/@anthropic-ai/claude-agent-sdk/node_modules/@img/sharp-darwin-arm64": {
+ "version": "0.33.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz",
+ "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-darwin-arm64": "1.0.4"
+ }
+ },
+ "node_modules/@anthropic-ai/claude-agent-sdk/node_modules/@img/sharp-darwin-x64": {
+ "version": "0.33.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz",
+ "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-darwin-x64": "1.0.4"
+ }
+ },
+ "node_modules/@anthropic-ai/claude-agent-sdk/node_modules/@img/sharp-libvips-darwin-arm64": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz",
+ "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@anthropic-ai/claude-agent-sdk/node_modules/@img/sharp-libvips-darwin-x64": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz",
+ "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@anthropic-ai/claude-agent-sdk/node_modules/@img/sharp-libvips-linux-arm": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz",
+ "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@anthropic-ai/claude-agent-sdk/node_modules/@img/sharp-libvips-linux-arm64": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz",
+ "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@anthropic-ai/claude-agent-sdk/node_modules/@img/sharp-libvips-linux-x64": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz",
+ "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@anthropic-ai/claude-agent-sdk/node_modules/@img/sharp-libvips-linuxmusl-arm64": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz",
+ "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@anthropic-ai/claude-agent-sdk/node_modules/@img/sharp-libvips-linuxmusl-x64": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz",
+ "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@anthropic-ai/claude-agent-sdk/node_modules/@img/sharp-linux-arm": {
+ "version": "0.33.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz",
+ "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-arm": "1.0.5"
+ }
+ },
+ "node_modules/@anthropic-ai/claude-agent-sdk/node_modules/@img/sharp-linux-arm64": {
+ "version": "0.33.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz",
+ "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-arm64": "1.0.4"
+ }
+ },
+ "node_modules/@anthropic-ai/claude-agent-sdk/node_modules/@img/sharp-linux-x64": {
+ "version": "0.33.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz",
+ "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-x64": "1.0.4"
+ }
+ },
+ "node_modules/@anthropic-ai/claude-agent-sdk/node_modules/@img/sharp-linuxmusl-arm64": {
+ "version": "0.33.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz",
+ "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linuxmusl-arm64": "1.0.4"
+ }
+ },
+ "node_modules/@anthropic-ai/claude-agent-sdk/node_modules/@img/sharp-linuxmusl-x64": {
+ "version": "0.33.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz",
+ "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linuxmusl-x64": "1.0.4"
+ }
+ },
+ "node_modules/@anthropic-ai/claude-agent-sdk/node_modules/@img/sharp-win32-x64": {
+ "version": "0.33.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz",
+ "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "Apache-2.0 AND LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
"node_modules/@automaker/app": {
"resolved": "apps/app",
"link": true
@@ -11019,6 +10725,10 @@
"resolved": "apps/marketing",
"link": true
},
+ "node_modules/@automaker/server": {
+ "resolved": "apps/server",
+ "link": true
+ },
"node_modules/@emnapi/runtime": {
"version": "1.7.1",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.1.tgz",
@@ -11029,6 +10739,448 @@
"tslib": "^2.4.0"
}
},
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.27.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.1.tgz",
+ "integrity": "sha512-HHB50pdsBX6k47S4u5g/CaLjqS3qwaOVE5ILsq64jyzgMhLuCuZ8rGzM9yhsAjfjkbgUPMzZEPa7DAp7yz6vuA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.27.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.1.tgz",
+ "integrity": "sha512-kFqa6/UcaTbGm/NncN9kzVOODjhZW8e+FRdSeypWe6j33gzclHtwlANs26JrupOntlcWmB0u8+8HZo8s7thHvg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.27.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.1.tgz",
+ "integrity": "sha512-45fuKmAJpxnQWixOGCrS+ro4Uvb4Re9+UTieUY2f8AEc+t7d4AaZ6eUJ3Hva7dtrxAAWHtlEFsXFMAgNnGU9uQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.27.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.1.tgz",
+ "integrity": "sha512-LBEpOz0BsgMEeHgenf5aqmn/lLNTFXVfoWMUox8CtWWYK9X4jmQzWjoGoNb8lmAYml/tQ/Ysvm8q7szu7BoxRQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.27.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.1.tgz",
+ "integrity": "sha512-veg7fL8eMSCVKL7IW4pxb54QERtedFDfY/ASrumK/SbFsXnRazxY4YykN/THYqFnFwJ0aVjiUrVG2PwcdAEqQQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.27.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.1.tgz",
+ "integrity": "sha512-+3ELd+nTzhfWb07Vol7EZ+5PTbJ/u74nC6iv4/lwIU99Ip5uuY6QoIf0Hn4m2HoV0qcnRivN3KSqc+FyCHjoVQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.27.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.1.tgz",
+ "integrity": "sha512-/8Rfgns4XD9XOSXlzUDepG8PX+AVWHliYlUkFI3K3GB6tqbdjYqdhcb4BKRd7C0BhZSoaCxhv8kTcBrcZWP+xg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.27.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.1.tgz",
+ "integrity": "sha512-GITpD8dK9C+r+5yRT/UKVT36h/DQLOHdwGVwwoHidlnA168oD3uxA878XloXebK4Ul3gDBBIvEdL7go9gCUFzQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.27.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.1.tgz",
+ "integrity": "sha512-ieMID0JRZY/ZeCrsFQ3Y3NlHNCqIhTprJfDgSB3/lv5jJZ8FX3hqPyXWhe+gvS5ARMBJ242PM+VNz/ctNj//eA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.27.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.1.tgz",
+ "integrity": "sha512-W9//kCrh/6in9rWIBdKaMtuTTzNj6jSeG/haWBADqLLa9P8O5YSRDzgD5y9QBok4AYlzS6ARHifAb75V6G670Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.27.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.1.tgz",
+ "integrity": "sha512-VIUV4z8GD8rtSVMfAj1aXFahsi/+tcoXXNYmXgzISL+KB381vbSTNdeZHHHIYqFyXcoEhu9n5cT+05tRv13rlw==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.27.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.1.tgz",
+ "integrity": "sha512-l4rfiiJRN7sTNI//ff65zJ9z8U+k6zcCg0LALU5iEWzY+a1mVZ8iWC1k5EsNKThZ7XCQ6YWtsZ8EWYm7r1UEsg==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.27.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.1.tgz",
+ "integrity": "sha512-U0bEuAOLvO/DWFdygTHWY8C067FXz+UbzKgxYhXC0fDieFa0kDIra1FAhsAARRJbvEyso8aAqvPdNxzWuStBnA==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.27.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.1.tgz",
+ "integrity": "sha512-NzdQ/Xwu6vPSf/GkdmRNsOfIeSGnh7muundsWItmBsVpMoNPVpM61qNzAVY3pZ1glzzAxLR40UyYM23eaDDbYQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.27.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.1.tgz",
+ "integrity": "sha512-7zlw8p3IApcsN7mFw0O1Z1PyEk6PlKMu18roImfl3iQHTnr/yAfYv6s4hXPidbDoI2Q0pW+5xeoM4eTCC0UdrQ==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.27.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.1.tgz",
+ "integrity": "sha512-cGj5wli+G+nkVQdZo3+7FDKC25Uh4ZVwOAK6A06Hsvgr8WqBBuOy/1s+PUEd/6Je+vjfm6stX0kmib5b/O2Ykw==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.27.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.1.tgz",
+ "integrity": "sha512-z3H/HYI9MM0HTv3hQZ81f+AKb+yEoCRlUby1F80vbQ5XdzEMyY/9iNlAmhqiBKw4MJXwfgsh7ERGEOhrM1niMA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.27.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.1.tgz",
+ "integrity": "sha512-wzC24DxAvk8Em01YmVXyjl96Mr+ecTPyOuADAvjGg+fyBpGmxmcr2E5ttf7Im8D0sXZihpxzO1isus8MdjMCXQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.27.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.1.tgz",
+ "integrity": "sha512-1YQ8ybGi2yIXswu6eNzJsrYIGFpnlzEWRl6iR5gMgmsrR0FcNoV1m9k9sc3PuP5rUBLshOZylc9nqSgymI+TYg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.27.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.1.tgz",
+ "integrity": "sha512-5Z+DzLCrq5wmU7RDaMDe2DVXMRm2tTDvX2KU14JJVBN2CT/qov7XVix85QoJqHltpvAOZUAc3ndU56HSMWrv8g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.27.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.1.tgz",
+ "integrity": "sha512-Q73ENzIdPF5jap4wqLtsfh8YbYSZ8Q0wnxplOlZUOyZy7B4ZKW8DXGWgTCZmF8VWD7Tciwv5F4NsRf6vYlZtqg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.27.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.1.tgz",
+ "integrity": "sha512-ajbHrGM/XiK+sXM0JzEbJAen+0E+JMQZ2l4RR4VFwvV9JEERx+oxtgkpoKv1SevhjavK2z2ReHk32pjzktWbGg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.27.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.1.tgz",
+ "integrity": "sha512-IPUW+y4VIjuDVn+OMzHc5FV4GubIwPnsz6ubkvN8cuhEqH81NovB53IUlrlBkPMEPxvNnf79MGBoz8rZ2iW8HA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.27.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.1.tgz",
+ "integrity": "sha512-RIVRWiljWA6CdVu8zkWcRmGP7iRRIIwvhDKem8UMBjPql2TXM5PkDVvvrzMtj1V+WFPB4K7zkIGM7VzRtFkjdg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.27.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.1.tgz",
+ "integrity": "sha512-2BR5M8CPbptC1AK5JbJT1fWrHLvejwZidKx3UMSF0ecHMa+smhi16drIrCEggkgviBwLYd5nwrFLSl5Kho96RQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.27.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.1.tgz",
+ "integrity": "sha512-d5X6RMYv6taIymSk8JBP+nxv8DQAMY6A51GPgusqLdK9wBz5wWIXy1KjTck6HnjE9hqJzJRdk+1p/t5soSbCtw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/@img/colour": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz",
@@ -11821,6 +11973,124 @@
"node": ">= 10"
}
},
+ "node_modules/@types/body-parser": {
+ "version": "1.19.6",
+ "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz",
+ "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/connect": "*",
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/connect": {
+ "version": "3.4.38",
+ "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
+ "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/cors": {
+ "version": "2.8.19",
+ "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz",
+ "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/express": {
+ "version": "5.0.6",
+ "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz",
+ "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/body-parser": "*",
+ "@types/express-serve-static-core": "^5.0.0",
+ "@types/serve-static": "^2"
+ }
+ },
+ "node_modules/@types/express-serve-static-core": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.0.tgz",
+ "integrity": "sha512-jnHMsrd0Mwa9Cf4IdOzbz543y4XJepXrbia2T4b6+spXC2We3t1y6K44D3mR8XMFSXMCf3/l7rCgddfx7UNVBA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*",
+ "@types/qs": "*",
+ "@types/range-parser": "*",
+ "@types/send": "*"
+ }
+ },
+ "node_modules/@types/http-errors": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz",
+ "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/node": {
+ "version": "20.19.26",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.26.tgz",
+ "integrity": "sha512-0l6cjgF0XnihUpndDhk+nyD3exio3iKaYROSgvh/qSevPXax3L8p5DBRFjbvalnwatGgHEQn2R88y2fA3g4irg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~6.21.0"
+ }
+ },
+ "node_modules/@types/qs": {
+ "version": "6.14.0",
+ "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz",
+ "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/range-parser": {
+ "version": "1.2.7",
+ "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz",
+ "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/send": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz",
+ "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/serve-static": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz",
+ "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/http-errors": "*",
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/ws": {
+ "version": "8.18.1",
+ "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
+ "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
"node_modules/@zeit/schemas": {
"version": "2.36.0",
"resolved": "https://registry.npmjs.org/@zeit/schemas/-/schemas-2.36.0.tgz",
@@ -11828,6 +12098,44 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/accepts": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
+ "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-types": "^3.0.0",
+ "negotiator": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/accepts/node_modules/mime-types": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
+ "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "^1.54.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/accepts/node_modules/negotiator": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
+ "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
"node_modules/ajv": {
"version": "8.12.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz",
@@ -11961,6 +12269,53 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/body-parser": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.1.tgz",
+ "integrity": "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "^3.1.2",
+ "content-type": "^1.0.5",
+ "debug": "^4.4.3",
+ "http-errors": "^2.0.0",
+ "iconv-lite": "^0.7.0",
+ "on-finished": "^2.4.1",
+ "qs": "^6.14.0",
+ "raw-body": "^3.0.1",
+ "type-is": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/body-parser/node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/body-parser/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
"node_modules/boxen": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/boxen/-/boxen-7.0.0.tgz",
@@ -11999,12 +12354,40 @@
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/call-bound": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "get-intrinsic": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/camelcase": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-7.0.1.tgz",
@@ -12206,6 +12589,46 @@
"node": ">= 0.6"
}
},
+ "node_modules/content-type": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
+ "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie-signature": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
+ "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.6.0"
+ }
+ },
+ "node_modules/cors": {
+ "version": "2.8.5",
+ "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
+ "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
+ "license": "MIT",
+ "dependencies": {
+ "object-assign": "^4",
+ "vary": "^1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -12240,6 +12663,15 @@
"node": ">=4.0.0"
}
},
+ "node_modules/depd": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/detect-libc": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
@@ -12249,6 +12681,32 @@
"node": ">=8"
}
},
+ "node_modules/dotenv": {
+ "version": "17.2.3",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz",
+ "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://dotenvx.com"
+ }
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/eastasianwidth": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
@@ -12256,6 +12714,12 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/ee-first": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
+ "license": "MIT"
+ },
"node_modules/emoji-regex": {
"version": "9.2.2",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
@@ -12263,6 +12727,102 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/encodeurl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/esbuild": {
+ "version": "0.27.1",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.1.tgz",
+ "integrity": "sha512-yY35KZckJJuVVPXpvjgxiCuVEJT67F6zDeVTv4rizyPrfGBUpZQsvmxnN+C371c2esD/hNMjj4tpBhuueLN7aA==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.27.1",
+ "@esbuild/android-arm": "0.27.1",
+ "@esbuild/android-arm64": "0.27.1",
+ "@esbuild/android-x64": "0.27.1",
+ "@esbuild/darwin-arm64": "0.27.1",
+ "@esbuild/darwin-x64": "0.27.1",
+ "@esbuild/freebsd-arm64": "0.27.1",
+ "@esbuild/freebsd-x64": "0.27.1",
+ "@esbuild/linux-arm": "0.27.1",
+ "@esbuild/linux-arm64": "0.27.1",
+ "@esbuild/linux-ia32": "0.27.1",
+ "@esbuild/linux-loong64": "0.27.1",
+ "@esbuild/linux-mips64el": "0.27.1",
+ "@esbuild/linux-ppc64": "0.27.1",
+ "@esbuild/linux-riscv64": "0.27.1",
+ "@esbuild/linux-s390x": "0.27.1",
+ "@esbuild/linux-x64": "0.27.1",
+ "@esbuild/netbsd-arm64": "0.27.1",
+ "@esbuild/netbsd-x64": "0.27.1",
+ "@esbuild/openbsd-arm64": "0.27.1",
+ "@esbuild/openbsd-x64": "0.27.1",
+ "@esbuild/openharmony-arm64": "0.27.1",
+ "@esbuild/sunos-x64": "0.27.1",
+ "@esbuild/win32-arm64": "0.27.1",
+ "@esbuild/win32-ia32": "0.27.1",
+ "@esbuild/win32-x64": "0.27.1"
+ }
+ },
+ "node_modules/escape-html": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
+ "license": "MIT"
+ },
+ "node_modules/etag": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
"node_modules/execa": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
@@ -12287,6 +12847,110 @@
"url": "https://github.com/sindresorhus/execa?sponsor=1"
}
},
+ "node_modules/express": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
+ "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
+ "license": "MIT",
+ "dependencies": {
+ "accepts": "^2.0.0",
+ "body-parser": "^2.2.1",
+ "content-disposition": "^1.0.0",
+ "content-type": "^1.0.5",
+ "cookie": "^0.7.1",
+ "cookie-signature": "^1.2.1",
+ "debug": "^4.4.0",
+ "depd": "^2.0.0",
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "etag": "^1.8.1",
+ "finalhandler": "^2.1.0",
+ "fresh": "^2.0.0",
+ "http-errors": "^2.0.0",
+ "merge-descriptors": "^2.0.0",
+ "mime-types": "^3.0.0",
+ "on-finished": "^2.4.1",
+ "once": "^1.4.0",
+ "parseurl": "^1.3.3",
+ "proxy-addr": "^2.0.7",
+ "qs": "^6.14.0",
+ "range-parser": "^1.2.1",
+ "router": "^2.2.0",
+ "send": "^1.1.0",
+ "serve-static": "^2.2.0",
+ "statuses": "^2.0.1",
+ "type-is": "^2.0.1",
+ "vary": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/express/node_modules/content-disposition": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz",
+ "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/express/node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/express/node_modules/mime-types": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
+ "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "^1.54.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/express/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/express/node_modules/range-parser": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@@ -12294,6 +12958,92 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/finalhandler": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz",
+ "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.4.0",
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "on-finished": "^2.4.1",
+ "parseurl": "^1.3.3",
+ "statuses": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/finalhandler/node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/finalhandler/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/forwarded": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fresh": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
+ "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/geist": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/geist/-/geist-1.5.1.tgz",
@@ -12303,6 +13053,43 @@
"next": ">=13.2.0"
}
},
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/get-stream": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
@@ -12316,6 +13103,31 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/get-tsconfig": {
+ "version": "4.13.0",
+ "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz",
+ "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "resolve-pkg-maps": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
@@ -12326,6 +13138,50 @@
"node": ">=8"
}
},
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/http-errors": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
+ "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
+ "license": "MIT",
+ "dependencies": {
+ "depd": "~2.0.0",
+ "inherits": "~2.0.4",
+ "setprototypeof": "~1.2.0",
+ "statuses": "~2.0.2",
+ "toidentifier": "~1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
"node_modules/human-signals": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
@@ -12336,12 +13192,43 @@
"node": ">=10.17.0"
}
},
+ "node_modules/iconv-lite": {
+ "version": "0.7.1",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.1.tgz",
+ "integrity": "sha512-2Tth85cXwGFHfvRgZWszZSvdo+0Xsqmw8k8ZwxScfcBneNUraK+dxRxRm24nszx80Y0TVio8kKLt5sLE7ZCLlw==",
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "license": "ISC"
+ },
"node_modules/ini": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
"license": "ISC"
},
+ "node_modules/ipaddr.js": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
"node_modules/is-docker": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz",
@@ -12381,6 +13268,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/is-promise": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
+ "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
+ "license": "MIT"
+ },
"node_modules/is-stream": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
@@ -12442,6 +13335,36 @@
"url": "https://opencollective.com/parcel"
}
},
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/media-typer": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
+ "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/merge-descriptors": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
+ "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/merge-stream": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
@@ -12453,7 +13376,6 @@
"version": "1.54.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
"integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.6"
@@ -12615,6 +13537,39 @@
"node": ">=8"
}
},
+ "node_modules/object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-inspect": {
+ "version": "1.13.4",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/on-finished": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+ "license": "MIT",
+ "dependencies": {
+ "ee-first": "1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/on-headers": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz",
@@ -12625,6 +13580,15 @@
"node": ">= 0.8"
}
},
+ "node_modules/once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+ "license": "ISC",
+ "dependencies": {
+ "wrappy": "1"
+ }
+ },
"node_modules/onetime": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
@@ -12641,6 +13605,15 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/parseurl": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/path-is-inside": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz",
@@ -12699,6 +13672,19 @@
"node": "^10 || ^12 || >=14"
}
},
+ "node_modules/proxy-addr": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
+ "license": "MIT",
+ "dependencies": {
+ "forwarded": "0.2.0",
+ "ipaddr.js": "1.9.1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@@ -12709,6 +13695,21 @@
"node": ">=6"
}
},
+ "node_modules/qs": {
+ "version": "6.14.0",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
+ "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "side-channel": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/range-parser": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz",
@@ -12719,6 +13720,21 @@
"node": ">= 0.6"
}
},
+ "node_modules/raw-body": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz",
+ "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "~3.1.2",
+ "http-errors": "~2.0.1",
+ "iconv-lite": "~0.7.0",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
"node_modules/rc": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
@@ -12789,6 +13805,65 @@
"node": ">=0.10.0"
}
},
+ "node_modules/resolve-pkg-maps": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
+ "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
+ }
+ },
+ "node_modules/router": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
+ "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.4.0",
+ "depd": "^2.0.0",
+ "is-promise": "^4.0.0",
+ "parseurl": "^1.3.3",
+ "path-to-regexp": "^8.0.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "node_modules/router/node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/router/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/router/node_modules/path-to-regexp": {
+ "version": "8.3.0",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz",
+ "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
@@ -12809,6 +13884,12 @@
],
"license": "MIT"
},
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "license": "MIT"
+ },
"node_modules/scheduler": {
"version": "0.27.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
@@ -12828,6 +13909,76 @@
"node": ">=10"
}
},
+ "node_modules/send": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz",
+ "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.3.5",
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "etag": "^1.8.1",
+ "fresh": "^2.0.0",
+ "http-errors": "^2.0.0",
+ "mime-types": "^3.0.1",
+ "ms": "^2.1.3",
+ "on-finished": "^2.4.1",
+ "range-parser": "^1.2.1",
+ "statuses": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "node_modules/send/node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/send/node_modules/mime-types": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
+ "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "^1.54.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/send/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/send/node_modules/range-parser": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
"node_modules/serve": {
"version": "14.2.5",
"resolved": "https://registry.npmjs.org/serve/-/serve-14.2.5.tgz",
@@ -12880,6 +14031,27 @@
"node": ">= 0.8"
}
},
+ "node_modules/serve-static": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz",
+ "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==",
+ "license": "MIT",
+ "dependencies": {
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "parseurl": "^1.3.3",
+ "send": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "node_modules/setprototypeof": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
+ "license": "ISC"
+ },
"node_modules/sharp": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz",
@@ -12948,6 +14120,78 @@
"node": ">=8"
}
},
+ "node_modules/side-channel": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
+ "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3",
+ "side-channel-list": "^1.0.0",
+ "side-channel-map": "^1.0.1",
+ "side-channel-weakmap": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-list": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
+ "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-map": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-weakmap": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3",
+ "side-channel-map": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/signal-exit": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
@@ -12964,6 +14208,15 @@
"node": ">=0.10.0"
}
},
+ "node_modules/statuses": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
+ "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/string-width": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
@@ -13053,12 +14306,41 @@
"node": ">=8"
}
},
+ "node_modules/toidentifier": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
+ "node_modules/tsx": {
+ "version": "4.21.0",
+ "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz",
+ "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "~0.27.0",
+ "get-tsconfig": "^4.7.5"
+ },
+ "bin": {
+ "tsx": "dist/cli.mjs"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ }
+ },
"node_modules/type-fest": {
"version": "2.19.0",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz",
@@ -13072,6 +14354,66 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/type-is": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz",
+ "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==",
+ "license": "MIT",
+ "dependencies": {
+ "content-type": "^1.0.5",
+ "media-typer": "^1.1.0",
+ "mime-types": "^3.0.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/type-is/node_modules/mime-types": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
+ "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "^1.54.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
+ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/unpipe": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/update-check": {
"version": "1.5.4",
"resolved": "https://registry.npmjs.org/update-check/-/update-check-1.5.4.tgz",
@@ -13097,7 +14439,6 @@
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.8"
@@ -13152,6 +14493,42 @@
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
+ },
+ "node_modules/wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+ "license": "ISC"
+ },
+ "node_modules/ws": {
+ "version": "8.18.3",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
+ "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/zod": {
+ "version": "3.25.76",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
+ "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
}
}
}
diff --git a/package.json b/package.json
index 922aa5db..98dade6e 100644
--- a/package.json
+++ b/package.json
@@ -13,9 +13,13 @@
"dev:electron:debug": "npm run dev:electron:debug --workspace=apps/app",
"dev:electron:wsl": "npm run dev:electron:wsl --workspace=apps/app",
"dev:electron:wsl:gpu": "npm run dev:electron:wsl:gpu --workspace=apps/app",
+ "dev:server": "npm run dev --workspace=apps/server",
+ "dev:full": "concurrently \"npm run dev:server\" \"npm run dev:web\"",
"build": "npm run build --workspace=apps/app",
- "build:electron": "npm run build:electron --workspace=apps/app",
+ "build:server": "npm run build --workspace=apps/server",
+ "build:electron": "npm run build --workspace=apps/app",
"start": "npm run start --workspace=apps/app",
+ "start:server": "npm run start --workspace=apps/server",
"lint": "npm run lint --workspace=apps/app",
"test": "npm run test --workspace=apps/app",
"test:headed": "npm run test:headed --workspace=apps/app",
diff --git a/plan.md b/plan.md
new file mode 100644
index 00000000..1e719824
--- /dev/null
+++ b/plan.md
@@ -0,0 +1,267 @@
+# Plan: Full Web Support for Automaker
+
+## Goal
+Make the app work fully in web browsers while keeping Electron support. Web mode connects to a backend server (self-hosted or cloud). Electron embeds the same server locally.
+
+## Architecture
+
+```
+┌─────────────────────────────────────┐
+│ Next.js Frontend │
+│ (same code both modes) │
+└───────────────┬─────────────────────┘
+ │
+ ┌───────────┴───────────┐
+ │ │
+[Web Mode] [Electron Mode]
+ │ │
+HTTP/WebSocket HTTP/WebSocket
+to remote server to localhost:3008
+ │ │
+ └───────────┬───────────┘
+ │
+┌───────────────▼─────────────────────┐
+│ Backend Server │
+│ (apps/server) │
+│ - Express + WebSocket │
+│ - All services from electron/ │
+│ - Claude Agent SDK │
+│ - File ops, Git, PTY │
+└─────────────────────────────────────┘
+```
+
+**Key insight**: Electron uses the same HTTP API - just connects to localhost instead of remote.
+
+---
+
+## New Package: `apps/server`
+
+```
+apps/server/
+├── package.json
+├── src/
+│ ├── index.ts # Express server entry
+│ ├── routes/
+│ │ ├── fs.ts # File system routes
+│ │ ├── agent.ts # Agent routes
+│ │ ├── sessions.ts # Session routes
+│ │ ├── auto-mode.ts # Auto mode routes
+│ │ ├── features.ts # Features routes
+│ │ ├── worktree.ts # Git worktree routes
+│ │ ├── setup.ts # Setup/config routes
+│ │ └── suggestions.ts # Feature suggestions routes
+│ ├── services/ # Moved from electron/services/
+│ │ ├── agent-service.ts
+│ │ ├── auto-mode-service.ts
+│ │ ├── worktree-manager.ts
+│ │ ├── feature-loader.ts
+│ │ ├── feature-executor.ts
+│ │ └── ...
+│ └── lib/
+│ ├── events.ts # Event emitter for streaming
+│ └── security.ts # Path validation
+```
+
+---
+
+## Critical Files to Modify
+
+| File | Change |
+|------|--------|
+| `apps/app/src/lib/electron.ts` | Add `HttpApiClient` class that implements `ElectronAPI` using fetch/WebSocket |
+| `apps/app/electron/main.js` | Simplify to: spawn server + create window (remove 1500+ lines of IPC handlers) |
+| `apps/app/electron/preload.js` | Simplify to just expose `isElectron` flag |
+| `apps/app/package.json` | Remove server-side deps (Claude SDK, pty) |
+| Root `package.json` | Add `apps/server` workspace |
+
+---
+
+## Implementation Phases
+
+### Phase 1: Create Server Package (Foundation)
+1. Create `apps/server` with Express + TypeScript setup
+2. Add health check endpoint: `GET /api/health`
+3. Copy one simple service (feature-loader) and create route
+4. Test with curl/Postman
+
+### Phase 2: File System API
+1. Create `POST /api/fs/read`, `POST /api/fs/write`, etc.
+2. Add path security (allowlist validation)
+3. Update `electron.ts` with `HttpApiClient` for fs operations
+4. Test: file operations work in web mode
+
+### Phase 3: Agent API with Streaming
+1. Add WebSocket server for events (`/api/events`)
+2. Migrate `agent-service.js` to TypeScript
+3. Create routes: `POST /api/agent/send`, etc.
+4. Events stream via WebSocket instead of IPC
+5. Test: chat works in web mode
+
+### Phase 4: Sessions & Features API
+1. Migrate session management routes
+2. Migrate features CRUD routes
+3. Test: project/feature management works
+
+### Phase 5: Auto Mode & Worktree
+1. Migrate `auto-mode-service.js` (complex - has streaming)
+2. Migrate `worktree-manager.js`
+3. Test: auto mode runs features in web
+
+### Phase 6: Remaining Services
+1. Spec regeneration
+2. Feature suggestions
+3. Setup/CLI detection
+4. Model provider checks
+
+### Phase 7: Simplify Electron
+1. Update `main.js` to spawn server process + create window
+2. Remove all IPC handlers
+3. Electron app uses HTTP like web
+4. Test: Electron still works
+
+### Phase 8: Production Ready
+1. Add authentication (API key header)
+2. Configure CORS for production
+3. Add `ALLOWED_PROJECT_DIRS` env for security
+4. Docker setup for deployment
+5. Update build scripts
+
+---
+
+## API Design Pattern
+
+Convert IPC handlers to REST:
+
+```
+IPC: dialog:openDirectory → Web: User types path, POST /api/fs/validate
+IPC: fs:readFile → POST /api/fs/read { filePath }
+IPC: agent:send → POST /api/agent/send { sessionId, message, ... }
+IPC: auto-mode:start → POST /api/auto-mode/start { projectPath }
+IPC: features:getAll → GET /api/projects/:path/features
+```
+
+Streaming via WebSocket:
+```
+ws://server/api/events
+
+Events: agent:stream, auto-mode:event, suggestions:event
+```
+
+---
+
+## Web-Specific Handling
+
+| Feature | Electron | Web |
+|---------|----------|-----|
+| File picker | Native dialog | Text input + server validation |
+| Open link | shell.openExternal | window.open() |
+| Data directory | app.getPath('userData') | Server's DATA_DIR env |
+
+---
+
+## Configuration
+
+**Server `.env`:**
+```
+PORT=3008
+DATA_DIR=/path/to/data
+ANTHROPIC_API_KEY=xxx
+ALLOWED_PROJECT_DIRS=/home/user/projects
+```
+
+**Frontend `.env.local`:**
+```
+NEXT_PUBLIC_SERVER_URL=http://localhost:3008
+```
+
+---
+
+## Estimated Scope
+
+- New files: ~15-20 (server package)
+- Modified files: ~5 (electron.ts, main.js, preload.js, package.jsons)
+- Deleted lines: ~1500 (IPC handlers from main.js)
+- Services to migrate: ~10
+
+---
+
+## Implementation Status
+
+### ✅ ALL PHASES COMPLETE
+
+- [x] **Phase 1**: Server package foundation (`apps/server`)
+ - Express server with WebSocket support
+ - Event emitter for streaming
+ - Security module for path validation
+ - Health check endpoint
+
+- [x] **Phase 2**: HttpApiClient in frontend
+ - `apps/app/src/lib/http-api-client.ts` - full implementation
+ - Modified `electron.ts` to use HTTP client when not in Electron
+ - No mocks - all calls go through HTTP
+
+- [x] **Phase 3**: Agent API with streaming
+ - `apps/server/src/services/agent-service.ts`
+ - `apps/server/src/routes/agent.ts`
+ - WebSocket streaming for responses
+
+- [x] **Phase 4**: Sessions & Features API
+ - `apps/server/src/routes/sessions.ts`
+ - `apps/server/src/services/feature-loader.ts`
+ - `apps/server/src/routes/features.ts`
+
+- [x] **Phase 5**: Auto Mode & Worktree
+ - `apps/server/src/services/auto-mode-service.ts` - full implementation with Claude SDK
+ - `apps/server/src/routes/auto-mode.ts`
+ - `apps/server/src/routes/worktree.ts`
+ - `apps/server/src/routes/git.ts`
+
+- [x] **Phase 6**: Remaining services
+ - `apps/server/src/routes/setup.ts` - CLI detection, API keys, platform info
+ - `apps/server/src/routes/suggestions.ts` - AI-powered feature suggestions
+ - `apps/server/src/routes/spec-regeneration.ts` - spec generation from overview
+ - `apps/server/src/routes/models.ts` - model providers and availability
+ - `apps/server/src/routes/running-agents.ts` - active agent tracking
+
+- [x] **Phase 7**: Simplify Electron
+ - `apps/app/electron/main-simplified.js` - spawns server, minimal IPC
+ - `apps/app/electron/preload-simplified.js` - only native features exposed
+ - Updated `electron.ts` to detect simplified mode
+ - Updated `http-api-client.ts` to use native dialogs when available
+
+- [x] **Phase 8**: Production ready
+ - `apps/server/src/lib/auth.ts` - API key authentication middleware
+ - `apps/server/Dockerfile` - multi-stage Docker build
+ - `docker-compose.yml` - easy deployment configuration
+ - `apps/server/.env.example` - documented configuration
+
+---
+
+## Additional Fixes Applied
+
+### State Persistence
+- Features now cached in localStorage via Zustand persist middleware
+- Board view properly handles API failures by keeping cached data
+- Theme and UI state properly persisted across refreshes
+
+### Authentication Display
+- Server now returns proper auth method names: `oauth_token_env`, `oauth_token`, `api_key_env`, `api_key`
+- Settings view displays correct auth source (OAuth token, API key, subscription, etc.)
+- Added support for Codex subscription detection
+- Fixed "Unknown method" display issue
+
+### Bug Fixes
+- Fixed board-view.tsx crash when feature status is unknown (defaults to backlog)
+- Removed "Mock IPC" label from web mode indicator
+- Fixed unused imports and dependency warnings
+- Updated API key authentication header support in HTTP client
+
+---
+
+## Summary
+
+The architecture is simple: **one backend server, two ways to access it** (web browser or Electron shell).
+
+- **Web users**: Connect browser to your cloud-hosted server
+- **Electron users**: App spawns server locally, connects to localhost
+- **Same codebase**: Frontend code unchanged, backend services extracted to standalone server