fix: detect and clear cross-platform CLI paths in settings (#535)
* perf: fix frontend lag with batched IPC events and optimized store updates Critical performance fixes addressing 2-5s UI lag during task execution: Frontend optimizations: - Batch IPC log events (100+/sec → 6/sec batched updates) - Add batchAppendLogs to task-store for efficient log appending - Only set updatedAt on phase changes, not every progress tick - Memoize sanitizeMarkdownForDisplay and formatRelativeTime in TaskCard - Add React.memo with custom comparators to DroppableColumn - Use IntersectionObserver to pause animations when cards not visible - Reduce debug logging verbosity Backend optimizations: - Add project index caching with 5-minute TTL in client.py - Batch StatusManager file writes with threading.Timer debounce 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * chore(deps): update all frontend dependencies including major versions Updates all frontend dependencies to latest versions: - react-resizable-panels 3.0.6 → 4.2.0 (breaking API change) - globals 16.5.0 → 17.0.0 - lucide-react 0.560.0 → 0.562.0 - zod 4.2.1 → 4.3.4 - Plus other minor/patch updates Updates TerminalGrid.tsx for react-resizable-panels v4 API: - PanelGroup → Group - PanelResizeHandle → Separator - direction → orientation - Removed order prop - Changed div wrappers to React.Fragment for proper resize handling 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(memory): fix learning loop to retrieve patterns and gotchas The memory system was storing patterns and gotchas correctly (100% working) but never retrieving them for agent prompts. The root cause was that get_relevant_context() only performed generic semantic search without filtering for specific episode types. Changes: - Add get_patterns_and_gotchas() method to search.py that specifically retrieves PATTERN and GOTCHA episodes with focused queries - Add min_score filtering to reduce noise from low-relevance results - Add wrapper method to graphiti.py facade class - Update memory_manager.py to call new method and format results into dedicated "Learned Patterns" and "Known Gotchas" sections This enables cross-session learning where patterns discovered in session 1 will now be available to sessions 2, 3, 4, etc. * memory is now a app wide setting * fix: detect and clear cross-platform CLI paths in settings When settings are synced/transferred between platforms (e.g., Windows to macOS), CLI tool paths can persist with wrong platform separators causing "Claude Code not found" errors with Windows paths on macOS. Changes: - Add isWrongPlatformPath() to detect paths from different platforms - Update all CLI tool detection methods to skip wrong-platform paths - Add settings migration to clear cross-platform paths on load - Export isPathFromWrongPlatform() for use in settings handlers Fixes issue where Windows paths like C:\Users\...\claude.exe appeared in error messages on macOS systems. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(security): address CodeRabbit security findings Security fixes: - [CRITICAL] Fix command injection in validatePython() by using execFileSync instead of execSync with string interpolation (cli-tool-manager.ts) - [HIGH] Add path validation to FILE_EXPLORER_LIST to prevent directory traversal attacks (file-handlers.ts) - [HIGH] Fix xterm shell injection by using cwd option instead of embedding path in bash -c command (settings-handlers.ts) - [MEDIUM] Add URL scheme validation to SHELL_OPEN_EXTERNAL to block dangerous protocols like file:// and javascript: (settings-handlers.ts) Other fixes: - [MEDIUM] Fix TaskCard memo comparison to check all subtasks, not just first 5 (TaskCard.tsx) - [LOW] Fix type hint for optional BuildStatus parameter (status.py) - [LOW] Remove unused useRef import (useIpc.ts) Already fixed (no action needed): - Race conditions in client.py and status.py (locks already in place) - IntersectionObserver in PhaseProgressIndicator (dependency array already []) - Unused imports in KanbanBoard.tsx (already removed) - memory-env-builder.ts exists (CodeRabbit incorrectly reported missing) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -182,7 +182,7 @@ class StatusManager:
|
||||
f"[StatusManager] Scheduled batched write in {self._WRITE_DEBOUNCE_MS}ms"
|
||||
)
|
||||
|
||||
def write(self, status: BuildStatus = None, immediate: bool = False) -> None:
|
||||
def write(self, status: BuildStatus | None = None, immediate: bool = False) -> None:
|
||||
"""Write status to file.
|
||||
|
||||
Args:
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
* - Graceful fallbacks when tools not found
|
||||
*/
|
||||
|
||||
import { execSync, execFileSync } from 'child_process';
|
||||
import { execFileSync } from 'child_process';
|
||||
import { existsSync } from 'fs';
|
||||
import path from 'path';
|
||||
import os from 'os';
|
||||
@@ -64,6 +64,45 @@ interface CacheEntry {
|
||||
source: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a path appears to be from a different platform.
|
||||
* Detects Windows paths on Unix and Unix paths on Windows.
|
||||
*
|
||||
* @param pathStr - The path to check
|
||||
* @returns true if the path is from a different platform
|
||||
*/
|
||||
function isWrongPlatformPath(pathStr: string | undefined): boolean {
|
||||
if (!pathStr) return false;
|
||||
|
||||
const isWindows = process.platform === 'win32';
|
||||
|
||||
if (isWindows) {
|
||||
// On Windows, reject Unix-style absolute paths (starting with /)
|
||||
// but allow relative paths and Windows paths
|
||||
if (pathStr.startsWith('/') && !pathStr.startsWith('//')) {
|
||||
// Unix absolute path on Windows
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
// On Unix (macOS/Linux), reject Windows-style paths
|
||||
// Windows paths have: drive letter (C:), backslashes, or specific Windows paths
|
||||
if (/^[A-Za-z]:[/\\]/.test(pathStr)) {
|
||||
// Drive letter path (C:\, D:/, etc.)
|
||||
return true;
|
||||
}
|
||||
if (pathStr.includes('\\')) {
|
||||
// Contains backslashes (Windows path separators)
|
||||
return true;
|
||||
}
|
||||
if (pathStr.includes('AppData') || pathStr.includes('Program Files')) {
|
||||
// Contains Windows-specific directory names
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Centralized CLI Tool Manager
|
||||
*
|
||||
@@ -165,7 +204,7 @@ class CLIToolManager {
|
||||
* Detect Python with multi-level priority
|
||||
*
|
||||
* Priority order:
|
||||
* 1. User configuration
|
||||
* 1. User configuration (if valid for current platform)
|
||||
* 2. Bundled Python (packaged apps only)
|
||||
* 3. Homebrew Python (macOS)
|
||||
* 4. System PATH (py -3, python3, python)
|
||||
@@ -179,19 +218,26 @@ class CLIToolManager {
|
||||
|
||||
// 1. User configuration
|
||||
if (this.userConfig.pythonPath) {
|
||||
const validation = this.validatePython(this.userConfig.pythonPath);
|
||||
if (validation.valid) {
|
||||
return {
|
||||
found: true,
|
||||
path: this.userConfig.pythonPath,
|
||||
version: validation.version,
|
||||
source: 'user-config',
|
||||
message: `Using user-configured Python: ${this.userConfig.pythonPath}`,
|
||||
};
|
||||
// Check if path is from wrong platform (e.g., Windows path on macOS)
|
||||
if (isWrongPlatformPath(this.userConfig.pythonPath)) {
|
||||
console.warn(
|
||||
`[Python] User-configured path is from different platform, ignoring: ${this.userConfig.pythonPath}`
|
||||
);
|
||||
} else {
|
||||
const validation = this.validatePython(this.userConfig.pythonPath);
|
||||
if (validation.valid) {
|
||||
return {
|
||||
found: true,
|
||||
path: this.userConfig.pythonPath,
|
||||
version: validation.version,
|
||||
source: 'user-config',
|
||||
message: `Using user-configured Python: ${this.userConfig.pythonPath}`,
|
||||
};
|
||||
}
|
||||
console.warn(
|
||||
`[Python] User-configured path invalid: ${validation.message}`
|
||||
);
|
||||
}
|
||||
console.warn(
|
||||
`[Python] User-configured path invalid: ${validation.message}`
|
||||
);
|
||||
}
|
||||
|
||||
// 2. Bundled Python (packaged apps only)
|
||||
@@ -279,7 +325,7 @@ class CLIToolManager {
|
||||
* Detect Git with multi-level priority
|
||||
*
|
||||
* Priority order:
|
||||
* 1. User configuration
|
||||
* 1. User configuration (if valid for current platform)
|
||||
* 2. Homebrew Git (macOS)
|
||||
* 3. System PATH
|
||||
*
|
||||
@@ -288,17 +334,24 @@ class CLIToolManager {
|
||||
private detectGit(): ToolDetectionResult {
|
||||
// 1. User configuration
|
||||
if (this.userConfig.gitPath) {
|
||||
const validation = this.validateGit(this.userConfig.gitPath);
|
||||
if (validation.valid) {
|
||||
return {
|
||||
found: true,
|
||||
path: this.userConfig.gitPath,
|
||||
version: validation.version,
|
||||
source: 'user-config',
|
||||
message: `Using user-configured Git: ${this.userConfig.gitPath}`,
|
||||
};
|
||||
// Check if path is from wrong platform (e.g., Windows path on macOS)
|
||||
if (isWrongPlatformPath(this.userConfig.gitPath)) {
|
||||
console.warn(
|
||||
`[Git] User-configured path is from different platform, ignoring: ${this.userConfig.gitPath}`
|
||||
);
|
||||
} else {
|
||||
const validation = this.validateGit(this.userConfig.gitPath);
|
||||
if (validation.valid) {
|
||||
return {
|
||||
found: true,
|
||||
path: this.userConfig.gitPath,
|
||||
version: validation.version,
|
||||
source: 'user-config',
|
||||
message: `Using user-configured Git: ${this.userConfig.gitPath}`,
|
||||
};
|
||||
}
|
||||
console.warn(`[Git] User-configured path invalid: ${validation.message}`);
|
||||
}
|
||||
console.warn(`[Git] User-configured path invalid: ${validation.message}`);
|
||||
}
|
||||
|
||||
// 2. Homebrew (macOS)
|
||||
@@ -351,7 +404,7 @@ class CLIToolManager {
|
||||
* Detect GitHub CLI with multi-level priority
|
||||
*
|
||||
* Priority order:
|
||||
* 1. User configuration
|
||||
* 1. User configuration (if valid for current platform)
|
||||
* 2. Homebrew gh (macOS)
|
||||
* 3. System PATH
|
||||
* 4. Windows Program Files
|
||||
@@ -361,19 +414,26 @@ class CLIToolManager {
|
||||
private detectGitHubCLI(): ToolDetectionResult {
|
||||
// 1. User configuration
|
||||
if (this.userConfig.githubCLIPath) {
|
||||
const validation = this.validateGitHubCLI(this.userConfig.githubCLIPath);
|
||||
if (validation.valid) {
|
||||
return {
|
||||
found: true,
|
||||
path: this.userConfig.githubCLIPath,
|
||||
version: validation.version,
|
||||
source: 'user-config',
|
||||
message: `Using user-configured GitHub CLI: ${this.userConfig.githubCLIPath}`,
|
||||
};
|
||||
// Check if path is from wrong platform (e.g., Windows path on macOS)
|
||||
if (isWrongPlatformPath(this.userConfig.githubCLIPath)) {
|
||||
console.warn(
|
||||
`[GitHub CLI] User-configured path is from different platform, ignoring: ${this.userConfig.githubCLIPath}`
|
||||
);
|
||||
} else {
|
||||
const validation = this.validateGitHubCLI(this.userConfig.githubCLIPath);
|
||||
if (validation.valid) {
|
||||
return {
|
||||
found: true,
|
||||
path: this.userConfig.githubCLIPath,
|
||||
version: validation.version,
|
||||
source: 'user-config',
|
||||
message: `Using user-configured GitHub CLI: ${this.userConfig.githubCLIPath}`,
|
||||
};
|
||||
}
|
||||
console.warn(
|
||||
`[GitHub CLI] User-configured path invalid: ${validation.message}`
|
||||
);
|
||||
}
|
||||
console.warn(
|
||||
`[GitHub CLI] User-configured path invalid: ${validation.message}`
|
||||
);
|
||||
}
|
||||
|
||||
// 2. Homebrew (macOS)
|
||||
@@ -449,7 +509,7 @@ class CLIToolManager {
|
||||
* Detect Claude CLI with multi-level priority
|
||||
*
|
||||
* Priority order:
|
||||
* 1. User configuration
|
||||
* 1. User configuration (if valid for current platform)
|
||||
* 2. Homebrew claude (macOS)
|
||||
* 3. System PATH
|
||||
* 4. Windows/macOS/Linux standard locations
|
||||
@@ -459,19 +519,26 @@ class CLIToolManager {
|
||||
private detectClaude(): ToolDetectionResult {
|
||||
// 1. User configuration
|
||||
if (this.userConfig.claudePath) {
|
||||
const validation = this.validateClaude(this.userConfig.claudePath);
|
||||
if (validation.valid) {
|
||||
return {
|
||||
found: true,
|
||||
path: this.userConfig.claudePath,
|
||||
version: validation.version,
|
||||
source: 'user-config',
|
||||
message: `Using user-configured Claude CLI: ${this.userConfig.claudePath}`,
|
||||
};
|
||||
// Check if path is from wrong platform (e.g., Windows path on macOS)
|
||||
if (isWrongPlatformPath(this.userConfig.claudePath)) {
|
||||
console.warn(
|
||||
`[Claude CLI] User-configured path is from different platform, ignoring: ${this.userConfig.claudePath}`
|
||||
);
|
||||
} else {
|
||||
const validation = this.validateClaude(this.userConfig.claudePath);
|
||||
if (validation.valid) {
|
||||
return {
|
||||
found: true,
|
||||
path: this.userConfig.claudePath,
|
||||
version: validation.version,
|
||||
source: 'user-config',
|
||||
message: `Using user-configured Claude CLI: ${this.userConfig.claudePath}`,
|
||||
};
|
||||
}
|
||||
console.warn(
|
||||
`[Claude CLI] User-configured path invalid: ${validation.message}`
|
||||
);
|
||||
}
|
||||
console.warn(
|
||||
`[Claude CLI] User-configured path invalid: ${validation.message}`
|
||||
);
|
||||
}
|
||||
|
||||
// 2. Homebrew (macOS)
|
||||
@@ -563,13 +630,17 @@ class CLIToolManager {
|
||||
const MINIMUM_VERSION = '3.10.0';
|
||||
|
||||
try {
|
||||
const version = execSync(`${pythonCmd} --version`, {
|
||||
stdio: 'pipe',
|
||||
// Parse command to handle cases like 'py -3' on Windows
|
||||
// This avoids command injection by using execFileSync instead of execSync
|
||||
const parts = pythonCmd.split(' ');
|
||||
const cmd = parts[0];
|
||||
const args = [...parts.slice(1), '--version'];
|
||||
|
||||
const version = execFileSync(cmd, args, {
|
||||
encoding: 'utf-8',
|
||||
timeout: 5000,
|
||||
windowsHide: true,
|
||||
})
|
||||
.toString()
|
||||
.trim();
|
||||
}).trim();
|
||||
|
||||
const match = version.match(/Python (\d+\.\d+\.\d+)/);
|
||||
if (!match) {
|
||||
@@ -860,3 +931,23 @@ export function getToolInfo(tool: CLITool): ToolDetectionResult {
|
||||
export function clearToolCache(): void {
|
||||
cliToolManager.clearCache();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a path appears to be from a different platform.
|
||||
* Useful for detecting cross-platform path issues in settings.
|
||||
*
|
||||
* @param pathStr - The path to check
|
||||
* @returns true if the path is from a different platform
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { isPathFromWrongPlatform } from './cli-tool-manager';
|
||||
*
|
||||
* // On macOS, this returns true for Windows paths
|
||||
* isPathFromWrongPlatform('C:\\Program Files\\claude.exe'); // true
|
||||
* isPathFromWrongPlatform('/usr/local/bin/claude'); // false
|
||||
* ```
|
||||
*/
|
||||
export function isPathFromWrongPlatform(pathStr: string | undefined): boolean {
|
||||
return isWrongPlatformPath(pathStr);
|
||||
}
|
||||
|
||||
@@ -51,7 +51,12 @@ export function registerFileHandlers(): void {
|
||||
IPC_CHANNELS.FILE_EXPLORER_LIST,
|
||||
async (_, dirPath: string): Promise<IPCResult<FileNode[]>> => {
|
||||
try {
|
||||
const entries = readdirSync(dirPath, { withFileTypes: true });
|
||||
// Validate and normalize path to prevent directory traversal
|
||||
const validation = validatePath(dirPath);
|
||||
if (!validation.valid) {
|
||||
return { success: false, error: validation.error };
|
||||
}
|
||||
const entries = readdirSync(validation.path, { withFileTypes: true });
|
||||
|
||||
// Filter and map entries
|
||||
const nodes: FileNode[] = [];
|
||||
@@ -65,7 +70,7 @@ export function registerFileHandlers(): void {
|
||||
if (entry.isDirectory() && IGNORED_DIRS.has(entry.name)) continue;
|
||||
|
||||
nodes.push({
|
||||
path: path.join(dirPath, entry.name),
|
||||
path: path.join(validation.path, entry.name),
|
||||
name: entry.name,
|
||||
isDirectory: entry.isDirectory()
|
||||
});
|
||||
|
||||
@@ -13,7 +13,7 @@ import type { BrowserWindow } from 'electron';
|
||||
import { getEffectiveVersion } from '../auto-claude-updater';
|
||||
import { setUpdateChannel } from '../app-updater';
|
||||
import { getSettingsPath, readSettingsFile } from '../settings-utils';
|
||||
import { configureTools, getToolPath, getToolInfo } from '../cli-tool-manager';
|
||||
import { configureTools, getToolPath, getToolInfo, isPathFromWrongPlatform } from '../cli-tool-manager';
|
||||
|
||||
const settingsPath = getSettingsPath();
|
||||
|
||||
@@ -124,6 +124,22 @@ export function registerSettingsHandlers(
|
||||
needsSave = true;
|
||||
}
|
||||
|
||||
// Migration: Clear CLI tool paths that are from a different platform
|
||||
// Fixes issue where Windows paths persisted on macOS (and vice versa)
|
||||
// when settings were synced/transferred between platforms
|
||||
// See: https://github.com/AndyMik90/Auto-Claude/issues/XXX
|
||||
const pathFields = ['pythonPath', 'gitPath', 'githubCLIPath', 'claudePath', 'autoBuildPath'] as const;
|
||||
for (const field of pathFields) {
|
||||
const pathValue = settings[field];
|
||||
if (pathValue && isPathFromWrongPlatform(pathValue)) {
|
||||
console.warn(
|
||||
`[SETTINGS_GET] Clearing ${field} - path from different platform: ${pathValue}`
|
||||
);
|
||||
delete settings[field];
|
||||
needsSave = true;
|
||||
}
|
||||
}
|
||||
|
||||
// If no manual autoBuildPath is set, try to auto-detect
|
||||
if (!settings.autoBuildPath) {
|
||||
const detectedPath = detectAutoBuildSourcePath();
|
||||
@@ -369,7 +385,22 @@ export function registerSettingsHandlers(
|
||||
ipcMain.handle(
|
||||
IPC_CHANNELS.SHELL_OPEN_EXTERNAL,
|
||||
async (_, url: string): Promise<void> => {
|
||||
await shell.openExternal(url);
|
||||
// Validate URL scheme to prevent opening dangerous protocols
|
||||
try {
|
||||
const parsedUrl = new URL(url);
|
||||
if (!['http:', 'https:'].includes(parsedUrl.protocol)) {
|
||||
console.warn(`[SHELL_OPEN_EXTERNAL] Blocked URL with unsafe protocol: ${parsedUrl.protocol}`);
|
||||
throw new Error(`Unsafe URL protocol: ${parsedUrl.protocol}`);
|
||||
}
|
||||
await shell.openExternal(url);
|
||||
} catch (error) {
|
||||
if (error instanceof TypeError) {
|
||||
// Invalid URL format
|
||||
console.warn(`[SHELL_OPEN_EXTERNAL] Invalid URL format: ${url}`);
|
||||
throw new Error('Invalid URL format');
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -427,17 +458,21 @@ export function registerSettingsHandlers(
|
||||
});
|
||||
} else {
|
||||
// Linux: Try common terminal emulators with argument arrays
|
||||
const terminals: Array<{ cmd: string; args: string[] }> = [
|
||||
// Note: xterm uses cwd option to avoid shell injection vulnerabilities
|
||||
const terminals: Array<{ cmd: string; args: string[]; useCwd?: boolean }> = [
|
||||
{ cmd: 'gnome-terminal', args: ['--working-directory', resolvedPath] },
|
||||
{ cmd: 'konsole', args: ['--workdir', resolvedPath] },
|
||||
{ cmd: 'xfce4-terminal', args: ['--working-directory', resolvedPath] },
|
||||
{ cmd: 'xterm', args: ['-e', 'bash', '-c', `cd '${resolvedPath.replace(/'/g, "'\\''")}' && exec bash`] }
|
||||
{ cmd: 'xterm', args: ['-e', 'bash'], useCwd: true }
|
||||
];
|
||||
|
||||
let opened = false;
|
||||
for (const { cmd, args } of terminals) {
|
||||
for (const { cmd, args, useCwd } of terminals) {
|
||||
try {
|
||||
execFileSync(cmd, args, { stdio: 'ignore' });
|
||||
execFileSync(cmd, args, {
|
||||
stdio: 'ignore',
|
||||
...(useCwd ? { cwd: resolvedPath } : {})
|
||||
});
|
||||
opened = true;
|
||||
break;
|
||||
} catch {
|
||||
|
||||
@@ -63,8 +63,8 @@ function taskCardPropsAreEqual(prevProps: TaskCardProps, nextProps: TaskCardProp
|
||||
prevTask.metadata?.category === nextTask.metadata?.category &&
|
||||
prevTask.metadata?.complexity === nextTask.metadata?.complexity &&
|
||||
prevTask.metadata?.archivedAt === nextTask.metadata?.archivedAt &&
|
||||
// Check if subtask statuses changed (quick check on first few)
|
||||
prevTask.subtasks.slice(0, 5).every((s, i) => s.status === nextTask.subtasks[i]?.status)
|
||||
// Check if any subtask statuses changed (compare all subtasks)
|
||||
prevTask.subtasks.every((s, i) => s.status === nextTask.subtasks[i]?.status)
|
||||
);
|
||||
|
||||
// Only log when actually re-rendering (reduces noise significantly)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { unstable_batchedUpdates } from 'react-dom';
|
||||
import { useTaskStore } from '../stores/task-store';
|
||||
import { useRoadmapStore } from '../stores/roadmap-store';
|
||||
|
||||
Reference in New Issue
Block a user