352ecbc506
* ✨ feat: add claude-code-templates via git submodule with build-time copy - Add git submodule for davila7/claude-code-templates - Create scripts/copy-templates.js to copy components at build time - Update package.json build script to include template copying - Add resources/data/components/ to .gitignore (generated files) Templates are now automatically synced from external repo to resources/data/components/ during build process, avoiding manual file copying. To update templates: git submodule update --remote --merge * fix: update target directory for copying Claude Code templates * ✨ feat: merge Anthropics skills into template sync * 📝 docs: add agent plugins management implementation spec Add comprehensive implementation plan for plugin management feature: - Security validation and transactional operations - Plugin browsing, installation, and management UI - IPC handlers and PluginService architecture - Metadata caching and database integration * ✨ feat: add plugin management backend infrastructure Backend implementation for Claude Code plugin management: - Add PluginService with security validation and caching - Create IPC handlers for plugin operations (list, install, uninstall) - Add markdown parser with safe YAML frontmatter parsing - Extend AgentConfiguration schema with installed_plugins field - Update preload bridge to expose plugin API to renderer - Add plugin types (PluginMetadata, PluginError, PluginResult) Features: - Transactional install/uninstall with rollback - Path traversal prevention and file validation - 5-minute plugin list caching for performance - SHA-256 content hashing for integrity checks - Duplicate plugin handling (auto-replace) Dependencies added: - gray-matter: Markdown frontmatter parsing - js-yaml: Safe YAML parsing with FAILSAFE_SCHEMA * ✨ feat: add plugin management UI and integration Complete frontend implementation for Claude Code plugin management: **React Hooks:** - useAvailablePlugins: Fetch and cache available plugins - useInstalledPlugins: List installed plugins with refresh - usePluginActions: Install/uninstall with loading states **UI Components (HeroUI):** - PluginCard: Display plugin with install/uninstall actions - CategoryFilter: Multi-select chip-based category filter - InstalledPluginsList: Table view with uninstall confirmation - PluginBrowser: Search, filter, pagination, responsive grid - PluginSettings: Main container with Available/Installed tabs **Integration:** - Added "Plugins" tab to AgentSettingsPopup - Full i18n support (English, Simplified Chinese, Traditional Chinese) - Toast notifications for success/error states - Loading skeletons and empty states **Features:** - Search plugins by name/description - Filter by category and type (agents/commands) - Pagination (12 items per page) - Install/uninstall with confirmation dialogs - Real-time plugin list updates - Responsive grid layout (1-3 columns) All code formatted with Biome and follows existing patterns. * 🐛 fix: add missing plugin i18n keys at root level Add plugin translation keys at root 'plugins.*' level to match component usage: - Search and filter UI strings - Pluralization support for result counts - Empty state messages - Action button labels - Confirmation dialog text Translations added for all three locales (en-US, zh-CN, zh-TW). * 🐛 fix: use getResourcePath() utility for plugin directory resolution Replace manual path calculation with getResourcePath() utility which correctly handles both development and production environments. This fixes the issue where plugins were not loading because __dirname was resolving to the wrong location. Fixes: - Plugins now load correctly in development mode - Path resolution consistent with other resource loading in the app - Removed unused 'app' import from electron * 🎨 fix: improve plugin UI scrolling and category filter layout Fixes two UI issues: 1. Enable scrolling for plugin list: - Changed overflow-hidden to overflow-y-auto on tab containers - Plugin grid now scrollable when content exceeds viewport 2. Make category filter more compact: - Added max-h-24 (96px) height limit to category chip container - Enabled vertical scrolling for category chips - Prevents category filter from taking too much vertical space UI improvements enhance usability when browsing large plugin collections. * 🎨 fix: ensure both agent and command badges have visible backgrounds Changed Chip variant from 'flat' to 'solid' for plugin type badges. This ensures both agent (primary) and command (secondary) badges display with consistent, visible background colors instead of command badges appearing as text-only. * ✨ feat: add plugin detail modal for viewing full plugin information Add modal to display complete plugin details when clicking on a card: Features: - Click any plugin card to view full details in a modal - Shows complete description (not truncated) - Displays all metadata: version, author, tools, allowed_tools, tags - Shows file info: filename, size, source path, install date - Install/uninstall actions available in modal - Hover effect on cards to indicate clickability - Button clicks don't trigger card click (event.stopPropagation) Components: - New PluginDetailModal component with scrollable content - Updated PluginCard to be clickable (isPressable) - Updated PluginBrowser to manage modal state UI improvements provide better plugin exploration and decision-making. * 🐛 fix: render plugin detail modal above agent settings modal Use React portal to render PluginDetailModal directly to document.body, ensuring it appears above the agent settings modal instead of being blocked by it. Changes: - Import createPortal from react-dom - Wrap modal content in createPortal(modalContent, document.body) - Add z-[9999] to modal wrapper for proper layering Fixes modal visibility issue where plugin details were hidden behind the parent agent settings modal. * ✨ feat: add plugin content viewing and editing in detail modal - Added IPC channels for reading and writing plugin content - Implemented readContent() and writeContent() methods in PluginService - Added IPC handlers for content operations with proper error handling - Exposed plugin content API through preload bridge - Updated PluginDetailModal to fetch and display markdown content - Added edit mode with textarea for modifying plugin content - Implemented save/cancel functionality with optimistic UI updates - Added agentId prop to component chain for write operations - Updated AgentConfigurationSchema to include all plugin metadata fields - Moved plugin types to shared @types for cross-process access - Added validation and security checks for content read/write - Updated content hash in DB after successful edits * 🐛 fix: change event handler from onPress to onClick for uninstall and install buttons * 📝 docs: update AI Assistant Guide to clarify proposal and commit guidelines * 📝 docs: add skills support extension spec for agent plugins management * ✨ feat: add secure file operation utilities for skills plugin system - Implement copyDirectoryRecursive() with security protections - Implement deleteDirectoryRecursive() with path validation - Implement getDirectorySize() for folder size calculation - Add path traversal protection using isPathInside() - Handle symlinks securely to prevent attacks - Add recursion depth limits to prevent stack overflow - Preserve file permissions during copy - Handle race conditions and missing files gracefully - Skip special files (pipes, sockets, devices) Security features: - Path validation against allowedBasePath boundary - Symlink detection and skip to prevent circular loops - Input validation for null/empty/relative paths - Comprehensive error handling and logging Updated spec status to "In Progress" and added implementation progress checklist. * ✨ feat: add skill type support and skill metadata parsing Type System Updates (plugin.ts): - Add PluginType export for 'agent' | 'command' | 'skill' - Update PluginMetadataSchema to include 'skill' in type enum - Update InstalledPluginSchema to support skill type - Update all option interfaces to support skill type - Add skills array to ListAvailablePluginsResult - Document filename semantics differences between types Markdown Parser Updates (markdownParser.ts): - Implement parseSkillMetadata() function for SKILL.md parsing - Add comprehensive input validation (absolute path check) - Add robust error handling with specific PluginErrors - Add try-catch around file operations and YAML parsing - Add type validation for frontmatter data fields - Add proper logging using loggerService - Handle getDirectorySize() failures gracefully - Document hash scope decision (SKILL.md only vs entire folder) - Use FAILSAFE_SCHEMA for safe YAML parsing Security improvements: - Path validation to ensure absolute paths - Differentiate ENOENT from permission errors - Type validation for all frontmatter fields - Safe YAML parsing to prevent deserialization attacks Updated spec progress tracking. * ✨ feat: implement complete skill support in PluginService Core Infrastructure: - Add imports for parseSkillMetadata and file operation utilities - Add PluginType to imports for type-safe handling Skill-Specific Methods: - sanitizeFolderName() - validates folder names (no dots allowed) - scanSkillDirectory() - scans skills/ for skill folders - installSkill() - copies folders with transaction/rollback - uninstallSkill() - removes folders with transaction/rollback Updated Methods for Skills Support: - listAvailable() - now scans and returns skills array - install() - branches on type to handle skills vs files - uninstall() - branches on type for skill/file handling - ensureClaudeDirectory() - handles 'skills' subdirectory - listInstalled() - validates skill folders on filesystem - writeContent() - updated signature to accept PluginType Key Implementation Details: - Skills use folder names WITHOUT extensions - Agents/commands use filenames WITH .md extension - Different sanitization rules for folders vs files - Transaction pattern with rollback for all operations - Comprehensive logging and error handling - Maintains backward compatibility with existing code Updated spec progress tracking. * ✨ feat: add skill support to frontend hooks and UI components Frontend Hooks (usePlugins.ts): - Add skills state to useAvailablePlugins hook - Return skills array in hook result - Update install() to accept 'skill' type - Update uninstall() to accept 'skill' type UI Components: - PluginCard: Add 'skill' type badge with success color - PluginBrowser: Add skills prop and include in plugin list - PluginBrowser: Update type definitions to include 'skill' - PluginBrowser: Include skills in tab filtering Complete frontend integration for skills plugin type. Updated spec progress tracking. * ♻️ refactor: remove unused variable in installSkill method * 📝 docs: mark implementation as complete with summary Implementation Status: COMPLETE (11/12 tasks) Completed: - ✅ File operation utilities with security protections - ✅ Skill metadata parsing with validation - ✅ Plugin type system updated to include 'skill' - ✅ PluginService skill methods (scan, install, uninstall) - ✅ PluginService updated for skill support - ✅ IPC handlers (no changes needed - already generic) - ✅ Frontend hooks updated for skills - ✅ UI components updated (PluginCard, PluginBrowser) - ✅ Build check passed with lint fixes Deferred (non-blocking): - ⏸️ Session integration - requires further investigation into session handler location and implementation The core skills plugin system is fully implemented and functional. Skills can be browsed, installed, and uninstalled through the UI. All security requirements met with path validation and transaction rollback. Code passes lint checks and follows project patterns. * 🐛 fix: pass skills prop to PluginBrowser component Fixed "skills is not iterable" error by: - Destructuring skills from useAvailablePlugins hook - Updating type annotations to include 'skill' type - Passing skills prop to PluginBrowser component This completes the missing UI wiring for skills support. * ✨ feat: add Skills tab to plugin browser Added missing Skills tab to PluginBrowser component: - Added Skills tab to type tabs - Added translations for skills in all locales (en-us, zh-cn, zh-tw) - English: "Skills" - Simplified Chinese: "技能" - Traditional Chinese: "技能" This completes the UI integration for the skills plugin type. * ✨ feat: add 'skill' type to AgentConfiguration and GetAgentSessionResponse schemas * ⬆️ chore: upgrade @anthropic-ai/claude-agent-sdk to v0.1.25 with patch - Updated from v0.1.1 to v0.1.25 - Applied fork/IPC patch to new version - Removed old patch file - All tests passing * 🐛 fix: resolve linting and TypeScript type errors in build check - Add external/** and resources/data/claude-code-plugins/** to lint ignore patterns to exclude git submodules and plugin templates from linting - Fix TypeScript error handling in IPC handlers by properly typing caught errors - Fix AgentConfiguration type mismatches by providing default values for permission_mode and max_turns when spreading configuration - Replace control character regex with String.fromCharCode() to avoid ESLint no-control-regex rule in sanitization functions - Fix markdownParser yaml.load return type by adding type assertion - Add getPluginErrorMessage helper to properly extract error messages from PluginError discriminated union types Main process TypeScript errors: Fixed (0 errors) Linting errors: Fixed (0 errors from 4397) Remaining: 4 renderer TypeScript errors in settings components * ♻️ refactor: improve plugin error handling and reorganize i18n structure * ⬆️ chore: update @anthropic-ai/claude-agent-sdk to include patch and additional dependencies * 🗑️ chore: remove unused Claude code plugins and related configurations - Deleted `.gitmodules` and associated submodules for `claude-code-templates` and `anthropics-skills`. - Updated `.gitignore`, `.oxlintrc.json`, and `eslint.config.mjs` to exclude `claude-code-plugins`. - Modified `package.json` to remove the build script dependency on copying templates. - Adjusted `PluginService.ts` to handle plugin paths without relying on removed resources. * format code * delete * delete * fix(i18n): Auto update translations for PR #10854 * ✨ feat: enhance PluginService and markdownParser with recursive skill directory search - Added `findAllSkillDirectories` function to recursively locate directories containing `SKILL.md`. - Updated `scanSkillDirectory` method in `PluginService` to utilize the new recursive search. - Modified `PluginDetailModal` to append `/SKILL.md` to the source path for skill plugins. * fix(i18n): Auto update translations for PR #10854 * remove specs * update claude code plugins files --------- Co-authored-by: suyao <sy20010504@gmail.com> Co-authored-by: beyondkmp <beyondkmp@gmail.com> Co-authored-by: GitHub Action <action@github.com>
1021 lines
40 KiB
TypeScript
1021 lines
40 KiB
TypeScript
import fs from 'node:fs'
|
|
import { arch } from 'node:os'
|
|
import path from 'node:path'
|
|
|
|
import { loggerService } from '@logger'
|
|
import { isLinux, isMac, isPortable, isWin } from '@main/constant'
|
|
import { generateSignature } from '@main/integration/cherryai'
|
|
import anthropicService from '@main/services/AnthropicService'
|
|
import { getBinaryPath, isBinaryExists, runInstallScript } from '@main/utils/process'
|
|
import { handleZoomFactor } from '@main/utils/zoom'
|
|
import { SpanEntity, TokenUsage } from '@mcp-trace/trace-core'
|
|
import { MIN_WINDOW_HEIGHT, MIN_WINDOW_WIDTH, UpgradeChannel } from '@shared/config/constant'
|
|
import { IpcChannel } from '@shared/IpcChannel'
|
|
import type { PluginError } from '@types'
|
|
import {
|
|
AgentPersistedMessage,
|
|
FileMetadata,
|
|
Notification,
|
|
OcrProvider,
|
|
Provider,
|
|
Shortcut,
|
|
SupportedOcrFile,
|
|
ThemeMode
|
|
} from '@types'
|
|
import checkDiskSpace from 'check-disk-space'
|
|
import { BrowserWindow, dialog, ipcMain, ProxyConfig, session, shell, systemPreferences, webContents } from 'electron'
|
|
import fontList from 'font-list'
|
|
|
|
import { agentMessageRepository } from './services/agents/database'
|
|
import { apiServerService } from './services/ApiServerService'
|
|
import appService from './services/AppService'
|
|
import AppUpdater from './services/AppUpdater'
|
|
import BackupManager from './services/BackupManager'
|
|
import { codeToolsService } from './services/CodeToolsService'
|
|
import { configManager } from './services/ConfigManager'
|
|
import CopilotService from './services/CopilotService'
|
|
import DxtService from './services/DxtService'
|
|
import { ExportService } from './services/ExportService'
|
|
import { fileStorage as fileManager } from './services/FileStorage'
|
|
import FileService from './services/FileSystemService'
|
|
import KnowledgeService from './services/KnowledgeService'
|
|
import mcpService from './services/MCPService'
|
|
import MemoryService from './services/memory/MemoryService'
|
|
import { openTraceWindow, setTraceWindowTitle } from './services/NodeTraceService'
|
|
import NotificationService from './services/NotificationService'
|
|
import * as NutstoreService from './services/NutstoreService'
|
|
import ObsidianVaultService from './services/ObsidianVaultService'
|
|
import { ocrService } from './services/ocr/OcrService'
|
|
import OvmsManager from './services/OvmsManager'
|
|
import { PluginService } from './services/PluginService'
|
|
import { proxyManager } from './services/ProxyManager'
|
|
import { pythonService } from './services/PythonService'
|
|
import { FileServiceManager } from './services/remotefile/FileServiceManager'
|
|
import { searchService } from './services/SearchService'
|
|
import { SelectionService } from './services/SelectionService'
|
|
import { registerShortcuts, unregisterAllShortcuts } from './services/ShortcutService'
|
|
import {
|
|
addEndMessage,
|
|
addStreamMessage,
|
|
bindTopic,
|
|
cleanHistoryTrace,
|
|
cleanLocalData,
|
|
cleanTopic,
|
|
getEntity,
|
|
getSpans,
|
|
saveEntity,
|
|
saveSpans,
|
|
tokenUsage
|
|
} from './services/SpanCacheService'
|
|
import storeSyncService from './services/StoreSyncService'
|
|
import { themeService } from './services/ThemeService'
|
|
import VertexAIService from './services/VertexAIService'
|
|
import { setOpenLinkExternal } from './services/WebviewService'
|
|
import { windowService } from './services/WindowService'
|
|
import { calculateDirectorySize, getResourcePath } from './utils'
|
|
import { decrypt, encrypt } from './utils/aes'
|
|
import {
|
|
getCacheDir,
|
|
getConfigDir,
|
|
getFilesDir,
|
|
getNotesDir,
|
|
hasWritePermission,
|
|
isPathInside,
|
|
untildify
|
|
} from './utils/file'
|
|
import { updateAppDataConfig } from './utils/init'
|
|
import { compress, decompress } from './utils/zip'
|
|
|
|
const logger = loggerService.withContext('IPC')
|
|
|
|
const backupManager = new BackupManager()
|
|
const exportService = new ExportService()
|
|
const obsidianVaultService = new ObsidianVaultService()
|
|
const vertexAIService = VertexAIService.getInstance()
|
|
const memoryService = MemoryService.getInstance()
|
|
const dxtService = new DxtService()
|
|
const ovmsManager = new OvmsManager()
|
|
const pluginService = PluginService.getInstance()
|
|
|
|
function normalizeError(error: unknown): Error {
|
|
return error instanceof Error ? error : new Error(String(error))
|
|
}
|
|
|
|
function extractPluginError(error: unknown): PluginError | null {
|
|
if (error && typeof error === 'object' && 'type' in error && typeof (error as { type: unknown }).type === 'string') {
|
|
return error as PluginError
|
|
}
|
|
return null
|
|
}
|
|
|
|
export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
|
const appUpdater = new AppUpdater()
|
|
const notificationService = new NotificationService()
|
|
|
|
// Initialize Python service with main window
|
|
pythonService.setMainWindow(mainWindow)
|
|
|
|
const checkMainWindow = () => {
|
|
if (!mainWindow || mainWindow.isDestroyed()) {
|
|
throw new Error('Main window does not exist or has been destroyed')
|
|
}
|
|
}
|
|
|
|
ipcMain.handle(IpcChannel.App_Info, () => ({
|
|
version: app.getVersion(),
|
|
isPackaged: app.isPackaged,
|
|
appPath: app.getAppPath(),
|
|
filesPath: getFilesDir(),
|
|
notesPath: getNotesDir(),
|
|
configPath: getConfigDir(),
|
|
appDataPath: app.getPath('userData'),
|
|
resourcesPath: getResourcePath(),
|
|
logsPath: logger.getLogsDir(),
|
|
arch: arch(),
|
|
isPortable: isWin && 'PORTABLE_EXECUTABLE_DIR' in process.env,
|
|
installPath: path.dirname(app.getPath('exe'))
|
|
}))
|
|
|
|
ipcMain.handle(IpcChannel.App_Proxy, async (_, proxy: string, bypassRules?: string) => {
|
|
let proxyConfig: ProxyConfig
|
|
|
|
if (proxy === 'system') {
|
|
// system proxy will use the system filter by themselves
|
|
proxyConfig = { mode: 'system' }
|
|
} else if (proxy) {
|
|
proxyConfig = { mode: 'fixed_servers', proxyRules: proxy, proxyBypassRules: bypassRules }
|
|
} else {
|
|
proxyConfig = { mode: 'direct' }
|
|
}
|
|
|
|
await proxyManager.configureProxy(proxyConfig)
|
|
})
|
|
|
|
ipcMain.handle(IpcChannel.App_Reload, () => mainWindow.reload())
|
|
ipcMain.handle(IpcChannel.App_Quit, () => app.quit())
|
|
ipcMain.handle(IpcChannel.Open_Website, (_, url: string) => shell.openExternal(url))
|
|
|
|
// Update
|
|
ipcMain.handle(IpcChannel.App_QuitAndInstall, () => appUpdater.quitAndInstall())
|
|
|
|
// language
|
|
ipcMain.handle(IpcChannel.App_SetLanguage, (_, language) => {
|
|
configManager.setLanguage(language)
|
|
})
|
|
|
|
// spell check
|
|
ipcMain.handle(IpcChannel.App_SetEnableSpellCheck, (_, isEnable: boolean) => {
|
|
// disable spell check for all webviews
|
|
const webviews = webContents.getAllWebContents()
|
|
webviews.forEach((webview) => {
|
|
webview.session.setSpellCheckerEnabled(isEnable)
|
|
})
|
|
})
|
|
|
|
// spell check languages
|
|
ipcMain.handle(IpcChannel.App_SetSpellCheckLanguages, (_, languages: string[]) => {
|
|
if (languages.length === 0) {
|
|
return
|
|
}
|
|
const windows = BrowserWindow.getAllWindows()
|
|
windows.forEach((window) => {
|
|
window.webContents.session.setSpellCheckerLanguages(languages)
|
|
})
|
|
configManager.set('spellCheckLanguages', languages)
|
|
})
|
|
|
|
// launch on boot
|
|
ipcMain.handle(IpcChannel.App_SetLaunchOnBoot, (_, isLaunchOnBoot: boolean) => {
|
|
appService.setAppLaunchOnBoot(isLaunchOnBoot)
|
|
})
|
|
|
|
// launch to tray
|
|
ipcMain.handle(IpcChannel.App_SetLaunchToTray, (_, isActive: boolean) => {
|
|
configManager.setLaunchToTray(isActive)
|
|
})
|
|
|
|
// tray
|
|
ipcMain.handle(IpcChannel.App_SetTray, (_, isActive: boolean) => {
|
|
configManager.setTray(isActive)
|
|
})
|
|
|
|
// to tray on close
|
|
ipcMain.handle(IpcChannel.App_SetTrayOnClose, (_, isActive: boolean) => {
|
|
configManager.setTrayOnClose(isActive)
|
|
})
|
|
|
|
// auto update
|
|
ipcMain.handle(IpcChannel.App_SetAutoUpdate, (_, isActive: boolean) => {
|
|
appUpdater.setAutoUpdate(isActive)
|
|
configManager.setAutoUpdate(isActive)
|
|
})
|
|
|
|
ipcMain.handle(IpcChannel.App_SetTestPlan, async (_, isActive: boolean) => {
|
|
logger.info(`set test plan: ${isActive}`)
|
|
if (isActive !== configManager.getTestPlan()) {
|
|
appUpdater.cancelDownload()
|
|
configManager.setTestPlan(isActive)
|
|
}
|
|
})
|
|
|
|
ipcMain.handle(IpcChannel.App_SetTestChannel, async (_, channel: UpgradeChannel) => {
|
|
logger.info(`set test channel: ${channel}`)
|
|
if (channel !== configManager.getTestChannel()) {
|
|
appUpdater.cancelDownload()
|
|
configManager.setTestChannel(channel)
|
|
}
|
|
})
|
|
|
|
ipcMain.handle(IpcChannel.AgentMessage_PersistExchange, async (_event, payload) => {
|
|
try {
|
|
return await agentMessageRepository.persistExchange(payload)
|
|
} catch (error) {
|
|
logger.error('Failed to persist agent session messages', error as Error)
|
|
throw error
|
|
}
|
|
})
|
|
|
|
ipcMain.handle(
|
|
IpcChannel.AgentMessage_GetHistory,
|
|
async (_event, { sessionId }: { sessionId: string }): Promise<AgentPersistedMessage[]> => {
|
|
try {
|
|
return await agentMessageRepository.getSessionHistory(sessionId)
|
|
} catch (error) {
|
|
logger.error('Failed to get agent session history', error as Error)
|
|
throw error
|
|
}
|
|
}
|
|
)
|
|
|
|
//only for mac
|
|
if (isMac) {
|
|
ipcMain.handle(IpcChannel.App_MacIsProcessTrusted, (): boolean => {
|
|
return systemPreferences.isTrustedAccessibilityClient(false)
|
|
})
|
|
|
|
//return is only the current state, not the new state
|
|
ipcMain.handle(IpcChannel.App_MacRequestProcessTrust, (): boolean => {
|
|
return systemPreferences.isTrustedAccessibilityClient(true)
|
|
})
|
|
}
|
|
|
|
ipcMain.handle(IpcChannel.App_SetFullScreen, (_, value: boolean): void => {
|
|
mainWindow.setFullScreen(value)
|
|
})
|
|
|
|
ipcMain.handle(IpcChannel.App_IsFullScreen, (): boolean => {
|
|
return mainWindow.isFullScreen()
|
|
})
|
|
|
|
// Get System Fonts
|
|
ipcMain.handle(IpcChannel.App_GetSystemFonts, async () => {
|
|
try {
|
|
const fonts = await fontList.getFonts()
|
|
return fonts.map((font: string) => font.replace(/^"(.*)"$/, '$1')).filter((font: string) => font.length > 0)
|
|
} catch (error) {
|
|
logger.error('Failed to get system fonts:', error as Error)
|
|
return []
|
|
}
|
|
})
|
|
|
|
ipcMain.handle(IpcChannel.Config_Set, (_, key: string, value: any, isNotify: boolean = false) => {
|
|
configManager.set(key, value, isNotify)
|
|
})
|
|
|
|
ipcMain.handle(IpcChannel.Config_Get, (_, key: string) => {
|
|
return configManager.get(key)
|
|
})
|
|
|
|
// theme
|
|
ipcMain.handle(IpcChannel.App_SetTheme, (_, theme: ThemeMode) => {
|
|
themeService.setTheme(theme)
|
|
})
|
|
|
|
ipcMain.handle(IpcChannel.App_HandleZoomFactor, (_, delta: number, reset: boolean = false) => {
|
|
const windows = BrowserWindow.getAllWindows()
|
|
handleZoomFactor(windows, delta, reset)
|
|
return configManager.getZoomFactor()
|
|
})
|
|
|
|
// clear cache
|
|
ipcMain.handle(IpcChannel.App_ClearCache, async () => {
|
|
const sessions = [session.defaultSession, session.fromPartition('persist:webview')]
|
|
|
|
try {
|
|
await Promise.all(
|
|
sessions.map(async (session) => {
|
|
await session.clearCache()
|
|
await session.clearStorageData({
|
|
storages: ['cookies', 'filesystem', 'shadercache', 'websql', 'serviceworkers', 'cachestorage']
|
|
})
|
|
})
|
|
)
|
|
await fileManager.clearTemp()
|
|
// do not clear logs for now
|
|
// TODO clear logs
|
|
// await fs.writeFileSync(log.transports.file.getFile().path, '')
|
|
return { success: true }
|
|
} catch (error: any) {
|
|
logger.error('Failed to clear cache:', error)
|
|
return { success: false, error: error.message }
|
|
}
|
|
})
|
|
|
|
// get cache size
|
|
ipcMain.handle(IpcChannel.App_GetCacheSize, async () => {
|
|
const cachePath = getCacheDir()
|
|
logger.info(`Calculating cache size for path: ${cachePath}`)
|
|
|
|
try {
|
|
const sizeInBytes = await calculateDirectorySize(cachePath)
|
|
const sizeInMB = (sizeInBytes / (1024 * 1024)).toFixed(2)
|
|
return `${sizeInMB}`
|
|
} catch (error: any) {
|
|
logger.error(`Failed to calculate cache size for ${cachePath}: ${error.message}`)
|
|
return '0'
|
|
}
|
|
})
|
|
|
|
let preventQuitListener: ((event: Electron.Event) => void) | null = null
|
|
ipcMain.handle(IpcChannel.App_SetStopQuitApp, (_, stop: boolean = false, reason: string = '') => {
|
|
if (stop) {
|
|
// Only add listener if not already added
|
|
if (!preventQuitListener) {
|
|
preventQuitListener = (event: Electron.Event) => {
|
|
event.preventDefault()
|
|
notificationService.sendNotification({
|
|
title: reason,
|
|
message: reason
|
|
} as Notification)
|
|
}
|
|
app.on('before-quit', preventQuitListener)
|
|
}
|
|
} else {
|
|
// Remove listener if it exists
|
|
if (preventQuitListener) {
|
|
app.removeListener('before-quit', preventQuitListener)
|
|
preventQuitListener = null
|
|
}
|
|
}
|
|
})
|
|
|
|
// Select app data path
|
|
ipcMain.handle(IpcChannel.App_Select, async (_, options: Electron.OpenDialogOptions) => {
|
|
try {
|
|
const { canceled, filePaths } = await dialog.showOpenDialog(options)
|
|
if (canceled || filePaths.length === 0) {
|
|
return null
|
|
}
|
|
return filePaths[0]
|
|
} catch (error: any) {
|
|
logger.error('Failed to select app data path:', error)
|
|
return null
|
|
}
|
|
})
|
|
|
|
ipcMain.handle(IpcChannel.App_HasWritePermission, async (_, filePath: string) => {
|
|
const hasPermission = await hasWritePermission(filePath)
|
|
return hasPermission
|
|
})
|
|
|
|
ipcMain.handle(IpcChannel.App_ResolvePath, async (_, filePath: string) => {
|
|
return path.resolve(untildify(filePath))
|
|
})
|
|
|
|
// Check if a path is inside another path (proper parent-child relationship)
|
|
ipcMain.handle(IpcChannel.App_IsPathInside, async (_, childPath: string, parentPath: string) => {
|
|
return isPathInside(childPath, parentPath)
|
|
})
|
|
|
|
// Set app data path
|
|
ipcMain.handle(IpcChannel.App_SetAppDataPath, async (_, filePath: string) => {
|
|
updateAppDataConfig(filePath)
|
|
app.setPath('userData', filePath)
|
|
})
|
|
|
|
ipcMain.handle(IpcChannel.App_GetDataPathFromArgs, () => {
|
|
return process.argv
|
|
.slice(1)
|
|
.find((arg) => arg.startsWith('--new-data-path='))
|
|
?.split('--new-data-path=')[1]
|
|
})
|
|
|
|
ipcMain.handle(IpcChannel.App_FlushAppData, () => {
|
|
BrowserWindow.getAllWindows().forEach((w) => {
|
|
w.webContents.session.flushStorageData()
|
|
w.webContents.session.cookies.flushStore()
|
|
|
|
w.webContents.session.closeAllConnections()
|
|
})
|
|
|
|
session.defaultSession.flushStorageData()
|
|
session.defaultSession.cookies.flushStore()
|
|
session.defaultSession.closeAllConnections()
|
|
})
|
|
|
|
ipcMain.handle(IpcChannel.App_IsNotEmptyDir, async (_, path: string) => {
|
|
return fs.readdirSync(path).length > 0
|
|
})
|
|
|
|
// Copy user data to new location
|
|
ipcMain.handle(IpcChannel.App_Copy, async (_, oldPath: string, newPath: string, occupiedDirs: string[] = []) => {
|
|
try {
|
|
await fs.promises.cp(oldPath, newPath, {
|
|
recursive: true,
|
|
filter: (src) => {
|
|
if (occupiedDirs.some((dir) => src.startsWith(path.resolve(dir)))) {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
})
|
|
return { success: true }
|
|
} catch (error: any) {
|
|
logger.error('Failed to copy user data:', error)
|
|
return { success: false, error: error.message }
|
|
}
|
|
})
|
|
|
|
// Relaunch app
|
|
ipcMain.handle(IpcChannel.App_RelaunchApp, (_, options?: Electron.RelaunchOptions) => {
|
|
// Fix for .AppImage
|
|
if (isLinux && process.env.APPIMAGE) {
|
|
logger.info(`Relaunching app with options: ${process.env.APPIMAGE}`, options)
|
|
// On Linux, we need to use the APPIMAGE environment variable to relaunch
|
|
// https://github.com/electron-userland/electron-builder/issues/1727#issuecomment-769896927
|
|
options = options || {}
|
|
options.execPath = process.env.APPIMAGE
|
|
options.args = options.args || []
|
|
options.args.unshift('--appimage-extract-and-run')
|
|
}
|
|
|
|
if (isWin && isPortable) {
|
|
options = options || {}
|
|
options.execPath = process.env.PORTABLE_EXECUTABLE_FILE
|
|
options.args = options.args || []
|
|
}
|
|
|
|
app.relaunch(options)
|
|
app.exit(0)
|
|
})
|
|
|
|
// check for update
|
|
ipcMain.handle(IpcChannel.App_CheckForUpdate, async () => {
|
|
return await appUpdater.checkForUpdates()
|
|
})
|
|
|
|
// notification
|
|
ipcMain.handle(IpcChannel.Notification_Send, async (_, notification: Notification) => {
|
|
await notificationService.sendNotification(notification)
|
|
})
|
|
ipcMain.handle(IpcChannel.Notification_OnClick, (_, notification: Notification) => {
|
|
mainWindow.webContents.send('notification-click', notification)
|
|
})
|
|
|
|
// zip
|
|
ipcMain.handle(IpcChannel.Zip_Compress, (_, text: string) => compress(text))
|
|
ipcMain.handle(IpcChannel.Zip_Decompress, (_, text: Buffer) => decompress(text))
|
|
|
|
// system
|
|
ipcMain.handle(IpcChannel.System_GetDeviceType, () => (isMac ? 'mac' : isWin ? 'windows' : 'linux'))
|
|
ipcMain.handle(IpcChannel.System_GetHostname, () => require('os').hostname())
|
|
ipcMain.handle(IpcChannel.System_GetCpuName, () => require('os').cpus()[0].model)
|
|
ipcMain.handle(IpcChannel.System_ToggleDevTools, (e) => {
|
|
const win = BrowserWindow.fromWebContents(e.sender)
|
|
win && win.webContents.toggleDevTools()
|
|
})
|
|
|
|
// backup
|
|
ipcMain.handle(IpcChannel.Backup_Backup, backupManager.backup.bind(backupManager))
|
|
ipcMain.handle(IpcChannel.Backup_Restore, backupManager.restore.bind(backupManager))
|
|
ipcMain.handle(IpcChannel.Backup_BackupToWebdav, backupManager.backupToWebdav.bind(backupManager))
|
|
ipcMain.handle(IpcChannel.Backup_RestoreFromWebdav, backupManager.restoreFromWebdav.bind(backupManager))
|
|
ipcMain.handle(IpcChannel.Backup_ListWebdavFiles, backupManager.listWebdavFiles.bind(backupManager))
|
|
ipcMain.handle(IpcChannel.Backup_CheckConnection, backupManager.checkConnection.bind(backupManager))
|
|
ipcMain.handle(IpcChannel.Backup_CreateDirectory, backupManager.createDirectory.bind(backupManager))
|
|
ipcMain.handle(IpcChannel.Backup_DeleteWebdavFile, backupManager.deleteWebdavFile.bind(backupManager))
|
|
ipcMain.handle(IpcChannel.Backup_BackupToLocalDir, backupManager.backupToLocalDir.bind(backupManager))
|
|
ipcMain.handle(IpcChannel.Backup_RestoreFromLocalBackup, backupManager.restoreFromLocalBackup.bind(backupManager))
|
|
ipcMain.handle(IpcChannel.Backup_ListLocalBackupFiles, backupManager.listLocalBackupFiles.bind(backupManager))
|
|
ipcMain.handle(IpcChannel.Backup_DeleteLocalBackupFile, backupManager.deleteLocalBackupFile.bind(backupManager))
|
|
ipcMain.handle(IpcChannel.Backup_BackupToS3, backupManager.backupToS3.bind(backupManager))
|
|
ipcMain.handle(IpcChannel.Backup_RestoreFromS3, backupManager.restoreFromS3.bind(backupManager))
|
|
ipcMain.handle(IpcChannel.Backup_ListS3Files, backupManager.listS3Files.bind(backupManager))
|
|
ipcMain.handle(IpcChannel.Backup_DeleteS3File, backupManager.deleteS3File.bind(backupManager))
|
|
ipcMain.handle(IpcChannel.Backup_CheckS3Connection, backupManager.checkS3Connection.bind(backupManager))
|
|
|
|
// file
|
|
ipcMain.handle(IpcChannel.File_Open, fileManager.open.bind(fileManager))
|
|
ipcMain.handle(IpcChannel.File_OpenPath, fileManager.openPath.bind(fileManager))
|
|
ipcMain.handle(IpcChannel.File_Save, fileManager.save.bind(fileManager))
|
|
ipcMain.handle(IpcChannel.File_Select, fileManager.selectFile.bind(fileManager))
|
|
ipcMain.handle(IpcChannel.File_Upload, fileManager.uploadFile.bind(fileManager))
|
|
ipcMain.handle(IpcChannel.File_Clear, fileManager.clear.bind(fileManager))
|
|
ipcMain.handle(IpcChannel.File_Read, fileManager.readFile.bind(fileManager))
|
|
ipcMain.handle(IpcChannel.File_ReadExternal, fileManager.readExternalFile.bind(fileManager))
|
|
ipcMain.handle(IpcChannel.File_Delete, fileManager.deleteFile.bind(fileManager))
|
|
ipcMain.handle(IpcChannel.File_DeleteDir, fileManager.deleteDir.bind(fileManager))
|
|
ipcMain.handle(IpcChannel.File_DeleteExternalFile, fileManager.deleteExternalFile.bind(fileManager))
|
|
ipcMain.handle(IpcChannel.File_DeleteExternalDir, fileManager.deleteExternalDir.bind(fileManager))
|
|
ipcMain.handle(IpcChannel.File_Move, fileManager.moveFile.bind(fileManager))
|
|
ipcMain.handle(IpcChannel.File_MoveDir, fileManager.moveDir.bind(fileManager))
|
|
ipcMain.handle(IpcChannel.File_Rename, fileManager.renameFile.bind(fileManager))
|
|
ipcMain.handle(IpcChannel.File_RenameDir, fileManager.renameDir.bind(fileManager))
|
|
ipcMain.handle(IpcChannel.File_Get, fileManager.getFile.bind(fileManager))
|
|
ipcMain.handle(IpcChannel.File_SelectFolder, fileManager.selectFolder.bind(fileManager))
|
|
ipcMain.handle(IpcChannel.File_CreateTempFile, fileManager.createTempFile.bind(fileManager))
|
|
ipcMain.handle(IpcChannel.File_Mkdir, fileManager.mkdir.bind(fileManager))
|
|
ipcMain.handle(IpcChannel.File_Write, fileManager.writeFile.bind(fileManager))
|
|
ipcMain.handle(IpcChannel.File_WriteWithId, fileManager.writeFileWithId.bind(fileManager))
|
|
ipcMain.handle(IpcChannel.File_SaveImage, fileManager.saveImage.bind(fileManager))
|
|
ipcMain.handle(IpcChannel.File_Base64Image, fileManager.base64Image.bind(fileManager))
|
|
ipcMain.handle(IpcChannel.File_SaveBase64Image, fileManager.saveBase64Image.bind(fileManager))
|
|
ipcMain.handle(IpcChannel.File_SavePastedImage, fileManager.savePastedImage.bind(fileManager))
|
|
ipcMain.handle(IpcChannel.File_Base64File, fileManager.base64File.bind(fileManager))
|
|
ipcMain.handle(IpcChannel.File_GetPdfInfo, fileManager.pdfPageCount.bind(fileManager))
|
|
ipcMain.handle(IpcChannel.File_Download, fileManager.downloadFile.bind(fileManager))
|
|
ipcMain.handle(IpcChannel.File_Copy, fileManager.copyFile.bind(fileManager))
|
|
ipcMain.handle(IpcChannel.File_BinaryImage, fileManager.binaryImage.bind(fileManager))
|
|
ipcMain.handle(IpcChannel.File_OpenWithRelativePath, fileManager.openFileWithRelativePath.bind(fileManager))
|
|
ipcMain.handle(IpcChannel.File_IsTextFile, fileManager.isTextFile.bind(fileManager))
|
|
ipcMain.handle(IpcChannel.File_GetDirectoryStructure, fileManager.getDirectoryStructure.bind(fileManager))
|
|
ipcMain.handle(IpcChannel.File_CheckFileName, fileManager.fileNameGuard.bind(fileManager))
|
|
ipcMain.handle(IpcChannel.File_ValidateNotesDirectory, fileManager.validateNotesDirectory.bind(fileManager))
|
|
ipcMain.handle(IpcChannel.File_StartWatcher, fileManager.startFileWatcher.bind(fileManager))
|
|
ipcMain.handle(IpcChannel.File_StopWatcher, fileManager.stopFileWatcher.bind(fileManager))
|
|
ipcMain.handle(IpcChannel.File_ShowInFolder, fileManager.showInFolder.bind(fileManager))
|
|
|
|
// file service
|
|
ipcMain.handle(IpcChannel.FileService_Upload, async (_, provider: Provider, file: FileMetadata) => {
|
|
const service = FileServiceManager.getInstance().getService(provider)
|
|
return await service.uploadFile(file)
|
|
})
|
|
|
|
ipcMain.handle(IpcChannel.FileService_List, async (_, provider: Provider) => {
|
|
const service = FileServiceManager.getInstance().getService(provider)
|
|
return await service.listFiles()
|
|
})
|
|
|
|
ipcMain.handle(IpcChannel.FileService_Delete, async (_, provider: Provider, fileId: string) => {
|
|
const service = FileServiceManager.getInstance().getService(provider)
|
|
return await service.deleteFile(fileId)
|
|
})
|
|
|
|
ipcMain.handle(IpcChannel.FileService_Retrieve, async (_, provider: Provider, fileId: string) => {
|
|
const service = FileServiceManager.getInstance().getService(provider)
|
|
return await service.retrieveFile(fileId)
|
|
})
|
|
|
|
// fs
|
|
ipcMain.handle(IpcChannel.Fs_Read, FileService.readFile.bind(FileService))
|
|
ipcMain.handle(IpcChannel.Fs_ReadText, FileService.readTextFileWithAutoEncoding.bind(FileService))
|
|
|
|
// export
|
|
ipcMain.handle(IpcChannel.Export_Word, exportService.exportToWord.bind(exportService))
|
|
|
|
// open path
|
|
ipcMain.handle(IpcChannel.Open_Path, async (_, path: string) => {
|
|
await shell.openPath(path)
|
|
})
|
|
|
|
// shortcuts
|
|
ipcMain.handle(IpcChannel.Shortcuts_Update, (_, shortcuts: Shortcut[]) => {
|
|
configManager.setShortcuts(shortcuts)
|
|
// Refresh shortcuts registration
|
|
if (mainWindow) {
|
|
unregisterAllShortcuts()
|
|
registerShortcuts(mainWindow)
|
|
}
|
|
})
|
|
|
|
ipcMain.handle(IpcChannel.KnowledgeBase_Create, KnowledgeService.create.bind(KnowledgeService))
|
|
ipcMain.handle(IpcChannel.KnowledgeBase_Reset, KnowledgeService.reset.bind(KnowledgeService))
|
|
ipcMain.handle(IpcChannel.KnowledgeBase_Delete, KnowledgeService.delete.bind(KnowledgeService))
|
|
ipcMain.handle(IpcChannel.KnowledgeBase_Add, KnowledgeService.add.bind(KnowledgeService))
|
|
ipcMain.handle(IpcChannel.KnowledgeBase_Remove, KnowledgeService.remove.bind(KnowledgeService))
|
|
ipcMain.handle(IpcChannel.KnowledgeBase_Search, KnowledgeService.search.bind(KnowledgeService))
|
|
ipcMain.handle(IpcChannel.KnowledgeBase_Rerank, KnowledgeService.rerank.bind(KnowledgeService))
|
|
ipcMain.handle(IpcChannel.KnowledgeBase_Check_Quota, KnowledgeService.checkQuota.bind(KnowledgeService))
|
|
|
|
// memory
|
|
ipcMain.handle(IpcChannel.Memory_Add, async (_, messages, config) => {
|
|
return await memoryService.add(messages, config)
|
|
})
|
|
ipcMain.handle(IpcChannel.Memory_Search, async (_, query, config) => {
|
|
return await memoryService.search(query, config)
|
|
})
|
|
ipcMain.handle(IpcChannel.Memory_List, async (_, config) => {
|
|
return await memoryService.list(config)
|
|
})
|
|
ipcMain.handle(IpcChannel.Memory_Delete, async (_, id) => {
|
|
return await memoryService.delete(id)
|
|
})
|
|
ipcMain.handle(IpcChannel.Memory_Update, async (_, id, memory, metadata) => {
|
|
return await memoryService.update(id, memory, metadata)
|
|
})
|
|
ipcMain.handle(IpcChannel.Memory_Get, async (_, memoryId) => {
|
|
return await memoryService.get(memoryId)
|
|
})
|
|
ipcMain.handle(IpcChannel.Memory_SetConfig, async (_, config) => {
|
|
memoryService.setConfig(config)
|
|
})
|
|
ipcMain.handle(IpcChannel.Memory_DeleteUser, async (_, userId) => {
|
|
return await memoryService.deleteUser(userId)
|
|
})
|
|
ipcMain.handle(IpcChannel.Memory_DeleteAllMemoriesForUser, async (_, userId) => {
|
|
return await memoryService.deleteAllMemoriesForUser(userId)
|
|
})
|
|
ipcMain.handle(IpcChannel.Memory_GetUsersList, async () => {
|
|
return await memoryService.getUsersList()
|
|
})
|
|
|
|
// window
|
|
ipcMain.handle(IpcChannel.Windows_SetMinimumSize, (_, width: number, height: number) => {
|
|
checkMainWindow()
|
|
mainWindow.setMinimumSize(width, height)
|
|
})
|
|
|
|
ipcMain.handle(IpcChannel.Windows_ResetMinimumSize, () => {
|
|
checkMainWindow()
|
|
|
|
mainWindow.setMinimumSize(MIN_WINDOW_WIDTH, MIN_WINDOW_HEIGHT)
|
|
const [width, height] = mainWindow.getSize() ?? [MIN_WINDOW_WIDTH, MIN_WINDOW_HEIGHT]
|
|
if (width < MIN_WINDOW_WIDTH) {
|
|
mainWindow.setSize(MIN_WINDOW_WIDTH, height)
|
|
}
|
|
})
|
|
|
|
ipcMain.handle(IpcChannel.Windows_GetSize, () => {
|
|
checkMainWindow()
|
|
const [width, height] = mainWindow.getSize() ?? [MIN_WINDOW_WIDTH, MIN_WINDOW_HEIGHT]
|
|
return [width, height]
|
|
})
|
|
|
|
// Window Controls
|
|
ipcMain.handle(IpcChannel.Windows_Minimize, () => {
|
|
checkMainWindow()
|
|
mainWindow.minimize()
|
|
})
|
|
|
|
ipcMain.handle(IpcChannel.Windows_Maximize, () => {
|
|
checkMainWindow()
|
|
mainWindow.maximize()
|
|
})
|
|
|
|
ipcMain.handle(IpcChannel.Windows_Unmaximize, () => {
|
|
checkMainWindow()
|
|
mainWindow.unmaximize()
|
|
})
|
|
|
|
ipcMain.handle(IpcChannel.Windows_Close, () => {
|
|
checkMainWindow()
|
|
mainWindow.close()
|
|
})
|
|
|
|
ipcMain.handle(IpcChannel.Windows_IsMaximized, () => {
|
|
checkMainWindow()
|
|
return mainWindow.isMaximized()
|
|
})
|
|
|
|
// Send maximized state changes to renderer
|
|
mainWindow.on('maximize', () => {
|
|
mainWindow.webContents.send(IpcChannel.Windows_MaximizedChanged, true)
|
|
})
|
|
|
|
mainWindow.on('unmaximize', () => {
|
|
mainWindow.webContents.send(IpcChannel.Windows_MaximizedChanged, false)
|
|
})
|
|
|
|
// VertexAI
|
|
ipcMain.handle(IpcChannel.VertexAI_GetAuthHeaders, async (_, params) => {
|
|
return vertexAIService.getAuthHeaders(params)
|
|
})
|
|
|
|
ipcMain.handle(IpcChannel.VertexAI_GetAccessToken, async (_, params) => {
|
|
return vertexAIService.getAccessToken(params)
|
|
})
|
|
|
|
ipcMain.handle(IpcChannel.VertexAI_ClearAuthCache, async (_, projectId: string, clientEmail?: string) => {
|
|
vertexAIService.clearAuthCache(projectId, clientEmail)
|
|
})
|
|
|
|
// mini window
|
|
ipcMain.handle(IpcChannel.MiniWindow_Show, () => windowService.showMiniWindow())
|
|
ipcMain.handle(IpcChannel.MiniWindow_Hide, () => windowService.hideMiniWindow())
|
|
ipcMain.handle(IpcChannel.MiniWindow_Close, () => windowService.closeMiniWindow())
|
|
ipcMain.handle(IpcChannel.MiniWindow_Toggle, () => windowService.toggleMiniWindow())
|
|
ipcMain.handle(IpcChannel.MiniWindow_SetPin, (_, isPinned) => windowService.setPinMiniWindow(isPinned))
|
|
|
|
// aes
|
|
ipcMain.handle(IpcChannel.Aes_Encrypt, (_, text: string, secretKey: string, iv: string) =>
|
|
encrypt(text, secretKey, iv)
|
|
)
|
|
ipcMain.handle(IpcChannel.Aes_Decrypt, (_, encryptedData: string, iv: string, secretKey: string) =>
|
|
decrypt(encryptedData, iv, secretKey)
|
|
)
|
|
|
|
// Register MCP handlers
|
|
ipcMain.handle(IpcChannel.Mcp_RemoveServer, mcpService.removeServer)
|
|
ipcMain.handle(IpcChannel.Mcp_RestartServer, mcpService.restartServer)
|
|
ipcMain.handle(IpcChannel.Mcp_StopServer, mcpService.stopServer)
|
|
ipcMain.handle(IpcChannel.Mcp_ListTools, mcpService.listTools)
|
|
ipcMain.handle(IpcChannel.Mcp_CallTool, mcpService.callTool)
|
|
ipcMain.handle(IpcChannel.Mcp_ListPrompts, mcpService.listPrompts)
|
|
ipcMain.handle(IpcChannel.Mcp_GetPrompt, mcpService.getPrompt)
|
|
ipcMain.handle(IpcChannel.Mcp_ListResources, mcpService.listResources)
|
|
ipcMain.handle(IpcChannel.Mcp_GetResource, mcpService.getResource)
|
|
ipcMain.handle(IpcChannel.Mcp_GetInstallInfo, mcpService.getInstallInfo)
|
|
ipcMain.handle(IpcChannel.Mcp_CheckConnectivity, mcpService.checkMcpConnectivity)
|
|
ipcMain.handle(IpcChannel.Mcp_AbortTool, mcpService.abortTool)
|
|
ipcMain.handle(IpcChannel.Mcp_GetServerVersion, mcpService.getServerVersion)
|
|
|
|
// DXT upload handler
|
|
ipcMain.handle(IpcChannel.Mcp_UploadDxt, async (event, fileBuffer: ArrayBuffer, fileName: string) => {
|
|
try {
|
|
// Create a temporary file with the uploaded content
|
|
const tempPath = await fileManager.createTempFile(event, fileName)
|
|
await fileManager.writeFile(event, tempPath, Buffer.from(fileBuffer))
|
|
|
|
// Process DXT file using the temporary path
|
|
return await dxtService.uploadDxt(event, tempPath)
|
|
} catch (error) {
|
|
logger.error('DXT upload error:', error as Error)
|
|
return {
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Failed to upload DXT file'
|
|
}
|
|
}
|
|
})
|
|
|
|
// Register Python execution handler
|
|
ipcMain.handle(
|
|
IpcChannel.Python_Execute,
|
|
async (_, script: string, context?: Record<string, any>, timeout?: number) => {
|
|
return await pythonService.executeScript(script, context, timeout)
|
|
}
|
|
)
|
|
|
|
ipcMain.handle(IpcChannel.App_IsBinaryExist, (_, name: string) => isBinaryExists(name))
|
|
ipcMain.handle(IpcChannel.App_GetBinaryPath, (_, name: string) => getBinaryPath(name))
|
|
ipcMain.handle(IpcChannel.App_InstallUvBinary, () => runInstallScript('install-uv.js'))
|
|
ipcMain.handle(IpcChannel.App_InstallBunBinary, () => runInstallScript('install-bun.js'))
|
|
ipcMain.handle(IpcChannel.App_InstallOvmsBinary, () => runInstallScript('install-ovms.js'))
|
|
|
|
//copilot
|
|
ipcMain.handle(IpcChannel.Copilot_GetAuthMessage, CopilotService.getAuthMessage.bind(CopilotService))
|
|
ipcMain.handle(IpcChannel.Copilot_GetCopilotToken, CopilotService.getCopilotToken.bind(CopilotService))
|
|
ipcMain.handle(IpcChannel.Copilot_SaveCopilotToken, CopilotService.saveCopilotToken.bind(CopilotService))
|
|
ipcMain.handle(IpcChannel.Copilot_GetToken, CopilotService.getToken.bind(CopilotService))
|
|
ipcMain.handle(IpcChannel.Copilot_Logout, CopilotService.logout.bind(CopilotService))
|
|
ipcMain.handle(IpcChannel.Copilot_GetUser, CopilotService.getUser.bind(CopilotService))
|
|
|
|
// Obsidian service
|
|
ipcMain.handle(IpcChannel.Obsidian_GetVaults, () => {
|
|
return obsidianVaultService.getVaults()
|
|
})
|
|
|
|
ipcMain.handle(IpcChannel.Obsidian_GetFiles, (_event, vaultName) => {
|
|
return obsidianVaultService.getFilesByVaultName(vaultName)
|
|
})
|
|
|
|
// nutstore
|
|
ipcMain.handle(IpcChannel.Nutstore_GetSsoUrl, NutstoreService.getNutstoreSSOUrl.bind(NutstoreService))
|
|
ipcMain.handle(IpcChannel.Nutstore_DecryptToken, (_, token: string) => NutstoreService.decryptToken(token))
|
|
ipcMain.handle(IpcChannel.Nutstore_GetDirectoryContents, (_, token: string, path: string) =>
|
|
NutstoreService.getDirectoryContents(token, path)
|
|
)
|
|
|
|
// search window
|
|
ipcMain.handle(IpcChannel.SearchWindow_Open, async (_, uid: string) => {
|
|
await searchService.openSearchWindow(uid)
|
|
})
|
|
ipcMain.handle(IpcChannel.SearchWindow_Close, async (_, uid: string) => {
|
|
await searchService.closeSearchWindow(uid)
|
|
})
|
|
ipcMain.handle(IpcChannel.SearchWindow_OpenUrl, async (_, uid: string, url: string) => {
|
|
return await searchService.openUrlInSearchWindow(uid, url)
|
|
})
|
|
|
|
// webview
|
|
ipcMain.handle(IpcChannel.Webview_SetOpenLinkExternal, (_, webviewId: number, isExternal: boolean) =>
|
|
setOpenLinkExternal(webviewId, isExternal)
|
|
)
|
|
ipcMain.handle(IpcChannel.Webview_SetSpellCheckEnabled, (_, webviewId: number, isEnable: boolean) => {
|
|
const webview = webContents.fromId(webviewId)
|
|
if (!webview) return
|
|
webview.session.setSpellCheckerEnabled(isEnable)
|
|
})
|
|
|
|
// store sync
|
|
storeSyncService.registerIpcHandler()
|
|
|
|
// selection assistant
|
|
SelectionService.registerIpcHandler()
|
|
|
|
ipcMain.handle(IpcChannel.App_QuoteToMain, (_, text: string) => windowService.quoteToMainWindow(text))
|
|
|
|
ipcMain.handle(IpcChannel.App_SetDisableHardwareAcceleration, (_, isDisable: boolean) => {
|
|
configManager.setDisableHardwareAcceleration(isDisable)
|
|
})
|
|
ipcMain.handle(IpcChannel.TRACE_SAVE_DATA, (_, topicId: string) => saveSpans(topicId))
|
|
ipcMain.handle(IpcChannel.TRACE_GET_DATA, (_, topicId: string, traceId: string, modelName?: string) =>
|
|
getSpans(topicId, traceId, modelName)
|
|
)
|
|
ipcMain.handle(IpcChannel.TRACE_SAVE_ENTITY, (_, entity: SpanEntity) => saveEntity(entity))
|
|
ipcMain.handle(IpcChannel.TRACE_GET_ENTITY, (_, spanId: string) => getEntity(spanId))
|
|
ipcMain.handle(IpcChannel.TRACE_BIND_TOPIC, (_, topicId: string, traceId: string) => bindTopic(traceId, topicId))
|
|
ipcMain.handle(IpcChannel.TRACE_CLEAN_TOPIC, (_, topicId: string, traceId?: string) => cleanTopic(topicId, traceId))
|
|
ipcMain.handle(IpcChannel.TRACE_TOKEN_USAGE, (_, spanId: string, usage: TokenUsage) => tokenUsage(spanId, usage))
|
|
ipcMain.handle(IpcChannel.TRACE_CLEAN_HISTORY, (_, topicId: string, traceId: string, modelName?: string) =>
|
|
cleanHistoryTrace(topicId, traceId, modelName)
|
|
)
|
|
ipcMain.handle(
|
|
IpcChannel.TRACE_OPEN_WINDOW,
|
|
(_, topicId: string, traceId: string, autoOpen?: boolean, modelName?: string) =>
|
|
openTraceWindow(topicId, traceId, autoOpen, modelName)
|
|
)
|
|
ipcMain.handle(IpcChannel.TRACE_SET_TITLE, (_, title: string) => setTraceWindowTitle(title))
|
|
ipcMain.handle(IpcChannel.TRACE_ADD_END_MESSAGE, (_, spanId: string, modelName: string, message: string) =>
|
|
addEndMessage(spanId, modelName, message)
|
|
)
|
|
ipcMain.handle(IpcChannel.TRACE_CLEAN_LOCAL_DATA, () => cleanLocalData())
|
|
ipcMain.handle(
|
|
IpcChannel.TRACE_ADD_STREAM_MESSAGE,
|
|
(_, spanId: string, modelName: string, context: string, msg: any) =>
|
|
addStreamMessage(spanId, modelName, context, msg)
|
|
)
|
|
|
|
ipcMain.handle(IpcChannel.App_GetDiskInfo, async (_, directoryPath: string) => {
|
|
try {
|
|
const diskSpace = await checkDiskSpace(directoryPath) // { free, size } in bytes
|
|
logger.debug('disk space', diskSpace)
|
|
const { free, size } = diskSpace
|
|
return {
|
|
free,
|
|
size
|
|
}
|
|
} catch (error) {
|
|
logger.error('check disk space error', error as Error)
|
|
return null
|
|
}
|
|
})
|
|
// API Server
|
|
apiServerService.registerIpcHandlers()
|
|
|
|
// Anthropic OAuth
|
|
ipcMain.handle(IpcChannel.Anthropic_StartOAuthFlow, () => anthropicService.startOAuthFlow())
|
|
ipcMain.handle(IpcChannel.Anthropic_CompleteOAuthWithCode, (_, code: string) =>
|
|
anthropicService.completeOAuthWithCode(code)
|
|
)
|
|
ipcMain.handle(IpcChannel.Anthropic_CancelOAuthFlow, () => anthropicService.cancelOAuthFlow())
|
|
ipcMain.handle(IpcChannel.Anthropic_GetAccessToken, () => anthropicService.getValidAccessToken())
|
|
ipcMain.handle(IpcChannel.Anthropic_HasCredentials, () => anthropicService.hasCredentials())
|
|
ipcMain.handle(IpcChannel.Anthropic_ClearCredentials, () => anthropicService.clearCredentials())
|
|
|
|
// CodeTools
|
|
ipcMain.handle(IpcChannel.CodeTools_Run, codeToolsService.run)
|
|
ipcMain.handle(IpcChannel.CodeTools_GetAvailableTerminals, () => codeToolsService.getAvailableTerminalsForPlatform())
|
|
ipcMain.handle(IpcChannel.CodeTools_SetCustomTerminalPath, (_, terminalId: string, path: string) =>
|
|
codeToolsService.setCustomTerminalPath(terminalId, path)
|
|
)
|
|
ipcMain.handle(IpcChannel.CodeTools_GetCustomTerminalPath, (_, terminalId: string) =>
|
|
codeToolsService.getCustomTerminalPath(terminalId)
|
|
)
|
|
ipcMain.handle(IpcChannel.CodeTools_RemoveCustomTerminalPath, (_, terminalId: string) =>
|
|
codeToolsService.removeCustomTerminalPath(terminalId)
|
|
)
|
|
|
|
// OCR
|
|
ipcMain.handle(IpcChannel.OCR_ocr, (_, file: SupportedOcrFile, provider: OcrProvider) =>
|
|
ocrService.ocr(file, provider)
|
|
)
|
|
ipcMain.handle(IpcChannel.OCR_ListProviders, () => ocrService.listProviderIds())
|
|
|
|
// OVMS
|
|
ipcMain.handle(IpcChannel.Ovms_AddModel, (_, modelName: string, modelId: string, modelSource: string, task: string) =>
|
|
ovmsManager.addModel(modelName, modelId, modelSource, task)
|
|
)
|
|
ipcMain.handle(IpcChannel.Ovms_StopAddModel, () => ovmsManager.stopAddModel())
|
|
ipcMain.handle(IpcChannel.Ovms_GetModels, () => ovmsManager.getModels())
|
|
ipcMain.handle(IpcChannel.Ovms_IsRunning, () => ovmsManager.initializeOvms())
|
|
ipcMain.handle(IpcChannel.Ovms_GetStatus, () => ovmsManager.getOvmsStatus())
|
|
ipcMain.handle(IpcChannel.Ovms_RunOVMS, () => ovmsManager.runOvms())
|
|
ipcMain.handle(IpcChannel.Ovms_StopOVMS, () => ovmsManager.stopOvms())
|
|
|
|
// CherryAI
|
|
ipcMain.handle(IpcChannel.Cherryai_GetSignature, (_, params) => generateSignature(params))
|
|
|
|
// Claude Code Plugins
|
|
ipcMain.handle(IpcChannel.ClaudeCodePlugin_ListAvailable, async () => {
|
|
try {
|
|
const data = await pluginService.listAvailable()
|
|
return { success: true, data }
|
|
} catch (error) {
|
|
const pluginError = extractPluginError(error)
|
|
if (pluginError) {
|
|
logger.error('Failed to list available plugins', pluginError)
|
|
return { success: false, error: pluginError }
|
|
}
|
|
|
|
const err = normalizeError(error)
|
|
logger.error('Failed to list available plugins', err)
|
|
return {
|
|
success: false,
|
|
error: {
|
|
type: 'TRANSACTION_FAILED',
|
|
operation: 'list-available',
|
|
reason: err.message
|
|
}
|
|
}
|
|
}
|
|
})
|
|
|
|
ipcMain.handle(IpcChannel.ClaudeCodePlugin_Install, async (_, options) => {
|
|
try {
|
|
const data = await pluginService.install(options)
|
|
return { success: true, data }
|
|
} catch (error) {
|
|
logger.error('Failed to install plugin', { options, error })
|
|
return { success: false, error }
|
|
}
|
|
})
|
|
|
|
ipcMain.handle(IpcChannel.ClaudeCodePlugin_Uninstall, async (_, options) => {
|
|
try {
|
|
await pluginService.uninstall(options)
|
|
return { success: true, data: undefined }
|
|
} catch (error) {
|
|
logger.error('Failed to uninstall plugin', { options, error })
|
|
return { success: false, error }
|
|
}
|
|
})
|
|
|
|
ipcMain.handle(IpcChannel.ClaudeCodePlugin_ListInstalled, async (_, agentId: string) => {
|
|
try {
|
|
const data = await pluginService.listInstalled(agentId)
|
|
return { success: true, data }
|
|
} catch (error) {
|
|
const pluginError = extractPluginError(error)
|
|
if (pluginError) {
|
|
logger.error('Failed to list installed plugins', { agentId, error: pluginError })
|
|
return { success: false, error: pluginError }
|
|
}
|
|
|
|
const err = normalizeError(error)
|
|
logger.error('Failed to list installed plugins', { agentId, error: err })
|
|
return {
|
|
success: false,
|
|
error: {
|
|
type: 'TRANSACTION_FAILED',
|
|
operation: 'list-installed',
|
|
reason: err.message
|
|
}
|
|
}
|
|
}
|
|
})
|
|
|
|
ipcMain.handle(IpcChannel.ClaudeCodePlugin_InvalidateCache, async () => {
|
|
try {
|
|
pluginService.invalidateCache()
|
|
return { success: true, data: undefined }
|
|
} catch (error) {
|
|
const pluginError = extractPluginError(error)
|
|
if (pluginError) {
|
|
logger.error('Failed to invalidate plugin cache', pluginError)
|
|
return { success: false, error: pluginError }
|
|
}
|
|
|
|
const err = normalizeError(error)
|
|
logger.error('Failed to invalidate plugin cache', err)
|
|
return {
|
|
success: false,
|
|
error: {
|
|
type: 'TRANSACTION_FAILED',
|
|
operation: 'invalidate-cache',
|
|
reason: err.message
|
|
}
|
|
}
|
|
}
|
|
})
|
|
|
|
ipcMain.handle(IpcChannel.ClaudeCodePlugin_ReadContent, async (_, sourcePath: string) => {
|
|
try {
|
|
const data = await pluginService.readContent(sourcePath)
|
|
return { success: true, data }
|
|
} catch (error) {
|
|
logger.error('Failed to read plugin content', { sourcePath, error })
|
|
return { success: false, error }
|
|
}
|
|
})
|
|
|
|
ipcMain.handle(IpcChannel.ClaudeCodePlugin_WriteContent, async (_, options) => {
|
|
try {
|
|
await pluginService.writeContent(options.agentId, options.filename, options.type, options.content)
|
|
return { success: true, data: undefined }
|
|
} catch (error) {
|
|
logger.error('Failed to write plugin content', { options, error })
|
|
return { success: false, error }
|
|
}
|
|
})
|
|
}
|