mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-01 08:13:37 +00:00
Merge main into feat/cursor-cli-integration
Carefully merged latest changes from main branch into the Cursor CLI integration branch. This merge brings in important improvements and fixes while preserving all Cursor-related functionality. Key changes from main: - Sandbox mode security improvements and cloud storage compatibility - Version-based settings migrations (v2 schema) - Port configuration centralization - System paths utilities for CLI detection - Enhanced error handling in HttpApiClient - Windows MCP process cleanup fixes - New validation and build commands - GitHub issue templates and release process improvements Resolved conflicts in: - apps/server/src/routes/context/routes/describe-image.ts (Combined Cursor provider routing with secure-fs imports) - apps/server/src/services/auto-mode-service.ts (Merged failure tracking with raw output logging) - apps/server/tests/unit/services/terminal-service.test.ts (Updated to async tests with systemPathExists mocking) - libs/platform/src/index.ts (Combined WSL utilities with system-paths exports) - libs/types/src/settings.ts (Merged DEFAULT_PHASE_MODELS with SETTINGS_VERSION constants) All Cursor CLI integration features remain intact including: - CursorProvider and CliProvider base class - Phase-based model configuration - Provider registry and factory patterns - WSL support for Windows - Model override UI components - Cursor-specific settings and configurations 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -32,8 +32,31 @@ import type { Feature, ClaudeUsageResponse } from '@/store/app-store';
|
||||
import type { WorktreeAPI, GitAPI, ModelDefinition, ProviderStatus } from '@/types/electron';
|
||||
import { getGlobalFileBrowser } from '@/contexts/file-browser-context';
|
||||
|
||||
// Server URL - configurable via environment variable
|
||||
// Cached server URL (set during initialization in Electron mode)
|
||||
let cachedServerUrl: string | null = null;
|
||||
|
||||
/**
|
||||
* Initialize server URL from Electron IPC.
|
||||
* Must be called early in Electron mode before making API requests.
|
||||
*/
|
||||
export const initServerUrl = async (): Promise<void> => {
|
||||
if (typeof window !== 'undefined' && window.electronAPI?.getServerUrl) {
|
||||
try {
|
||||
cachedServerUrl = await window.electronAPI.getServerUrl();
|
||||
console.log('[HTTP Client] Server URL from Electron:', cachedServerUrl);
|
||||
} catch (error) {
|
||||
console.warn('[HTTP Client] Failed to get server URL from Electron:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Server URL - uses cached value from IPC or environment variable
|
||||
const getServerUrl = (): string => {
|
||||
// Use cached URL from Electron IPC if available
|
||||
if (cachedServerUrl) {
|
||||
return cachedServerUrl;
|
||||
}
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
const envUrl = import.meta.env.VITE_SERVER_URL;
|
||||
if (envUrl) return envUrl;
|
||||
@@ -41,9 +64,15 @@ const getServerUrl = (): string => {
|
||||
return 'http://localhost:3008';
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the server URL (exported for use in other modules)
|
||||
*/
|
||||
export const getServerUrlSync = (): string => getServerUrl();
|
||||
|
||||
// Cached API key for authentication (Electron mode only)
|
||||
let cachedApiKey: string | null = null;
|
||||
let apiKeyInitialized = false;
|
||||
let apiKeyInitPromise: Promise<void> | null = null;
|
||||
|
||||
// Cached session token for authentication (Web mode - explicit header auth)
|
||||
let cachedSessionToken: string | null = null;
|
||||
@@ -52,6 +81,17 @@ let cachedSessionToken: string | null = null;
|
||||
// Exported for use in WebSocket connections that need auth
|
||||
export const getApiKey = (): string | null => cachedApiKey;
|
||||
|
||||
/**
|
||||
* Wait for API key initialization to complete.
|
||||
* Returns immediately if already initialized.
|
||||
*/
|
||||
export const waitForApiKeyInit = (): Promise<void> => {
|
||||
if (apiKeyInitialized) return Promise.resolve();
|
||||
if (apiKeyInitPromise) return apiKeyInitPromise;
|
||||
// If not started yet, start it now
|
||||
return initApiKey();
|
||||
};
|
||||
|
||||
// Get session token for Web mode (returns cached value after login or token fetch)
|
||||
export const getSessionToken = (): string | null => cachedSessionToken;
|
||||
|
||||
@@ -73,30 +113,46 @@ export const isElectronMode = (): boolean => {
|
||||
};
|
||||
|
||||
/**
|
||||
* Initialize API key for Electron mode authentication.
|
||||
* Initialize API key and server URL for Electron mode authentication.
|
||||
* In web mode, authentication uses HTTP-only cookies instead.
|
||||
*
|
||||
* This should be called early in app initialization.
|
||||
*/
|
||||
export const initApiKey = async (): Promise<void> => {
|
||||
// Return existing promise if already in progress
|
||||
if (apiKeyInitPromise) return apiKeyInitPromise;
|
||||
|
||||
// Return immediately if already initialized
|
||||
if (apiKeyInitialized) return;
|
||||
apiKeyInitialized = true;
|
||||
|
||||
// Only Electron mode uses API key header auth
|
||||
if (typeof window !== 'undefined' && window.electronAPI?.getApiKey) {
|
||||
// Create and store the promise so concurrent calls wait for the same initialization
|
||||
apiKeyInitPromise = (async () => {
|
||||
try {
|
||||
cachedApiKey = await window.electronAPI.getApiKey();
|
||||
if (cachedApiKey) {
|
||||
console.log('[HTTP Client] Using API key from Electron');
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('[HTTP Client] Failed to get API key from Electron:', error);
|
||||
}
|
||||
}
|
||||
// Initialize server URL from Electron IPC first (needed for API requests)
|
||||
await initServerUrl();
|
||||
|
||||
// In web mode, authentication is handled via HTTP-only cookies
|
||||
console.log('[HTTP Client] Web mode - using cookie-based authentication');
|
||||
// Only Electron mode uses API key header auth
|
||||
if (typeof window !== 'undefined' && window.electronAPI?.getApiKey) {
|
||||
try {
|
||||
cachedApiKey = await window.electronAPI.getApiKey();
|
||||
if (cachedApiKey) {
|
||||
console.log('[HTTP Client] Using API key from Electron');
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('[HTTP Client] Failed to get API key from Electron:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// In web mode, authentication is handled via HTTP-only cookies
|
||||
console.log('[HTTP Client] Web mode - using cookie-based authentication');
|
||||
} finally {
|
||||
// Mark as initialized after completion, regardless of success or failure
|
||||
apiKeyInitialized = true;
|
||||
}
|
||||
})();
|
||||
|
||||
return apiKeyInitPromise;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -269,6 +325,32 @@ export const verifySession = async (): Promise<boolean> => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if the server is running in a containerized (sandbox) environment.
|
||||
* This endpoint is unauthenticated so it can be checked before login.
|
||||
*/
|
||||
export const checkSandboxEnvironment = async (): Promise<{
|
||||
isContainerized: boolean;
|
||||
error?: string;
|
||||
}> => {
|
||||
try {
|
||||
const response = await fetch(`${getServerUrl()}/api/health/environment`, {
|
||||
method: 'GET',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.warn('[HTTP Client] Failed to check sandbox environment');
|
||||
return { isContainerized: false, error: 'Failed to check environment' };
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return { isContainerized: data.isContainerized ?? false };
|
||||
} catch (error) {
|
||||
console.error('[HTTP Client] Sandbox environment check failed:', error);
|
||||
return { isContainerized: false, error: 'Network error' };
|
||||
}
|
||||
};
|
||||
|
||||
type EventType =
|
||||
| 'agent:stream'
|
||||
| 'auto-mode:event'
|
||||
@@ -296,7 +378,17 @@ export class HttpApiClient implements ElectronAPI {
|
||||
|
||||
constructor() {
|
||||
this.serverUrl = getServerUrl();
|
||||
this.connectWebSocket();
|
||||
// Wait for API key initialization before connecting WebSocket
|
||||
// This prevents 401 errors on startup in Electron mode
|
||||
waitForApiKeyInit()
|
||||
.then(() => {
|
||||
this.connectWebSocket();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('[HttpApiClient] API key initialization failed:', error);
|
||||
// Still attempt WebSocket connection - it may work with cookie auth
|
||||
this.connectWebSocket();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -389,8 +481,17 @@ export class HttpApiClient implements ElectronAPI {
|
||||
this.ws.onmessage = (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
console.log(
|
||||
'[HttpApiClient] WebSocket message:',
|
||||
data.type,
|
||||
'hasPayload:',
|
||||
!!data.payload,
|
||||
'callbacksRegistered:',
|
||||
this.eventCallbacks.has(data.type)
|
||||
);
|
||||
const callbacks = this.eventCallbacks.get(data.type);
|
||||
if (callbacks) {
|
||||
console.log('[HttpApiClient] Dispatching to', callbacks.size, 'callbacks');
|
||||
callbacks.forEach((cb) => cb(data.payload));
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -460,39 +561,103 @@ export class HttpApiClient implements ElectronAPI {
|
||||
}
|
||||
|
||||
private async post<T>(endpoint: string, body?: unknown): Promise<T> {
|
||||
// Ensure API key is initialized before making request
|
||||
await waitForApiKeyInit();
|
||||
const response = await fetch(`${this.serverUrl}${endpoint}`, {
|
||||
method: 'POST',
|
||||
headers: this.getHeaders(),
|
||||
credentials: 'include', // Include cookies for session auth
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
let errorMessage = `HTTP ${response.status}: ${response.statusText}`;
|
||||
try {
|
||||
const errorData = await response.json();
|
||||
if (errorData.error) {
|
||||
errorMessage = errorData.error;
|
||||
}
|
||||
} catch {
|
||||
// If parsing JSON fails, use status text
|
||||
}
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
private async get<T>(endpoint: string): Promise<T> {
|
||||
// Ensure API key is initialized before making request
|
||||
await waitForApiKeyInit();
|
||||
const response = await fetch(`${this.serverUrl}${endpoint}`, {
|
||||
headers: this.getHeaders(),
|
||||
credentials: 'include', // Include cookies for session auth
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
let errorMessage = `HTTP ${response.status}: ${response.statusText}`;
|
||||
try {
|
||||
const errorData = await response.json();
|
||||
if (errorData.error) {
|
||||
errorMessage = errorData.error;
|
||||
}
|
||||
} catch {
|
||||
// If parsing JSON fails, use status text
|
||||
}
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
private async put<T>(endpoint: string, body?: unknown): Promise<T> {
|
||||
// Ensure API key is initialized before making request
|
||||
await waitForApiKeyInit();
|
||||
const response = await fetch(`${this.serverUrl}${endpoint}`, {
|
||||
method: 'PUT',
|
||||
headers: this.getHeaders(),
|
||||
credentials: 'include', // Include cookies for session auth
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
let errorMessage = `HTTP ${response.status}: ${response.statusText}`;
|
||||
try {
|
||||
const errorData = await response.json();
|
||||
if (errorData.error) {
|
||||
errorMessage = errorData.error;
|
||||
}
|
||||
} catch {
|
||||
// If parsing JSON fails, use status text
|
||||
}
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
private async httpDelete<T>(endpoint: string): Promise<T> {
|
||||
// Ensure API key is initialized before making request
|
||||
await waitForApiKeyInit();
|
||||
const response = await fetch(`${this.serverUrl}${endpoint}`, {
|
||||
method: 'DELETE',
|
||||
headers: this.getHeaders(),
|
||||
credentials: 'include', // Include cookies for session auth
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
let errorMessage = `HTTP ${response.status}: ${response.statusText}`;
|
||||
try {
|
||||
const errorData = await response.json();
|
||||
if (errorData.error) {
|
||||
errorMessage = errorData.error;
|
||||
}
|
||||
} catch {
|
||||
// If parsing JSON fails, use status text
|
||||
}
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
@@ -566,14 +731,15 @@ export class HttpApiClient implements ElectronAPI {
|
||||
const result = await this.post<{
|
||||
success: boolean;
|
||||
path?: string;
|
||||
isAllowed?: boolean;
|
||||
error?: string;
|
||||
}>('/api/fs/validate-path', { filePath: path });
|
||||
|
||||
if (result.success && result.path) {
|
||||
if (result.success && result.path && result.isAllowed !== false) {
|
||||
return { canceled: false, filePaths: [result.path] };
|
||||
}
|
||||
|
||||
console.error('Invalid directory:', result.error);
|
||||
console.error('Invalid directory:', result.error || 'Path not allowed');
|
||||
return { canceled: true, filePaths: [] };
|
||||
}
|
||||
|
||||
@@ -1707,3 +1873,10 @@ export function getHttpApiClient(): HttpApiClient {
|
||||
}
|
||||
return httpApiClientInstance;
|
||||
}
|
||||
|
||||
// Start API key initialization immediately when this module is imported
|
||||
// This ensures the init promise is created early, even before React components mount
|
||||
// The actual async work happens in the background and won't block module loading
|
||||
initApiKey().catch((error) => {
|
||||
console.error('[HTTP Client] Failed to initialize API key:', error);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user