From 2752fbae3b21841525d8aaf7776f316e23a25e7b Mon Sep 17 00:00:00 2001 From: Mateusz Ruszkowski Date: Sun, 18 Jan 2026 17:56:51 +0100 Subject: [PATCH 1/3] fix(platform): use platform-specific paths for memory settings on Windows The Memory settings section was displaying Unix-style paths (~/.auto-claude/memories) instead of Windows paths (C:\Users\\.auto-claude\memories). This fix: - Adds new IPC handler (MEMORY_GET_DIR) to return platform-specific memories path - Updates MemoryBackendSection, SecuritySettings, MemoriesTab to fetch and display correct path - Updates GraphitiStep (onboarding wizard) to show correct platform path - Fixes path separator in memory-service.ts error message (uses path.join instead of /) - Updates env-handlers to use dynamic path in .env template - Removes trailing slashes from path displays to avoid mixed separators Co-Authored-By: Claude Opus 4.5 --- .../src/main/ipc-handlers/env-handlers.ts | 3 ++- .../src/main/ipc-handlers/memory-handlers.ts | 16 ++++++++++++++ apps/frontend/src/main/memory-service.ts | 2 +- apps/frontend/src/preload/api/project-api.ts | 3 +++ .../components/context/MemoriesTab.tsx | 18 +++++++++++++-- .../components/onboarding/GraphitiStep.tsx | 16 +++++++++++++- .../project-settings/MemoryBackendSection.tsx | 22 ++++++++++++++++--- .../project-settings/SecuritySettings.tsx | 22 ++++++++++++++++--- apps/frontend/src/shared/constants/ipc.ts | 1 + apps/frontend/src/shared/types/ipc.ts | 1 + 10 files changed, 93 insertions(+), 11 deletions(-) diff --git a/apps/frontend/src/main/ipc-handlers/env-handlers.ts b/apps/frontend/src/main/ipc-handlers/env-handlers.ts index c0d7e2278e..171b36e5fc 100644 --- a/apps/frontend/src/main/ipc-handlers/env-handlers.ts +++ b/apps/frontend/src/main/ipc-handlers/env-handlers.ts @@ -11,6 +11,7 @@ import { parseEnvFile } from './utils'; import { getClaudeCliInvocation, getClaudeCliInvocationAsync } from '../claude-cli-utils'; import { debugError } from '../../shared/utils/debug-logger'; import { getSpawnOptions, getSpawnCommand } from '../env-utils'; +import { getMemoriesDir } from '../config-paths'; // GitLab environment variable keys const GITLAB_ENV_KEYS = { @@ -336,7 +337,7 @@ ${existingVars['OLLAMA_EMBEDDING_DIM'] ? `OLLAMA_EMBEDDING_DIM=${existingVars['O # LadybugDB Database (embedded - no Docker required) ${existingVars['GRAPHITI_DATABASE'] ? `GRAPHITI_DATABASE=${existingVars['GRAPHITI_DATABASE']}` : '# GRAPHITI_DATABASE=auto_claude_memory'} -${existingVars['GRAPHITI_DB_PATH'] ? `GRAPHITI_DB_PATH=${existingVars['GRAPHITI_DB_PATH']}` : '# GRAPHITI_DB_PATH=~/.auto-claude/memories'} +${existingVars['GRAPHITI_DB_PATH'] ? `GRAPHITI_DB_PATH=${existingVars['GRAPHITI_DB_PATH']}` : `# GRAPHITI_DB_PATH=${getMemoriesDir()}`} `; return content; diff --git a/apps/frontend/src/main/ipc-handlers/memory-handlers.ts b/apps/frontend/src/main/ipc-handlers/memory-handlers.ts index 72d786a261..6b739d3848 100644 --- a/apps/frontend/src/main/ipc-handlers/memory-handlers.ts +++ b/apps/frontend/src/main/ipc-handlers/memory-handlers.ts @@ -413,6 +413,22 @@ export function registerMemoryHandlers(): void { } ); + // Get platform-specific memories directory path + ipcMain.handle( + IPC_CHANNELS.MEMORY_GET_DIR, + async (): Promise> => { + try { + const memoriesDir = getDefaultDbPath(); + return { success: true, data: memoriesDir }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to get memories directory', + }; + } + } + ); + // Test memory database connection ipcMain.handle( IPC_CHANNELS.MEMORY_TEST_CONNECTION, diff --git a/apps/frontend/src/main/memory-service.ts b/apps/frontend/src/main/memory-service.ts index 6efc625edf..208f719271 100644 --- a/apps/frontend/src/main/memory-service.ts +++ b/apps/frontend/src/main/memory-service.ts @@ -612,7 +612,7 @@ export class MemoryService { if (!data.databaseExists) { return { success: false, - message: `Database not found at ${data.databasePath}/${data.database}`, + message: `Database not found at ${path.join(data.databasePath, data.database)}`, }; } diff --git a/apps/frontend/src/preload/api/project-api.ts b/apps/frontend/src/preload/api/project-api.ts index 3852c9e440..c883897639 100644 --- a/apps/frontend/src/preload/api/project-api.ts +++ b/apps/frontend/src/preload/api/project-api.ts @@ -223,6 +223,9 @@ export const createProjectAPI = (): ProjectAPI => ({ testMemoryConnection: (dbPath?: string, database?: string): Promise> => ipcRenderer.invoke(IPC_CHANNELS.MEMORY_TEST_CONNECTION, dbPath, database), + getMemoriesDir: (): Promise> => + ipcRenderer.invoke(IPC_CHANNELS.MEMORY_GET_DIR), + // Graphiti Validation Operations validateLLMApiKey: (provider: string, apiKey: string): Promise> => ipcRenderer.invoke(IPC_CHANNELS.GRAPHITI_VALIDATE_LLM, provider, apiKey), diff --git a/apps/frontend/src/renderer/components/context/MemoriesTab.tsx b/apps/frontend/src/renderer/components/context/MemoriesTab.tsx index 736a01b065..ba6b3fc9b9 100644 --- a/apps/frontend/src/renderer/components/context/MemoriesTab.tsx +++ b/apps/frontend/src/renderer/components/context/MemoriesTab.tsx @@ -1,4 +1,4 @@ -import { useState, useMemo } from 'react'; +import { useState, useMemo, useEffect } from 'react'; import { RefreshCw, Database, @@ -79,6 +79,20 @@ export function MemoriesTab({ }: MemoriesTabProps) { const [localSearchQuery, setLocalSearchQuery] = useState(''); const [activeFilter, setActiveFilter] = useState('all'); + const [memoriesDir, setMemoriesDir] = useState(''); + + // Fetch platform-specific memories directory path for display + useEffect(() => { + window.electronAPI.getMemoriesDir() + .then((result) => { + if (result.success && result.data) { + setMemoriesDir(result.data); + } + }) + .catch((err) => { + console.error('Failed to get memories directory:', err); + }); + }, []); // Calculate memory counts by category const memoryCounts = useMemo(() => { @@ -146,7 +160,7 @@ export function MemoriesTab({ <>
- +
{/* Memory Stats Summary */} diff --git a/apps/frontend/src/renderer/components/onboarding/GraphitiStep.tsx b/apps/frontend/src/renderer/components/onboarding/GraphitiStep.tsx index 4b4d1bccb4..f329b7c372 100644 --- a/apps/frontend/src/renderer/components/onboarding/GraphitiStep.tsx +++ b/apps/frontend/src/renderer/components/onboarding/GraphitiStep.tsx @@ -146,6 +146,20 @@ export function GraphitiStep({ onNext, onBack, onSkip }: GraphitiStepProps) { database: null, provider: null }); + const [memoriesDir, setMemoriesDir] = useState(''); + + // Fetch platform-specific memories directory path + useEffect(() => { + window.electronAPI.getMemoriesDir() + .then((result) => { + if (result.success && result.data) { + setMemoriesDir(result.data); + } + }) + .catch((err) => { + console.error('Failed to get memories directory:', err); + }); + }, []); // Check LadybugDB/Kuzu availability on mount useEffect(() => { @@ -919,7 +933,7 @@ export function GraphitiStep({ onNext, onBack, onSkip }: GraphitiStepProps) { disabled={isSaving || isValidating} />

