- Updated `dev:kill` script to use a dedicated Node.js script for improved process termination. - Enhanced `TeamProvisioningService` to trigger team refresh events for live lead replies, improving message handling. - Refactored message deduplication logic in `handleGetData` to prevent duplicate messages from lead sessions and lead processes. - Introduced `validateOpenPathUserSelected` function to allow user-selected paths while enforcing security checks. - Improved UI components in `TeamListView` and `ActivityItem` for better user experience and accessibility. - Added progress bar for task completion in `DashboardView`, enhancing task tracking visibility.
263 lines
7.7 KiB
TypeScript
263 lines
7.7 KiB
TypeScript
/**
|
|
* IPC Handlers for Utility Operations.
|
|
*
|
|
* Handlers:
|
|
* - shell:openPath: Opens a folder or file in the system's default application
|
|
* - read-claude-md-files: Reads all global CLAUDE.md files for a project
|
|
* - read-directory-claude-md: Reads a specific directory's CLAUDE.md file
|
|
* - read-mentioned-file: Validates mentioned files for context injection
|
|
*/
|
|
|
|
import { createLogger } from '@shared/utils/logger';
|
|
import { app, type IpcMain, type IpcMainInvokeEvent, shell } from 'electron';
|
|
import * as fs from 'fs';
|
|
|
|
import {
|
|
type ClaudeMdFileInfo,
|
|
readAgentConfigs,
|
|
readAllClaudeMdFiles,
|
|
readDirectoryClaudeMd,
|
|
} from '../services';
|
|
|
|
import type { AgentConfig } from '@shared/types/api';
|
|
|
|
const logger = createLogger('IPC:utility');
|
|
import {
|
|
validateFilePath,
|
|
validateOpenPath,
|
|
validateOpenPathUserSelected,
|
|
} from '../utils/pathValidation';
|
|
import { countTokens } from '../utils/tokenizer';
|
|
|
|
/**
|
|
* Registers all utility-related IPC handlers.
|
|
*/
|
|
export function registerUtilityHandlers(ipcMain: IpcMain): void {
|
|
ipcMain.handle('get-app-version', handleGetAppVersion);
|
|
ipcMain.handle('shell:openPath', handleShellOpenPath);
|
|
ipcMain.handle('shell:openExternal', handleShellOpenExternal);
|
|
ipcMain.handle('read-claude-md-files', handleReadClaudeMdFiles);
|
|
ipcMain.handle('read-directory-claude-md', handleReadDirectoryClaudeMd);
|
|
ipcMain.handle('read-mentioned-file', handleReadMentionedFile);
|
|
ipcMain.handle('read-agent-configs', handleReadAgentConfigs);
|
|
|
|
logger.info('Utility handlers registered');
|
|
}
|
|
|
|
/**
|
|
* Removes all utility IPC handlers.
|
|
*/
|
|
export function removeUtilityHandlers(ipcMain: IpcMain): void {
|
|
ipcMain.removeHandler('get-app-version');
|
|
ipcMain.removeHandler('shell:openPath');
|
|
ipcMain.removeHandler('shell:openExternal');
|
|
ipcMain.removeHandler('read-claude-md-files');
|
|
ipcMain.removeHandler('read-directory-claude-md');
|
|
ipcMain.removeHandler('read-mentioned-file');
|
|
ipcMain.removeHandler('read-agent-configs');
|
|
|
|
logger.info('Utility handlers removed');
|
|
}
|
|
|
|
// =============================================================================
|
|
// Handler Implementations
|
|
// =============================================================================
|
|
|
|
/**
|
|
* Handler for 'get-app-version' IPC call.
|
|
* Returns the app version from package.json.
|
|
*/
|
|
function handleGetAppVersion(): string {
|
|
return app.getVersion();
|
|
}
|
|
|
|
/**
|
|
* Handler for 'shell:openExternal' IPC call.
|
|
* Opens a URL in the system's default browser.
|
|
*/
|
|
async function handleShellOpenExternal(
|
|
_event: IpcMainInvokeEvent,
|
|
url: string
|
|
): Promise<{ success: boolean; error?: string }> {
|
|
try {
|
|
let parsedUrl: URL;
|
|
try {
|
|
parsedUrl = new URL(url);
|
|
} catch {
|
|
return { success: false, error: 'Invalid URL' };
|
|
}
|
|
|
|
const protocol = parsedUrl.protocol.toLowerCase();
|
|
if (protocol !== 'http:' && protocol !== 'https:' && protocol !== 'mailto:') {
|
|
logger.error(`shell:openExternal - invalid URL scheme: ${url}`);
|
|
return { success: false, error: 'Only http, https, and mailto URLs are allowed' };
|
|
}
|
|
|
|
await shell.openExternal(parsedUrl.toString());
|
|
return { success: true };
|
|
} catch (error) {
|
|
logger.error('Error in shell:openExternal:', error);
|
|
return { success: false, error: String(error) };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handler for 'shell:openPath' IPC call.
|
|
* Opens a folder or file in the system's default application (Finder on macOS).
|
|
* Validates path security before opening.
|
|
* When userSelectedFromDialog is true, path was chosen via system folder picker —
|
|
* only sensitive-pattern checks apply, not project/claude directory restriction.
|
|
*/
|
|
async function handleShellOpenPath(
|
|
_event: IpcMainInvokeEvent,
|
|
targetPath: string,
|
|
projectRoot?: string,
|
|
userSelectedFromDialog?: boolean
|
|
): Promise<{ success: boolean; error?: string }> {
|
|
try {
|
|
const validation = userSelectedFromDialog
|
|
? validateOpenPathUserSelected(targetPath)
|
|
: validateOpenPath(targetPath, projectRoot ?? null);
|
|
if (!validation.valid) {
|
|
logger.error(`shell:openPath - validation failed: ${validation.error ?? 'Unknown error'}`);
|
|
return { success: false, error: validation.error };
|
|
}
|
|
|
|
const safePath = validation.normalizedPath!;
|
|
|
|
// Check if path exists
|
|
if (!fs.existsSync(safePath)) {
|
|
logger.error(`shell:openPath - path does not exist: ${safePath}`);
|
|
return { success: false, error: 'Path does not exist' };
|
|
}
|
|
|
|
// Open in default application (Finder on macOS)
|
|
const errorMessage = await shell.openPath(safePath);
|
|
if (errorMessage) {
|
|
logger.error(`shell:openPath - failed: ${errorMessage}`);
|
|
return { success: false, error: errorMessage };
|
|
}
|
|
|
|
return { success: true };
|
|
} catch (error) {
|
|
logger.error('Error in shell:openPath:', error);
|
|
return { success: false, error: String(error) };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handler for 'read-claude-md-files' IPC call.
|
|
* Reads all global CLAUDE.md files for a project.
|
|
*/
|
|
async function handleReadClaudeMdFiles(
|
|
_event: IpcMainInvokeEvent,
|
|
projectRoot: string
|
|
): Promise<Record<string, ClaudeMdFileInfo>> {
|
|
try {
|
|
const result = await readAllClaudeMdFiles(projectRoot);
|
|
// Convert Map to object for IPC serialization
|
|
const files: Record<string, ClaudeMdFileInfo> = {};
|
|
result.files.forEach((info, key) => {
|
|
files[key] = info;
|
|
});
|
|
|
|
return files;
|
|
} catch (error) {
|
|
logger.error(`Error in read-claude-md-files:`, error);
|
|
return {};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handler for 'read-directory-claude-md' IPC call.
|
|
* Reads a specific directory's CLAUDE.md file.
|
|
*/
|
|
async function handleReadDirectoryClaudeMd(
|
|
_event: IpcMainInvokeEvent,
|
|
dirPath: string
|
|
): Promise<ClaudeMdFileInfo> {
|
|
try {
|
|
const info = await readDirectoryClaudeMd(dirPath);
|
|
return info;
|
|
} catch (error) {
|
|
logger.error(`Error in read-directory-claude-md:`, error);
|
|
return {
|
|
path: dirPath,
|
|
exists: false,
|
|
charCount: 0,
|
|
estimatedTokens: 0,
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handler for 'read-mentioned-file' IPC call.
|
|
* Validates mentioned files for context injection.
|
|
* Returns file info if file exists, is a regular file, within allowed directories, and within token limits.
|
|
*
|
|
* Security: Validates path against allowed directories and sensitive file patterns.
|
|
*/
|
|
async function handleReadMentionedFile(
|
|
_event: IpcMainInvokeEvent,
|
|
absolutePath: string,
|
|
projectRoot: string,
|
|
maxTokens: number = 25000
|
|
): Promise<{ path: string; exists: boolean; charCount: number; estimatedTokens: number } | null> {
|
|
try {
|
|
// Validate path security
|
|
const validation = validateFilePath(absolutePath, projectRoot || null);
|
|
if (!validation.valid) {
|
|
return null;
|
|
}
|
|
|
|
const safePath = validation.normalizedPath!;
|
|
|
|
// Check if file exists
|
|
if (!fs.existsSync(safePath)) {
|
|
return null;
|
|
}
|
|
|
|
// Check if it's a file (not directory)
|
|
const stats = fs.statSync(safePath);
|
|
if (!stats.isFile()) {
|
|
return null;
|
|
}
|
|
|
|
// Read file content
|
|
const content = fs.readFileSync(safePath, 'utf8');
|
|
|
|
// Calculate tokens
|
|
const estimatedTokens = countTokens(content);
|
|
|
|
// Check token limit
|
|
if (estimatedTokens > maxTokens) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
path: safePath,
|
|
exists: true,
|
|
charCount: content.length,
|
|
estimatedTokens,
|
|
};
|
|
} catch (error) {
|
|
logger.error(`Error in read-mentioned-file for ${absolutePath}:`, error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handler for 'read-agent-configs' IPC call.
|
|
* Reads agent definitions from project's .claude/agents/ directory.
|
|
*/
|
|
async function handleReadAgentConfigs(
|
|
_event: IpcMainInvokeEvent,
|
|
projectRoot: string
|
|
): Promise<Record<string, AgentConfig>> {
|
|
try {
|
|
return await readAgentConfigs(projectRoot);
|
|
} catch (error) {
|
|
logger.error('Error in read-agent-configs:', error);
|
|
return {};
|
|
}
|
|
}
|