Files
Aperant/apps/frontend/src/preload/api/project-api.ts
T
Andy 72106109a2 Fix/windows issues (#471)
* fix(windows): Claude CLI detection failing on Windows

On Windows, npm installs CLI tools as .cmd batch wrappers alongside
bash scripts for Git Bash/Cygwin. Two issues prevented detection:

1. findExecutable() checked for extensionless files first, finding
   the unusable bash script before the .cmd wrapper

2. execFileSync() cannot execute .cmd files without shell: true

Changes:
- Reorder extension search to prioritize .exe/.cmd over extensionless
- Add shell: true when validating .cmd/.bat files on Windows

Both changes are Windows-specific and don't affect macOS behavior.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(windows): terminal shortcuts and invoke Claude not working

- Fix Ctrl+T/W shortcuts not working inside terminals on Windows
  xterm.js was capturing Ctrl+T as ^T character instead of letting it
  bubble up to window handler. Added T and W to custom key handler
  bypass list in useXterm.ts

- Fix "Invoke Claude" button failing on Windows with path error
  buildCdCommand() was using single quotes which cmd.exe doesn't
  recognize. Now uses platform-appropriate quoting (double quotes
  on Windows, single quotes on Unix)

- Improve terminal auto-naming to skip common commands
  Expanded skip list to include claude, git, npm, node, python,
  and other shell/dev commands that don't represent meaningful work.
  Terminal naming should come from actual task descriptions.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(windows): reduce installer size by 75% (~300MB savings)

Strip unnecessary files from Python site-packages during bundling:
- Remove googleapiclient/discovery_cache/documents (92MB cached API docs)
- Remove claude_agent_sdk/_bundled (224MB bundled Claude CLI)
- Remove pythonwin directory (9MB Windows IDE)
- Remove .chm help files (2.6MB)

The Claude Agent SDK will fall back to the system-installed Claude Code
CLI, which is already a prerequisite for Auto-Claude (required for
'claude setup-token').

Site-packages reduced from 446MB to 111MB (75% reduction).
Expected installer size: ~100MB instead of ~206MB.

* fix(frontend): sync project tabs with settings by removing project on tab close

Closing a project tab now removes the project from the app entirely,
keeping tabs and settings dropdown in sync. Files remain on disk so
users can re-add projects later.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor(settings): remove redundant Claude Auth project settings tab

Claude authentication is already available in the app-level Integrations
section, making this project-level tab redundant.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat(onboarding): add one-click Ollama installation for Windows/macOS/Linux

When users select Ollama as their embedding provider during onboarding
but don't have it installed, they now see an "Install Ollama" button
that opens their preferred terminal with the official install command.

Changes:
- Add checkOllamaInstalled() to detect if Ollama binary exists on system
- Add installOllama() to open terminal with platform-specific install:
  - Windows: winget install --id Ollama.Ollama
  - macOS/Linux: curl -fsSL https://ollama.com/install.sh | sh
- Update OllamaModelSelector to show install UI when Ollama not found
- Fix Windows terminal handling for commands with pipes (PowerShell)
- Use fire-and-forget pattern so UI doesn't hang waiting for terminal
- Add i18n translations (English & French) for install UI

The install uses the user's preferred terminal from the DevTools
onboarding step, respecting their configuration.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat(ipc-handlers): enhance task status persistence in implementation plan

- Added functionality to persist task status updates to the implementation plan file, preventing inconsistencies between UI and file state during task refresh.
- Implemented error handling for file read/write operations to ensure robustness in status updates.
- Updated task execution handlers to reflect changes in task status, including transitions to 'human_review' and 'backlog'.
- Introduced critical comments to clarify the importance of status persistence in maintaining accurate task states.

This update improves the reliability of task status management across the application.

* fix(security): address PR review findings for Windows issues

- Fix command injection vulnerability in buildCdCommand by using
  escapeShellArgWindows() to properly escape cmd.exe metacharacters
- Add confirmation dialog before removing project on tab close
- Add documentation explaining why skipCommands list exists
- Add security comment for shell: true usage in Claude CLI validation
- Remove unused EnvironmentSettings component (dead code cleanup)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(security): address follow-up PR review findings

- Fix command injection in Windows terminal by escaping PowerShell
  metacharacters (backticks, double quotes, dollar signs)
- Fix Git Bash to use passed command parameter instead of hardcoded value
- Add shared plan-file-utils with mutex locking for thread-safe updates
- Refactor agent-events-handlers and execution-handlers to use shared
  persistence utility, eliminating code duplication

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(security): strengthen shell escaping and document sync limitations

- Add escaping for parentheses, semicolons, ampersands in PowerShell
- Add escaping for semicolons, pipes, exclamation marks in Git Bash
- Add comprehensive documentation warning about persistPlanStatusSync
  bypassing the async locking mechanism

Note: The CRITICAL finding about EnvironmentSettings in SectionRouter.tsx
is a false positive - grep confirms no such import exists in the file.
Line 5 imports SecuritySettings, not EnvironmentSettings.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: address code scanning and TypeScript errors

- Fix TypeScript error in plan-file-utils.ts (generic type assertion)
- Add security comment for ollamaPath explaining hardcoded paths
- Remove useless isLoading conditional in OllamaModelSelector.tsx

Note: The EnvironmentSettings import finding remains a false positive -
grep confirms no such import exists in SectionRouter.tsx.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(ui): restore Retry button disabled state for defensive programming

Restored disabled={isLoading} and animate-spin on Retry buttons.

FALSE POSITIVES in review (verified via grep/sed):
- CRITICAL "EnvironmentSettings import at line 5": Line 5 is actually
  `import { SecuritySettings }` - no EnvironmentSettings import exists
- LOW "Dead code escape functions": Functions ARE used at lines 268, 275
  in openTerminalWithCommand for PowerShell and Git Bash escaping

The review tool appears to be using cached/stale file data.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(security): address CodeQL security alerts

- Fix 6 HIGH TOCTOU race conditions by removing existsSync checks
  and using try/catch with ENOENT detection instead
- Fix MEDIUM command injection by using execFileSync instead of
  execSync for Ollama path detection (avoids shell interpretation)
- Fix unused imports in execution-handlers.ts
- Remove useless isLoading conditional and add explanatory comment
  about React batching behavior preventing double-clicks

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(build): remove TOCTOU race condition in download-python script

Replace existsSync check with try/catch pattern to avoid race condition
between existence check and file operations. Handle ENOENT as no-op.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: address PR review issues for error handling and i18n

- Add error handling with toast notification for project removal in App.tsx
- Replace hardcoded strings with i18n translation keys in ProjectSettingsContent.tsx
- Add translation keys for projectSettings.noProjectSelected (en/fr)
- Add removeProject.error translation key to dialogs.json (en/fr)
- Add errors.unknownError translation key to common.json (en/fr)
- Refactor terminal command escaping in claude-code-handlers.ts
- Remove unused imports in execution-handlers.ts
- Add explanatory comment for React batching in OllamaModelSelector.tsx

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(app): replace toast with inline error display in remove project dialog

Toast function doesn't exist in this codebase. Use inline error display
with AlertCircle icon to show removal errors in the dialog itself.

🤖 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>
2026-01-01 12:53:27 +01:00

299 lines
11 KiB
TypeScript

import { ipcRenderer } from 'electron';
import { IPC_CHANNELS } from '../../shared/constants';
import type {
Project,
ProjectSettings,
IPCResult,
InitializationResult,
AutoBuildVersionInfo,
ProjectEnvConfig,
ClaudeAuthResult,
InfrastructureStatus,
GraphitiValidationResult,
GraphitiConnectionTestResult,
GitStatus
} from '../../shared/types';
// Tab state interface (persisted in main process)
export interface TabState {
openProjectIds: string[];
activeProjectId: string | null;
tabOrder: string[];
}
export interface ProjectAPI {
// Project Management
addProject: (projectPath: string) => Promise<IPCResult<Project>>;
removeProject: (projectId: string) => Promise<IPCResult>;
getProjects: () => Promise<IPCResult<Project[]>>;
updateProjectSettings: (
projectId: string,
settings: Partial<ProjectSettings>
) => Promise<IPCResult>;
initializeProject: (projectId: string) => Promise<IPCResult<InitializationResult>>;
checkProjectVersion: (projectId: string) => Promise<IPCResult<AutoBuildVersionInfo>>;
// Tab State (persisted in main process for reliability)
getTabState: () => Promise<IPCResult<TabState>>;
saveTabState: (tabState: TabState) => Promise<IPCResult>;
// Context Operations
getProjectContext: (projectId: string) => Promise<IPCResult<unknown>>;
refreshProjectIndex: (projectId: string) => Promise<IPCResult<unknown>>;
getMemoryStatus: (projectId: string) => Promise<IPCResult<unknown>>;
searchMemories: (projectId: string, query: string) => Promise<IPCResult<unknown>>;
getRecentMemories: (projectId: string, limit?: number) => Promise<IPCResult<unknown>>;
// Environment Configuration
getProjectEnv: (projectId: string) => Promise<IPCResult<ProjectEnvConfig>>;
updateProjectEnv: (projectId: string, config: Partial<ProjectEnvConfig>) => Promise<IPCResult>;
checkClaudeAuth: (projectId: string) => Promise<IPCResult<ClaudeAuthResult>>;
invokeClaudeSetup: (projectId: string) => Promise<IPCResult<ClaudeAuthResult>>;
// Dialog Operations
selectDirectory: () => Promise<string | null>;
createProjectFolder: (
location: string,
name: string,
initGit: boolean
) => Promise<IPCResult<import('../../shared/types').CreateProjectFolderResult>>;
getDefaultProjectLocation: () => Promise<string | null>;
// Memory Infrastructure Operations (LadybugDB - no Docker required)
getMemoryInfrastructureStatus: (dbPath?: string) => Promise<IPCResult<InfrastructureStatus>>;
listMemoryDatabases: (dbPath?: string) => Promise<IPCResult<string[]>>;
testMemoryConnection: (dbPath?: string, database?: string) => Promise<IPCResult<GraphitiValidationResult>>;
// Graphiti Validation Operations
validateLLMApiKey: (provider: string, apiKey: string) => Promise<IPCResult<GraphitiValidationResult>>;
testGraphitiConnection: (config: {
dbPath?: string;
database?: string;
llmProvider: string;
apiKey: string;
}) => Promise<IPCResult<GraphitiConnectionTestResult>>;
// Ollama Model Management
scanOllamaModels: (baseUrl: string) => Promise<IPCResult<{
models: Array<{
name: string;
size: number;
modified_at: string;
digest: string;
}>;
}>>;
downloadOllamaModel: (baseUrl: string, modelName: string) => Promise<IPCResult<{ message: string }>>;
onDownloadProgress: (callback: (data: {
modelName: string;
status: string;
completed: number;
total: number;
percentage: number;
}) => void) => () => void;
// Git Operations
getGitBranches: (projectPath: string) => Promise<IPCResult<string[]>>;
getCurrentGitBranch: (projectPath: string) => Promise<IPCResult<string | null>>;
detectMainBranch: (projectPath: string) => Promise<IPCResult<string | null>>;
checkGitStatus: (projectPath: string) => Promise<IPCResult<GitStatus>>;
initializeGit: (projectPath: string) => Promise<IPCResult<InitializationResult>>;
// Ollama Model Detection
checkOllamaStatus: (baseUrl?: string) => Promise<IPCResult<{
running: boolean;
url: string;
version?: string;
message?: string;
}>>;
checkOllamaInstalled: () => Promise<IPCResult<{
installed: boolean;
path?: string;
version?: string;
}>>;
installOllama: () => Promise<IPCResult<{ command: string }>>;
listOllamaModels: (baseUrl?: string) => Promise<IPCResult<{
models: Array<{
name: string;
size_bytes: number;
size_gb: number;
modified_at: string;
is_embedding: boolean;
embedding_dim?: number | null;
description?: string;
}>;
count: number;
}>>;
listOllamaEmbeddingModels: (baseUrl?: string) => Promise<IPCResult<{
embedding_models: Array<{
name: string;
embedding_dim: number | null;
description: string;
size_bytes: number;
size_gb: number;
}>;
count: number;
}>>;
pullOllamaModel: (modelName: string, baseUrl?: string) => Promise<IPCResult<{
model: string;
status: 'completed' | 'failed';
output: string[];
}>>;
}
export const createProjectAPI = (): ProjectAPI => ({
// Project Management
addProject: (projectPath: string): Promise<IPCResult<Project>> =>
ipcRenderer.invoke(IPC_CHANNELS.PROJECT_ADD, projectPath),
removeProject: (projectId: string): Promise<IPCResult> =>
ipcRenderer.invoke(IPC_CHANNELS.PROJECT_REMOVE, projectId),
getProjects: (): Promise<IPCResult<Project[]>> =>
ipcRenderer.invoke(IPC_CHANNELS.PROJECT_LIST),
updateProjectSettings: (
projectId: string,
settings: Partial<ProjectSettings>
): Promise<IPCResult> =>
ipcRenderer.invoke(IPC_CHANNELS.PROJECT_UPDATE_SETTINGS, projectId, settings),
initializeProject: (projectId: string): Promise<IPCResult<InitializationResult>> =>
ipcRenderer.invoke(IPC_CHANNELS.PROJECT_INITIALIZE, projectId),
checkProjectVersion: (projectId: string): Promise<IPCResult<AutoBuildVersionInfo>> =>
ipcRenderer.invoke(IPC_CHANNELS.PROJECT_CHECK_VERSION, projectId),
// Tab State (persisted in main process for reliability)
getTabState: (): Promise<IPCResult<TabState>> =>
ipcRenderer.invoke(IPC_CHANNELS.TAB_STATE_GET),
saveTabState: (tabState: TabState): Promise<IPCResult> =>
ipcRenderer.invoke(IPC_CHANNELS.TAB_STATE_SAVE, tabState),
// Context Operations
getProjectContext: (projectId: string) =>
ipcRenderer.invoke(IPC_CHANNELS.CONTEXT_GET, projectId),
refreshProjectIndex: (projectId: string) =>
ipcRenderer.invoke(IPC_CHANNELS.CONTEXT_REFRESH_INDEX, projectId),
getMemoryStatus: (projectId: string) =>
ipcRenderer.invoke(IPC_CHANNELS.CONTEXT_MEMORY_STATUS, projectId),
searchMemories: (projectId: string, query: string) =>
ipcRenderer.invoke(IPC_CHANNELS.CONTEXT_SEARCH_MEMORIES, projectId, query),
getRecentMemories: (projectId: string, limit?: number) =>
ipcRenderer.invoke(IPC_CHANNELS.CONTEXT_GET_MEMORIES, projectId, limit),
// Environment Configuration
getProjectEnv: (projectId: string): Promise<IPCResult<ProjectEnvConfig>> =>
ipcRenderer.invoke(IPC_CHANNELS.ENV_GET, projectId),
updateProjectEnv: (projectId: string, config: Partial<ProjectEnvConfig>): Promise<IPCResult> =>
ipcRenderer.invoke(IPC_CHANNELS.ENV_UPDATE, projectId, config),
checkClaudeAuth: (projectId: string): Promise<IPCResult<ClaudeAuthResult>> =>
ipcRenderer.invoke(IPC_CHANNELS.ENV_CHECK_CLAUDE_AUTH, projectId),
invokeClaudeSetup: (projectId: string): Promise<IPCResult<ClaudeAuthResult>> =>
ipcRenderer.invoke(IPC_CHANNELS.ENV_INVOKE_CLAUDE_SETUP, projectId),
// Dialog Operations
selectDirectory: (): Promise<string | null> =>
ipcRenderer.invoke(IPC_CHANNELS.DIALOG_SELECT_DIRECTORY),
createProjectFolder: (
location: string,
name: string,
initGit: boolean
): Promise<IPCResult<import('../../shared/types').CreateProjectFolderResult>> =>
ipcRenderer.invoke(IPC_CHANNELS.DIALOG_CREATE_PROJECT_FOLDER, location, name, initGit),
getDefaultProjectLocation: (): Promise<string | null> =>
ipcRenderer.invoke(IPC_CHANNELS.DIALOG_GET_DEFAULT_PROJECT_LOCATION),
// Memory Infrastructure Operations (LadybugDB - no Docker required)
getMemoryInfrastructureStatus: (dbPath?: string): Promise<IPCResult<InfrastructureStatus>> =>
ipcRenderer.invoke(IPC_CHANNELS.MEMORY_STATUS, dbPath),
listMemoryDatabases: (dbPath?: string): Promise<IPCResult<string[]>> =>
ipcRenderer.invoke(IPC_CHANNELS.MEMORY_LIST_DATABASES, dbPath),
testMemoryConnection: (dbPath?: string, database?: string): Promise<IPCResult<GraphitiValidationResult>> =>
ipcRenderer.invoke(IPC_CHANNELS.MEMORY_TEST_CONNECTION, dbPath, database),
// Graphiti Validation Operations
validateLLMApiKey: (provider: string, apiKey: string): Promise<IPCResult<GraphitiValidationResult>> =>
ipcRenderer.invoke(IPC_CHANNELS.GRAPHITI_VALIDATE_LLM, provider, apiKey),
testGraphitiConnection: (config: {
dbPath?: string;
database?: string;
llmProvider: string;
apiKey: string;
}): Promise<IPCResult<GraphitiConnectionTestResult>> =>
ipcRenderer.invoke(IPC_CHANNELS.GRAPHITI_TEST_CONNECTION, config),
// Ollama Model Management
scanOllamaModels: (baseUrl: string): Promise<IPCResult<{
models: Array<{
name: string;
size: number;
modified_at: string;
digest: string;
}>;
}>> =>
ipcRenderer.invoke('scan-ollama-models', baseUrl),
downloadOllamaModel: (baseUrl: string, modelName: string): Promise<IPCResult<{ message: string }>> =>
ipcRenderer.invoke('download-ollama-model', baseUrl, modelName),
onDownloadProgress: (callback: (data: {
modelName: string;
status: string;
completed: number;
total: number;
percentage: number;
}) => void) => {
const listener = (_: any, data: any) => callback(data);
ipcRenderer.on(IPC_CHANNELS.OLLAMA_PULL_PROGRESS, listener);
return () => ipcRenderer.off(IPC_CHANNELS.OLLAMA_PULL_PROGRESS, listener);
},
// Git Operations
getGitBranches: (projectPath: string): Promise<IPCResult<string[]>> =>
ipcRenderer.invoke(IPC_CHANNELS.GIT_GET_BRANCHES, projectPath),
getCurrentGitBranch: (projectPath: string): Promise<IPCResult<string | null>> =>
ipcRenderer.invoke(IPC_CHANNELS.GIT_GET_CURRENT_BRANCH, projectPath),
detectMainBranch: (projectPath: string): Promise<IPCResult<string | null>> =>
ipcRenderer.invoke(IPC_CHANNELS.GIT_DETECT_MAIN_BRANCH, projectPath),
checkGitStatus: (projectPath: string): Promise<IPCResult<GitStatus>> =>
ipcRenderer.invoke(IPC_CHANNELS.GIT_CHECK_STATUS, projectPath),
initializeGit: (projectPath: string): Promise<IPCResult<InitializationResult>> =>
ipcRenderer.invoke(IPC_CHANNELS.GIT_INITIALIZE, projectPath),
// Ollama Model Detection
checkOllamaStatus: (baseUrl?: string) =>
ipcRenderer.invoke(IPC_CHANNELS.OLLAMA_CHECK_STATUS, baseUrl),
checkOllamaInstalled: () =>
ipcRenderer.invoke(IPC_CHANNELS.OLLAMA_CHECK_INSTALLED),
installOllama: () =>
ipcRenderer.invoke(IPC_CHANNELS.OLLAMA_INSTALL),
listOllamaModels: (baseUrl?: string) =>
ipcRenderer.invoke(IPC_CHANNELS.OLLAMA_LIST_MODELS, baseUrl),
listOllamaEmbeddingModels: (baseUrl?: string) =>
ipcRenderer.invoke(IPC_CHANNELS.OLLAMA_LIST_EMBEDDING_MODELS, baseUrl),
pullOllamaModel: (modelName: string, baseUrl?: string) =>
ipcRenderer.invoke(IPC_CHANNELS.OLLAMA_PULL_MODEL, modelName, baseUrl)
});