- Stored in ~/.auto-claude/graphs/ + Stored in {memoriesDir || 'memories directory'}

diff --git a/apps/frontend/src/renderer/components/project-settings/MemoryBackendSection.tsx b/apps/frontend/src/renderer/components/project-settings/MemoryBackendSection.tsx index 6db71c43f3..118b2b68a1 100644 --- a/apps/frontend/src/renderer/components/project-settings/MemoryBackendSection.tsx +++ b/apps/frontend/src/renderer/components/project-settings/MemoryBackendSection.tsx @@ -48,6 +48,9 @@ export function MemoryBackendSection({ const [ollamaStatus, setOllamaStatus] = useState<'idle' | 'checking' | 'connected' | 'disconnected'>('idle'); const [ollamaError, setOllamaError] = useState(null); + // Platform-specific memories directory path + const [memoriesDir, setMemoriesDir] = useState(''); + const embeddingProvider = envConfig.graphitiProviderConfig?.embeddingProvider || 'openai'; const ollamaBaseUrl = envConfig.graphitiProviderConfig?.ollamaBaseUrl || 'http://localhost:11434'; @@ -90,6 +93,19 @@ export function MemoryBackendSection({ } }, [embeddingProvider, envConfig.graphitiEnabled, detectOllamaModels]); + // Fetch platform-specific memories directory path + useEffect(() => { + window.electronAPI.getMemoriesDir() + .then((result) => { + if (result.success && result.data) { + setMemoriesDir(result.data); + } + }) + .catch((err) => { + console.error('Failed to get memories directory:', err); + }); + }, []); + const badge = (

- Name for the memory database (stored in ~/.auto-claude/memories/) + Name for the memory database (stored in {memoriesDir || 'memories directory'})

- Custom storage location. Default: ~/.auto-claude/memories/ + Custom storage location. Default: {memoriesDir || 'memories directory'}

onUpdateConfig({ graphitiDbPath: e.target.value || undefined })} /> diff --git a/apps/frontend/src/renderer/components/project-settings/SecuritySettings.tsx b/apps/frontend/src/renderer/components/project-settings/SecuritySettings.tsx index 2e76234843..1bd2f792fd 100644 --- a/apps/frontend/src/renderer/components/project-settings/SecuritySettings.tsx +++ b/apps/frontend/src/renderer/components/project-settings/SecuritySettings.tsx @@ -54,11 +54,27 @@ export function SecuritySettings({ azure: false }); + // Platform-specific memories directory path + const [memoriesDir, setMemoriesDir] = useState(''); + // Sync parent's showOpenAIKey prop to local state useEffect(() => { setShowApiKey(prev => ({ ...prev, openai: showOpenAIKey })); }, [showOpenAIKey]); + // Fetch platform-specific memories directory path + useEffect(() => { + window.electronAPI.getMemoriesDir() + .then((result) => { + if (result.success && result.data) { + setMemoriesDir(result.data); + } + }) + .catch((err) => { + console.error('Failed to get memories directory:', err); + }); + }, []); + const embeddingProvider = envConfig?.graphitiProviderConfig?.embeddingProvider || 'ollama'; // Toggle API key visibility @@ -437,7 +453,7 @@ export function SecuritySettings({

- Stored in ~/.auto-claude/memories/ + Stored in {memoriesDir || 'memories directory'}

- Custom storage location. Default: ~/.auto-claude/memories/ + Custom storage location. Default: {memoriesDir || 'memories directory'}

updateEnvConfig({ graphitiDbPath: e.target.value || undefined })} /> diff --git a/apps/frontend/src/shared/constants/ipc.ts b/apps/frontend/src/shared/constants/ipc.ts index c8dea9ce9f..fc5facdcd3 100644 --- a/apps/frontend/src/shared/constants/ipc.ts +++ b/apps/frontend/src/shared/constants/ipc.ts @@ -410,6 +410,7 @@ export const IPC_CHANNELS = { MEMORY_STATUS: 'memory:status', MEMORY_LIST_DATABASES: 'memory:listDatabases', MEMORY_TEST_CONNECTION: 'memory:testConnection', + MEMORY_GET_DIR: 'memory:getDir', // Get platform-specific memories directory path // Graphiti validation GRAPHITI_VALIDATE_LLM: 'graphiti:validateLlm', diff --git a/apps/frontend/src/shared/types/ipc.ts b/apps/frontend/src/shared/types/ipc.ts index f6363800dd..f7cc6c1cb2 100644 --- a/apps/frontend/src/shared/types/ipc.ts +++ b/apps/frontend/src/shared/types/ipc.ts @@ -372,6 +372,7 @@ export interface ElectronAPI { getMemoryInfrastructureStatus: (dbPath?: string) => Promise>; listMemoryDatabases: (dbPath?: string) => Promise>; testMemoryConnection: (dbPath?: string, database?: string) => Promise>; + getMemoriesDir: () => Promise>; // Graphiti validation operations validateLLMApiKey: (provider: string, apiKey: string) => Promise>; From ddde098a4d8c879d1de958bd96dd31e2c144324b Mon Sep 17 00:00:00 2001 From: Mateusz Ruszkowski Date: Sun, 18 Jan 2026 18:52:39 +0100 Subject: [PATCH 2/3] feat(memory): add LadybugDB installation detection with helpful error messages - Add checkLadybugInstalled() function to detect if real_ladybug Python package is installed - Show detailed error message in Setup Wizard when LadybugDB is not installed - Include link to Visual Studio Build Tools download for Windows users - Update InfrastructureStatus component with same error handling - Add ladybugInstalled and ladybugError fields to MemoryServiceStatus interface On Windows, LadybugDB requires Visual Studio Build Tools to compile. This change provides clear guidance to users when installation fails. Co-Authored-By: Claude Opus 4.5 --- apps/frontend/src/main/memory-service.ts | 91 +++++++++++++++++++ apps/frontend/src/preload/api/project-api.ts | 1 + .../components/onboarding/GraphitiStep.tsx | 64 ++++++++++++- .../project-settings/InfrastructureStatus.tsx | 39 ++++++-- .../renderer/lib/mocks/infrastructure-mock.ts | 8 +- apps/frontend/src/shared/types/project.ts | 2 + 6 files changed, 192 insertions(+), 13 deletions(-) diff --git a/apps/frontend/src/main/memory-service.ts b/apps/frontend/src/main/memory-service.ts index 208f719271..fc4a0e4186 100644 --- a/apps/frontend/src/main/memory-service.ts +++ b/apps/frontend/src/main/memory-service.ts @@ -744,11 +744,97 @@ export function isKuzuAvailable(): boolean { return scriptPath !== null; } +/** + * Check if LadybugDB (real_ladybug) Python package is installed + * Returns detailed status about the installation + */ +export interface LadybugInstallStatus { + installed: boolean; + pythonAvailable: boolean; + error?: string; +} + +let ladybugInstallCache: LadybugInstallStatus | null = null; + +export function checkLadybugInstalled(): LadybugInstallStatus { + // Return cached result if available (avoid repeated slow checks) + if (ladybugInstallCache !== null) { + return ladybugInstallCache; + } + + const pythonCmd = findPythonCommand(); + if (!pythonCmd) { + ladybugInstallCache = { + installed: false, + pythonAvailable: false, + error: 'Python not found. Please install Python 3.12 or later.', + }; + return ladybugInstallCache; + } + + try { + const [cmd, args] = parsePythonCommand(pythonCmd); + const checkArgs = [...args, '-c', 'import real_ladybug; print("OK")']; + + const { spawnSync } = require('child_process'); + const result = spawnSync(cmd, checkArgs, { + encoding: 'utf-8', + timeout: 10000, + windowsHide: true, + }); + + if (result.status === 0 && result.stdout?.includes('OK')) { + ladybugInstallCache = { + installed: true, + pythonAvailable: true, + }; + } else { + // Parse error to provide helpful message + const stderr = result.stderr || ''; + let error = 'LadybugDB (real_ladybug) is not installed.'; + + if (stderr.includes('ModuleNotFoundError') || stderr.includes('No module named')) { + error = + 'LadybugDB (real_ladybug) is not installed. ' + + 'On Windows, this may require Visual Studio Build Tools to compile.'; + } else if (stderr.includes('WinError 2') || stderr.includes('system cannot find')) { + error = + 'Failed to build LadybugDB. ' + + 'Please install Visual Studio Build Tools with C++ workload from: ' + + 'https://visualstudio.microsoft.com/visual-cpp-build-tools/'; + } + + ladybugInstallCache = { + installed: false, + pythonAvailable: true, + error, + }; + } + } catch (err) { + ladybugInstallCache = { + installed: false, + pythonAvailable: true, + error: `Failed to check LadybugDB: ${err instanceof Error ? err.message : String(err)}`, + }; + } + + return ladybugInstallCache; +} + +/** + * Clear the LadybugDB installation cache (useful after installation attempt) + */ +export function clearLadybugInstallCache(): void { + ladybugInstallCache = null; +} + /** * Get memory service status */ export interface MemoryServiceStatus { kuzuInstalled: boolean; + ladybugInstalled: boolean; + ladybugError?: string; databasePath: string; databaseExists: boolean; databases: string[]; @@ -765,8 +851,13 @@ export function getMemoryServiceStatus(dbPath?: string): MemoryServiceStatus { const pythonAvailable = findPythonCommand() !== null; const scriptAvailable = getQueryScriptPath() !== null; + // Check if LadybugDB is actually installed + const ladybugStatus = checkLadybugInstalled(); + return { kuzuInstalled: pythonAvailable && scriptAvailable, + ladybugInstalled: ladybugStatus.installed, + ladybugError: ladybugStatus.error, databasePath: basePath, databaseExists: databases.length > 0, databases, diff --git a/apps/frontend/src/preload/api/project-api.ts b/apps/frontend/src/preload/api/project-api.ts index c883897639..70de7a7ec7 100644 --- a/apps/frontend/src/preload/api/project-api.ts +++ b/apps/frontend/src/preload/api/project-api.ts @@ -63,6 +63,7 @@ export interface ProjectAPI { getMemoryInfrastructureStatus: (dbPath?: string) => Promise>; listMemoryDatabases: (dbPath?: string) => Promise>; testMemoryConnection: (dbPath?: string, database?: string) => Promise>; + getMemoriesDir: () => Promise>; // Graphiti Validation Operations validateLLMApiKey: (provider: string, apiKey: string) => Promise>; diff --git a/apps/frontend/src/renderer/components/onboarding/GraphitiStep.tsx b/apps/frontend/src/renderer/components/onboarding/GraphitiStep.tsx index f329b7c372..53e0e363d5 100644 --- a/apps/frontend/src/renderer/components/onboarding/GraphitiStep.tsx +++ b/apps/frontend/src/renderer/components/onboarding/GraphitiStep.tsx @@ -141,6 +141,8 @@ export function GraphitiStep({ onNext, onBack, onSkip }: GraphitiStepProps) { const [success, setSuccess] = useState(false); const [isCheckingInfra, setIsCheckingInfra] = useState(true); const [kuzuAvailable, setKuzuAvailable] = useState(null); + const [ladybugInstalled, setLadybugInstalled] = useState(null); + const [ladybugError, setLadybugError] = useState(null); const [isValidating, setIsValidating] = useState(false); const [validationStatus, setValidationStatus] = useState({ database: null, @@ -167,9 +169,13 @@ export function GraphitiStep({ onNext, onBack, onSkip }: GraphitiStepProps) { setIsCheckingInfra(true); try { const result = await window.electronAPI.getMemoryInfrastructureStatus(); - setKuzuAvailable(result?.success && result?.data?.memory?.kuzuInstalled ? true : false); + const memory = result?.data?.memory; + setKuzuAvailable(result?.success && memory?.kuzuInstalled ? true : false); + setLadybugInstalled(memory?.ladybugInstalled ?? null); + setLadybugError(memory?.ladybugError ?? null); } catch { setKuzuAvailable(false); + setLadybugInstalled(false); } finally { setIsCheckingInfra(false); } @@ -819,8 +825,41 @@ export function GraphitiStep({ onNext, onBack, onSkip }: GraphitiStepProps) { )} - {/* Kuzu status notice */} - {kuzuAvailable === false && ( + {/* LadybugDB installation status */} + {ladybugInstalled === false && ladybugError && ( + + +
+ +
+

+ LadybugDB Not Installed +

+

+ {ladybugError} +

+ {ladybugError.includes('Visual Studio Build Tools') && ( + + + Download Visual Studio Build Tools + + )} +

+ After installing build tools, restart the application to retry. +

+
+
+
+
+ )} + + {/* Database will be created notice (when LadybugDB is installed but no DB yet) */} + {ladybugInstalled === true && kuzuAvailable === false && (
@@ -839,6 +878,25 @@ export function GraphitiStep({ onNext, onBack, onSkip }: GraphitiStepProps) { )} + {/* LadybugDB ready notice */} + {ladybugInstalled === true && kuzuAvailable === true && ( + + +
+ +
+

+ LadybugDB Ready +

+

+ Memory database is installed and available. +

+
+
+
+
+ )} + {/* Info card about Graphiti */} diff --git a/apps/frontend/src/renderer/components/project-settings/InfrastructureStatus.tsx b/apps/frontend/src/renderer/components/project-settings/InfrastructureStatus.tsx index 2e889c3a57..bcd27ba76f 100644 --- a/apps/frontend/src/renderer/components/project-settings/InfrastructureStatus.tsx +++ b/apps/frontend/src/renderer/components/project-settings/InfrastructureStatus.tsx @@ -1,4 +1,4 @@ -import { Loader2, CheckCircle2, AlertCircle, Database } from 'lucide-react'; +import { Loader2, CheckCircle2, AlertCircle, Database, ExternalLink } from 'lucide-react'; import type { InfrastructureStatus as InfrastructureStatusType } from '../../../shared/types'; interface InfrastructureStatusProps { @@ -14,6 +14,9 @@ export function InfrastructureStatus({ infrastructureStatus, isCheckingInfrastructure, }: InfrastructureStatusProps) { + const ladybugInstalled = infrastructureStatus?.memory.ladybugInstalled; + const ladybugError = infrastructureStatus?.memory.ladybugError; + return (
@@ -23,25 +26,43 @@ export function InfrastructureStatus({ )}
- {/* Kuzu Installation Status */} + {/* LadybugDB Installation Status */}
- {infrastructureStatus?.memory.kuzuInstalled ? ( + {ladybugInstalled ? ( ) : ( )} - Kuzu Database + LadybugDB
- {infrastructureStatus?.memory.kuzuInstalled ? ( + {ladybugInstalled ? ( Installed ) : ( - Not Available + Not Installed )}
+ {/* LadybugDB Error Details */} + {!ladybugInstalled && ladybugError && ( +
+

{ladybugError}

+ {ladybugError.includes('Visual Studio Build Tools') && ( + + + Download Visual Studio Build Tools + + )} +
+ )} + {/* Database Status */}
@@ -55,10 +76,10 @@ export function InfrastructureStatus({
{infrastructureStatus?.memory.databaseExists ? ( Ready - ) : infrastructureStatus?.memory.kuzuInstalled ? ( + ) : ladybugInstalled ? ( Will be created on first use ) : ( - Requires Kuzu + Requires LadybugDB )}
@@ -76,7 +97,7 @@ export function InfrastructureStatus({ Graph memory is ready to use
- ) : infrastructureStatus && !infrastructureStatus.memory.kuzuInstalled && ( + ) : infrastructureStatus && !ladybugInstalled && (

Graph memory requires Python 3.12+ with LadybugDB. No Docker needed.

diff --git a/apps/frontend/src/renderer/lib/mocks/infrastructure-mock.ts b/apps/frontend/src/renderer/lib/mocks/infrastructure-mock.ts index 81168fa011..21044bee3a 100644 --- a/apps/frontend/src/renderer/lib/mocks/infrastructure-mock.ts +++ b/apps/frontend/src/renderer/lib/mocks/infrastructure-mock.ts @@ -10,7 +10,8 @@ export const infrastructureMock = { data: { memory: { kuzuInstalled: true, - databasePath: '~/.auto-claude/graphs', + ladybugInstalled: true, + databasePath: '~/.auto-claude/memories', databaseExists: true, databases: ['auto_claude_memory'] }, @@ -32,6 +33,11 @@ export const infrastructureMock = { } }), + getMemoriesDir: async () => ({ + success: true, + data: '~/.auto-claude/memories' + }), + // LLM API Validation Operations validateLLMApiKey: async () => ({ success: true, diff --git a/apps/frontend/src/shared/types/project.ts b/apps/frontend/src/shared/types/project.ts index a0bd234b4c..e90876700f 100644 --- a/apps/frontend/src/shared/types/project.ts +++ b/apps/frontend/src/shared/types/project.ts @@ -152,6 +152,8 @@ export interface GraphitiMemoryStatus { // Memory Infrastructure Types export interface MemoryDatabaseStatus { kuzuInstalled: boolean; + ladybugInstalled: boolean; + ladybugError?: string; databasePath: string; databaseExists: boolean; databases: string[]; From 931baddb2ad86e2614574920cf0b0600d68ae3d2 Mon Sep 17 00:00:00 2001 From: Mateusz Ruszkowski Date: Mon, 19 Jan 2026 01:24:28 +0100 Subject: [PATCH 3/3] fix(terminal): add PowerShell call operator for command execution PowerShell interprets -- flags as the pre-decrement operator, causing errors like "The '--' operator works only on variables or on properties". Changes: - Add shellType parameter to escapeShellCommand function - Use call operator (&) prefix for PowerShell commands: & "cmd.exe" - Update all 4 escapeShellCommand calls to pass terminal.shellType - Add comprehensive tests for escapeShellCommand function This fixes Claude CLI invocation when using PowerShell as the terminal. Co-Authored-By: Claude Opus 4.5 --- .../claude-integration-handler.test.ts | 47 +++++++++++++++++++ .../terminal/claude-integration-handler.ts | 25 +++++++--- 2 files changed, 66 insertions(+), 6 deletions(-) diff --git a/apps/frontend/src/main/terminal/__tests__/claude-integration-handler.test.ts b/apps/frontend/src/main/terminal/__tests__/claude-integration-handler.test.ts index b83012b1f5..657f35d5fe 100644 --- a/apps/frontend/src/main/terminal/__tests__/claude-integration-handler.test.ts +++ b/apps/frontend/src/main/terminal/__tests__/claude-integration-handler.test.ts @@ -643,6 +643,53 @@ describe('claude-integration-handler - Helper Functions', () => { }); }); + describe('escapeShellCommand', () => { + it('should add & call operator for PowerShell on Windows', async () => { + mockPlatform('win32'); + const { escapeShellCommand } = await import('../claude-integration-handler'); + + // PowerShell needs & to execute commands with -- flags + // Without &, PowerShell interprets -- as the decrement operator + const result = escapeShellCommand('C:\\Users\\test\\claude.exe', 'powershell'); + expect(result).toBe('& "C:\\Users\\test\\claude.exe"'); + }); + + it('should NOT add & call operator for cmd.exe on Windows', async () => { + mockPlatform('win32'); + const { escapeShellCommand } = await import('../claude-integration-handler'); + + const result = escapeShellCommand('C:\\Users\\test\\claude.exe', 'cmd'); + expect(result).toBe('"C:\\Users\\test\\claude.exe"'); + expect(result).not.toContain('&'); + }); + + it('should default to cmd.exe style when shellType is undefined on Windows', async () => { + mockPlatform('win32'); + const { escapeShellCommand } = await import('../claude-integration-handler'); + + const result = escapeShellCommand('C:\\Users\\test\\claude.exe'); + expect(result).toBe('"C:\\Users\\test\\claude.exe"'); + expect(result).not.toContain('&'); + }); + + it('should use single quotes on Unix', async () => { + mockPlatform('darwin'); + const { escapeShellCommand } = await import('../claude-integration-handler'); + + const result = escapeShellCommand('/usr/local/bin/claude'); + expect(result).toBe("'/usr/local/bin/claude'"); + }); + + it('should escape special characters in PowerShell path', async () => { + mockPlatform('win32'); + const { escapeShellCommand } = await import('../claude-integration-handler'); + + // Paths with special chars like % should be escaped + const result = escapeShellCommand('C:\\Users\\test%user\\claude.exe', 'powershell'); + expect(result).toBe('& "C:\\Users\\test%%user\\claude.exe"'); + }); + }); + describe('finalizeClaudeInvoke', () => { it('should set terminal title to "Claude" for default profile when terminal has default name', async () => { const { finalizeClaudeInvoke } = await import('../claude-integration-handler'); diff --git a/apps/frontend/src/main/terminal/claude-integration-handler.ts b/apps/frontend/src/main/terminal/claude-integration-handler.ts index ccf6a64759..9c0abd0626 100644 --- a/apps/frontend/src/main/terminal/claude-integration-handler.ts +++ b/apps/frontend/src/main/terminal/claude-integration-handler.ts @@ -14,7 +14,7 @@ import * as OutputParser from './output-parser'; import * as SessionHandler from './session-handler'; import * as PtyManager from './pty-manager'; import { debugLog, debugError } from '../../shared/utils/debug-logger'; -import { escapeShellArg, escapeForWindowsDoubleQuote, buildCdCommand } from '../../shared/utils/shell-escape'; +import { escapeShellArg, escapeForWindowsDoubleQuote, buildCdCommand, type WindowsShellType } from '../../shared/utils/shell-escape'; import { getClaudeCliInvocation, getClaudeCliInvocationAsync } from '../claude-cli-utils'; import { isWindows } from '../platform'; import type { @@ -94,16 +94,29 @@ function buildPathPrefix(pathEnv: string): string { * On Windows, wraps in double quotes for cmd.exe. Since the value is inside * double quotes, we use escapeForWindowsDoubleQuote() (only escapes embedded * double quotes as ""). Caret escaping is NOT used inside double quotes. + * + * For PowerShell, adds the call operator (&) before the command to prevent + * PowerShell from interpreting -- flags as the pre-decrement operator. + * * On Unix/macOS, wraps in single quotes for bash. * * @param cmd - The command to escape + * @param shellType - On Windows, specify 'powershell' or 'cmd' for correct syntax. * @returns The escaped command safe for use in shell commands */ -function escapeShellCommand(cmd: string): string { +export function escapeShellCommand(cmd: string, shellType?: WindowsShellType): string { if (isWindows()) { // Windows: Wrap in double quotes and escape only embedded double quotes // Inside double quotes, caret is literal, so use escapeForWindowsDoubleQuote() const escapedCmd = escapeForWindowsDoubleQuote(cmd); + + if (shellType === 'powershell') { + // PowerShell: Use call operator (&) to execute the command + // Without &, PowerShell interprets "--flag" as pre-decrement operator + return `& "${escapedCmd}"`; + } + + // cmd.exe: Just wrap in double quotes return `"${escapedCmd}"`; } // Unix/macOS: Wrap in single quotes for bash @@ -571,7 +584,7 @@ export function invokeClaude( const cwdCommand = buildCdCommand(cwd, terminal.shellType); const { command: claudeCmd, env: claudeEnv } = getClaudeCliInvocation(); - const escapedClaudeCmd = escapeShellCommand(claudeCmd); + const escapedClaudeCmd = escapeShellCommand(claudeCmd, terminal.shellType); const pathPrefix = buildPathPrefix(claudeEnv.PATH || ''); const needsEnvOverride = profileId && profileId !== previousProfileId; @@ -676,7 +689,7 @@ export function resumeClaude( SessionHandler.releaseSessionId(terminal.id); const { command: claudeCmd, env: claudeEnv } = getClaudeCliInvocation(); - const escapedClaudeCmd = escapeShellCommand(claudeCmd); + const escapedClaudeCmd = escapeShellCommand(claudeCmd, terminal.shellType); const pathPrefix = buildPathPrefix(claudeEnv.PATH || ''); // Always use --continue which resumes the most recent session in the current directory. @@ -793,7 +806,7 @@ export async function invokeClaudeAsync( if (timeoutId) clearTimeout(timeoutId); }); - const escapedClaudeCmd = escapeShellCommand(claudeCmd); + const escapedClaudeCmd = escapeShellCommand(claudeCmd, terminal.shellType); const pathPrefix = buildPathPrefix(claudeEnv.PATH || ''); const needsEnvOverride = profileId && profileId !== previousProfileId; @@ -907,7 +920,7 @@ export async function resumeClaudeAsync( if (timeoutId) clearTimeout(timeoutId); }); - const escapedClaudeCmd = escapeShellCommand(claudeCmd); + const escapedClaudeCmd = escapeShellCommand(claudeCmd, terminal.shellType); const pathPrefix = buildPathPrefix(claudeEnv.PATH || ''); // Always use --continue which resumes the most recent session in the current directory.