diff --git a/adr/adr-008-Extensible-Jan-with-Docker.md b/adr/adr-008-Extensible-Jan-with-Docker.md new file mode 100644 index 0000000000..7b13864a2d --- /dev/null +++ b/adr/adr-008-Extensible-Jan-with-Docker.md @@ -0,0 +1,36 @@ +# ADR 008: Extensible-Jan-with-Docker + +## Changelog + +- 2023-10-24: Initial draft + +## Authors + +- @vuonghoainam + +## Status +Proposed + +## Context + +What is the issue that we're seeing that is motivating this decision or change? +- The A.I world is moving fast with multiple runtime/ prebaked environment. We or the builder cannot cover just everything but rather we should adopt it and facillitate it as much as possible within Jan. +- For `Run your own A.I`: Builder can build app on Jan (NodeJS env) and connect to external endpoint which serves the real A.I + - e.g 1: Nitro acting as proxy to `triton-inference-server` running within a Docker container controlled by Jan app + - e.g 2: Original models can be in many formats (pytorch, paddlepaddle). In order to run it with the most optimized version locally, there must be a step to transpile the model ([Ollama import model](https://github.com/jmorganca/ollama/blob/main/docs/import.md), Tensorrt). Btw Jan can prebuilt it and let user pull but later +- For `Build your own A.I`: User can fine tune model locally (of course Jan help it with remote but later) + +## Decision + +What is the change that we're proposing and/or doing? +- Add Docker client as Core module - [Docker node](https://github.com/apocas/dockerode) +- 2 example A.I app (adr-002) to demonstrate it and actually use! + +## Consequences + +What becomes easier or more difficult to do because of this change? +- We can extend limitlessly :D + +## Alternatives + +## Reference diff --git a/electron/tests/my-models.e2e.spec.ts b/electron/tests/my-models.e2e.spec.ts index 627612ea87..788b80fc07 100644 --- a/electron/tests/my-models.e2e.spec.ts +++ b/electron/tests/my-models.e2e.spec.ts @@ -36,11 +36,6 @@ test.afterAll(async () => { test("shows my models", async () => { await page.getByTestId("My Models").first().click(); - const header = await page - .getByRole("heading") - .filter({ hasText: "My Models" }) - .first() - .isVisible(); - expect(header).toBe(false); + await page.getByTestId("testid-mymodels-header").isVisible(); // More test cases here... }); diff --git a/electron/tsconfig.json b/electron/tsconfig.json index 1880eed7c3..8276542b2f 100644 --- a/electron/tsconfig.json +++ b/electron/tsconfig.json @@ -10,9 +10,10 @@ "noEmitOnError": true, "baseUrl": ".", "allowJs": true, + "skipLibCheck": true, "paths": { "*": ["node_modules/*"] }, "typeRoots": ["node_modules/@types"] }, "include": ["./**/*.ts"], - "exclude": ["core", "build", "dist", "tests"] + "exclude": ["core", "build", "dist", "tests", "node_modules"] } diff --git a/plugins/inference-plugin/index.ts b/plugins/inference-plugin/index.ts index 04ed4b0f0c..525e7e009b 100644 --- a/plugins/inference-plugin/index.ts +++ b/plugins/inference-plugin/index.ts @@ -67,7 +67,7 @@ function requestInference( } subscriber.complete(); }) - .catch(subscriber.error); + .catch((err) => subscriber.error(err)); }); } @@ -143,7 +143,8 @@ async function handleMessageRequest(data: NewMessageRequest) { }, error: async (err) => { message.message = - message.message.trim() + "\n" + "Error occurred: " + err; + message.message.trim() + "\n" + "Error occurred: " + err.message; + events.emit(EventName.OnMessageResponseUpdate, message); // TODO: Common collections should be able to access via core functions instead of store await store.updateOne("messages", message._id, message); }, diff --git a/plugins/inference-plugin/module.ts b/plugins/inference-plugin/module.ts index 0ddddc37e8..b851f0c4e0 100644 --- a/plugins/inference-plugin/module.ts +++ b/plugins/inference-plugin/module.ts @@ -14,53 +14,49 @@ const initModel = (fileName) => { if (!fileName) { reject("Model not found, please download again."); } - if (subprocess) { - console.error("A subprocess is already running. Attempt to kill then reinit."); - killSubprocess(); - } resolve(fileName); }) - // Kill port process if it is already in use - .then((fileName) => - tcpPortUsed - .waitUntilFree(PORT, 200, 3000) - .catch(() => killPortProcess(PORT)) - .then(() => fileName) - ) // Spawn Nitro subprocess to load model .then(() => { - let binaryFolder = path.join(__dirname, "nitro"); // Current directory by default - let binaryName; + return tcpPortUsed.check(PORT, "127.0.0.1").then((inUse) => { + if (!inUse) { + let binaryFolder = path.join(__dirname, "nitro"); // Current directory by default + let binaryName; - if (process.platform === "win32") { - // Todo: Need to check for CUDA support to switch between CUDA and non-CUDA binaries - binaryName = "nitro_start_windows.bat"; - } else if (process.platform === "darwin") { - // Mac OS platform - binaryName = process.arch === "arm64" ? "nitro_mac_arm64" : "nitro_mac_intel"; - } else { - // Linux - // Todo: Need to check for CUDA support to switch between CUDA and non-CUDA binaries - binaryName = "nitro_start_linux.sh"; // For other platforms - } + if (process.platform === "win32") { + // Todo: Need to check for CUDA support to switch between CUDA and non-CUDA binaries + binaryName = "nitro_start_windows.bat"; + } else if (process.platform === "darwin") { + // Mac OS platform + binaryName = + process.arch === "arm64" + ? "nitro_mac_arm64" + : "nitro_mac_intel"; + } else { + // Linux + // Todo: Need to check for CUDA support to switch between CUDA and non-CUDA binaries + binaryName = "nitro_start_linux.sh"; // For other platforms + } - const binaryPath = path.join(binaryFolder, binaryName); + const binaryPath = path.join(binaryFolder, binaryName); - // Execute the binary - subprocess = spawn(binaryPath, { cwd: binaryFolder }); + // Execute the binary + subprocess = spawn(binaryPath, { cwd: binaryFolder }); - // Handle subprocess output - subprocess.stdout.on("data", (data) => { - console.log(`stdout: ${data}`); - }); + // Handle subprocess output + subprocess.stdout.on("data", (data) => { + console.log(`stdout: ${data}`); + }); - subprocess.stderr.on("data", (data) => { - console.error(`stderr: ${data}`); - }); + subprocess.stderr.on("data", (data) => { + console.error(`stderr: ${data}`); + }); - subprocess.on("close", (code) => { - console.log(`child process exited with code ${code}`); - subprocess = null; + subprocess.on("close", (code) => { + console.log(`child process exited with code ${code}`); + subprocess = null; + }); + } }); }) .then(() => tcpPortUsed.waitUntilUsed(PORT, 300, 30000)) diff --git a/plugins/inference-plugin/package.json b/plugins/inference-plugin/package.json index c13f7bafe7..9d2fbd50b1 100644 --- a/plugins/inference-plugin/package.json +++ b/plugins/inference-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@janhq/inference-plugin", - "version": "1.0.13", + "version": "1.0.14", "description": "Inference Plugin, powered by @janhq/nitro, bring a high-performance Llama model inference in pure C++.", "icon": "https://raw.githubusercontent.com/tailwindlabs/heroicons/88e98b0c2b458553fbadccddc2d2f878edc0387b/src/20/solid/command-line.svg", "main": "dist/index.js", diff --git a/web/app/_components/ConversationalList/index.tsx b/web/app/_components/ConversationalList/index.tsx index 6a49318fb4..827874923b 100644 --- a/web/app/_components/ConversationalList/index.tsx +++ b/web/app/_components/ConversationalList/index.tsx @@ -14,7 +14,7 @@ const ConversationalList: React.FC = ({ models }) => (
- {models.map((item) => ( + {models?.map((item) => ( ))}
diff --git a/web/app/_components/CreateBotContainer/index.tsx b/web/app/_components/CreateBotContainer/index.tsx index 52111e21f1..2ba2946adc 100644 --- a/web/app/_components/CreateBotContainer/index.tsx +++ b/web/app/_components/CreateBotContainer/index.tsx @@ -13,15 +13,14 @@ import DraggableProgressBar from '../DraggableProgressBar' import { useSetAtom } from 'jotai' import { activeBotAtom } from '@helpers/atoms/Bot.atom' import { - leftSideBarExpandStateAtom, rightSideBarExpandStateAtom, } from '@helpers/atoms/SideBarExpand.atom' import { MainViewState, setMainViewStateAtom, } from '@helpers/atoms/MainView.atom' -import { executeSerial } from '../../../../electron/core/plugin-manager/execution/extension-manager' import { DataService } from '@janhq/core' +import { executeSerial } from '@services/pluginService' const CreateBotContainer: React.FC = () => { const { downloadedModels } = useGetDownloadedModels() diff --git a/web/app/_components/HistoryItem/index.tsx b/web/app/_components/HistoryItem/index.tsx index 118ebf5011..ebf0a4f847 100644 --- a/web/app/_components/HistoryItem/index.tsx +++ b/web/app/_components/HistoryItem/index.tsx @@ -1,7 +1,6 @@ import React from 'react' import { useAtomValue, useSetAtom } from 'jotai' import { ModelManagementService } from '@janhq/core' -import { executeSerial } from '../../../../electron/core/plugin-manager/execution/extension-manager' import { getActiveConvoIdAtom, setActiveConvoIdAtom, @@ -13,6 +12,7 @@ import { } from '@helpers/atoms/MainView.atom' import { displayDate } from '@utils/datetime' import { twMerge } from 'tailwind-merge' +import { executeSerial } from '@services/pluginService' type Props = { conversation: Conversation diff --git a/web/app/_components/LeftRibbonNav/index.tsx b/web/app/_components/LeftRibbonNav/index.tsx index 327cd730e1..c315d06820 100644 --- a/web/app/_components/LeftRibbonNav/index.tsx +++ b/web/app/_components/LeftRibbonNav/index.tsx @@ -59,7 +59,7 @@ const LeftRibbonNav: React.FC = () => { const onBotListClick = async () => { const bots = await getAllBots() - if (bots.length === 0) { + if (bots?.length === 0) { alert('You have no bot') return } diff --git a/web/app/_components/ModelTable/index.tsx b/web/app/_components/ModelTable/index.tsx index eaa6273f21..c3f0d96cf8 100644 --- a/web/app/_components/ModelTable/index.tsx +++ b/web/app/_components/ModelTable/index.tsx @@ -20,7 +20,7 @@ const ModelTable: React.FC = ({ models }) => ( - {models.map((model) => ( + {models?.map((model) => ( ))} diff --git a/web/containers/BottomBar/index.tsx b/web/containers/BottomBar/index.tsx index 6526b71372..e10c14d29c 100644 --- a/web/containers/BottomBar/index.tsx +++ b/web/containers/BottomBar/index.tsx @@ -5,12 +5,14 @@ import { useAtomValue } from 'jotai' import { modelDownloadStateAtom } from '@helpers/atoms/DownloadState.atom' import { formatDownloadPercentage } from '@utils/converter' import { activeAssistantModelAtom, stateModel } from '@helpers/atoms/Model.atom' +import useGetAppVersion from '@hooks/useGetAppVersion' const BottomBar = () => { const activeModel = useAtomValue(activeAssistantModelAtom) const stateModelStartStop = useAtomValue(stateModel) const { ram, cpu } = useGetSystemResources() const modelDownloadStates = useAtomValue(modelDownloadStateAtom) + const appVersion = useGetAppVersion() const downloadStates: DownloadState[] = [] for (const [, value] of Object.entries(modelDownloadStates)) { @@ -51,7 +53,7 @@ const BottomBar = () => {
-

Jan v0.2.0

+

Jan {appVersion?.version ?? ''}

) diff --git a/web/containers/Providers/index.tsx b/web/containers/Providers/index.tsx index 72a24b9e1c..e0d28a8b85 100644 --- a/web/containers/Providers/index.tsx +++ b/web/containers/Providers/index.tsx @@ -13,13 +13,9 @@ import { activationPoints, extensionPoints, } from '../../../electron/core/plugin-manager/execution/index' -import { - isCorePluginInstalled, - setupBasePlugins, -} from '@services/pluginService' import EventListenerWrapper from '@helpers/EventListenerWrapper' import { setupCoreServices } from '@services/coreService' -import { executeSerial } from '../../../electron/core/plugin-manager/execution/extension-manager' +import { executeSerial, isCorePluginInstalled, setupBasePlugins } from '@services/pluginService' const Providers = (props: PropsWithChildren) => { const [setupCore, setSetupCore] = useState(false) diff --git a/web/containers/Sidebar/Left.tsx b/web/containers/Sidebar/Left.tsx index be2d9c0b95..e837058f31 100644 --- a/web/containers/Sidebar/Left.tsx +++ b/web/containers/Sidebar/Left.tsx @@ -36,8 +36,8 @@ export const SidebarLeft = () => { const onBotListClick = async () => { const bots = await getAllBots() - if (bots.length === 0) { - alert('You have no bot') + if (!bots || bots?.length === 0) { + alert('You have not created any bot') return } diff --git a/web/helpers/EventListenerWrapper.tsx b/web/helpers/EventListenerWrapper.tsx index d15c95d13d..a75a9bb689 100644 --- a/web/helpers/EventListenerWrapper.tsx +++ b/web/helpers/EventListenerWrapper.tsx @@ -3,7 +3,6 @@ import { useSetAtom } from 'jotai' import { ReactNode, useEffect } from 'react' import { appDownloadProgress } from './JotaiWrapper' -import { executeSerial } from '../../electron/core/plugin-manager/execution/extension-manager' import { ModelManagementService } from '@janhq/core' import { setDownloadStateAtom, @@ -12,6 +11,7 @@ import { import { getDownloadedModels } from '../hooks/useGetDownloadedModels' import { downloadedModelAtom } from './atoms/DownloadedModel.atom' import EventHandler from './EventHandler' +import { executeSerial } from '@services/pluginService' type Props = { children: ReactNode diff --git a/web/hooks/useCreateBot.ts b/web/hooks/useCreateBot.ts index c54db71fe3..46503de7b4 100644 --- a/web/hooks/useCreateBot.ts +++ b/web/hooks/useCreateBot.ts @@ -1,9 +1,10 @@ -import { executeSerial } from '../../electron/core/plugin-manager/execution/extension-manager' +import { DataService } from '@janhq/core' +import { executeSerial } from '@services/pluginService' export default function useCreateBot() { const createBot = async (bot: Bot) => { try { - await executeSerial('createBot', bot) + await executeSerial(DataService.CreateBot, bot) } catch (err) { alert(err) console.error(err) diff --git a/web/hooks/useDeleteBot.ts b/web/hooks/useDeleteBot.ts index e253970da8..8d7f6abed2 100644 --- a/web/hooks/useDeleteBot.ts +++ b/web/hooks/useDeleteBot.ts @@ -1,7 +1,8 @@ import { useSetAtom } from 'jotai' -import { executeSerial } from '../../electron/core/plugin-manager/execution/extension-manager' import { activeBotAtom } from '@helpers/atoms/Bot.atom' import { rightSideBarExpandStateAtom } from '@helpers/atoms/SideBarExpand.atom' +import { executeSerial } from '@services/pluginService' +import { DataService } from '@janhq/core' export default function useDeleteBot() { const setActiveBot = useSetAtom(activeBotAtom) @@ -9,7 +10,7 @@ export default function useDeleteBot() { const deleteBot = async (botId: string): Promise<'success' | 'failed'> => { try { - await executeSerial('deleteBot', botId) + await executeSerial(DataService.DeleteBot, botId) setRightPanelVisibility(false) setActiveBot(undefined) return 'success' diff --git a/web/hooks/useGetAppVersion.ts b/web/hooks/useGetAppVersion.ts index e89de231fc..cdcd12e3eb 100644 --- a/web/hooks/useGetAppVersion.ts +++ b/web/hooks/useGetAppVersion.ts @@ -8,7 +8,7 @@ export default function useGetAppVersion() { }, []) const getAppVersion = () => { - window.electronAPI.appVersion().then((version: string | undefined) => { + window.coreAPI?.appVersion().then((version: string | undefined) => { setVersion(version ?? '') }) } diff --git a/web/hooks/useGetBots.ts b/web/hooks/useGetBots.ts index aa1c02fb45..e35e76478c 100644 --- a/web/hooks/useGetBots.ts +++ b/web/hooks/useGetBots.ts @@ -1,9 +1,10 @@ -import { executeSerial } from '../../electron/core/plugin-manager/execution/extension-manager' +import { DataService } from '@janhq/core' +import { executeSerial } from '@services/pluginService' export default function useGetBots() { const getAllBots = async (): Promise => { try { - const bots = await executeSerial('getBots') + const bots = await executeSerial(DataService.GetBots) return bots } catch (err) { alert(`Failed to get bots: ${err}`) @@ -14,7 +15,7 @@ export default function useGetBots() { const getBotById = async (botId: string): Promise => { try { - const bot: Bot = await executeSerial('getBotById', botId) + const bot: Bot = await executeSerial(DataService.GetBotById, botId) return bot } catch (err) { alert(`Failed to get bot ${botId}: ${err}`) diff --git a/web/hooks/useGetDownloadedModels.ts b/web/hooks/useGetDownloadedModels.ts index d77ab810d8..1474c55a0d 100644 --- a/web/hooks/useGetDownloadedModels.ts +++ b/web/hooks/useGetDownloadedModels.ts @@ -1,8 +1,9 @@ import { useEffect } from 'react' -import { executeSerial } from '../../electron/core/plugin-manager/execution/extension-manager' import { ModelManagementService } from '@janhq/core' import { useAtom } from 'jotai' import { downloadedModelAtom } from '@helpers/atoms/DownloadedModel.atom' +import { extensionPoints } from '../../electron/core/plugin-manager/execution' +import { executeSerial } from '@services/pluginService' export function useGetDownloadedModels() { const [downloadedModels, setDownloadedModels] = useAtom(downloadedModelAtom) @@ -17,6 +18,9 @@ export function useGetDownloadedModels() { } export async function getDownloadedModels(): Promise { + if (!extensionPoints.get(ModelManagementService.GetFinishedDownloadModels)) { + return [] + } const downloadedModels: AssistantModel[] = await executeSerial( ModelManagementService.GetFinishedDownloadModels ) diff --git a/web/hooks/useGetSystemResources.ts b/web/hooks/useGetSystemResources.ts index 0e77882a56..a6f751f76a 100644 --- a/web/hooks/useGetSystemResources.ts +++ b/web/hooks/useGetSystemResources.ts @@ -1,9 +1,9 @@ import { useEffect, useState } from 'react' -import { executeSerial } from '../../electron/core/plugin-manager/execution/extension-manager' import { extensionPoints } from '../../electron/core/plugin-manager/execution' import { SystemMonitoringService } from '@janhq/core' import { useSetAtom } from 'jotai' import { totalRamAtom } from '@helpers/atoms/SystemBar.atom' +import { executeSerial } from '@services/pluginService' export default function useGetSystemResources() { const [ram, setRam] = useState(0) const [cpu, setCPU] = useState(0) diff --git a/web/hooks/useUpdateBot.ts b/web/hooks/useUpdateBot.ts index 5eff88e705..b913ab3940 100644 --- a/web/hooks/useUpdateBot.ts +++ b/web/hooks/useUpdateBot.ts @@ -1,4 +1,5 @@ -import { executeSerial } from '../../electron/core/plugin-manager/execution/extension-manager' +import { DataService } from '@janhq/core' +import { executeSerial } from '@services/pluginService' export default function useUpdateBot() { const updateBot = async ( @@ -15,7 +16,7 @@ export default function useUpdateBot() { } } - await executeSerial('updateBot', bot) + await executeSerial(DataService.UpdateBot, bot) console.debug('Bot updated', JSON.stringify(bot, null, 2)) } catch (err) { alert(`Update bot error: ${err}`) diff --git a/web/next.config.js b/web/next.config.js index dde766300d..318b3108b0 100644 --- a/web/next.config.js +++ b/web/next.config.js @@ -21,7 +21,14 @@ const nextConfig = { // do some stuff here config.optimization.minimize = false config.optimization.minimizer = [] - config.plugins = [...config.plugins, new webpack.DefinePlugin({})] + config.plugins = [ + ...config.plugins, + new webpack.DefinePlugin({ + PLUGIN_CATALOG: JSON.stringify( + 'https://cdn.jsdelivr.net/npm/@janhq/plugin-catalog@latest/dist/index.js' + ), + }), + ] return config }, } diff --git a/web/screens/ExploreModels/ExploreModelList/index.tsx b/web/screens/ExploreModels/ExploreModelList/index.tsx index 6c3acee710..0f5f08da83 100644 --- a/web/screens/ExploreModels/ExploreModelList/index.tsx +++ b/web/screens/ExploreModels/ExploreModelList/index.tsx @@ -12,7 +12,7 @@ const ExploreModelList: React.FC = () => { return (
- {models.map((item) => ( + {models?.map((item) => ( ))}
diff --git a/web/screens/MyModels/index.tsx b/web/screens/MyModels/index.tsx index 22b6fe2d4b..19ccf76075 100644 --- a/web/screens/MyModels/index.tsx +++ b/web/screens/MyModels/index.tsx @@ -65,7 +65,7 @@ const MyModelsScreen = () => { return (
-

My Models

+

My Models

You have {downloadedModels.length} models downloaded

diff --git a/web/screens/Settings/CorePlugins/PluginsCatalog.tsx b/web/screens/Settings/CorePlugins/PluginsCatalog.tsx index 17c23b154e..8bfad63e35 100644 --- a/web/screens/Settings/CorePlugins/PluginsCatalog.tsx +++ b/web/screens/Settings/CorePlugins/PluginsCatalog.tsx @@ -25,9 +25,17 @@ const PluginCatalog = () => { * Loads the plugin catalog module from a CDN and sets it as the plugin catalog state. */ useEffect(() => { - executeSerial(DataService.GetPluginManifest).then((data) => { - setPluginCatalog(data) - }) + // Load plugin manifest from plugin if any + if (extensionPoints.get(DataService.GetPluginManifest)) { + executeSerial(DataService.GetPluginManifest).then((data) => { + setPluginCatalog(data) + }) + } else { + // Fallback to app default manifest + import( + /* webpackIgnore: true */ PLUGIN_CATALOG + `?t=${Date.now()}` + ).then((data) => setPluginCatalog(data.default)) + } }, []) /** diff --git a/web/screens/Settings/CorePlugins/PreferencePlugins/index.tsx b/web/screens/Settings/CorePlugins/PreferencePlugins/index.tsx index 841acc38a2..fbf5dc625b 100644 --- a/web/screens/Settings/CorePlugins/PreferencePlugins/index.tsx +++ b/web/screens/Settings/CorePlugins/PreferencePlugins/index.tsx @@ -1,4 +1,4 @@ -import React, { Children } from 'react' +import React from 'react' import { execute } from '../../../../../electron/core/plugin-manager/execution/extension-manager' type Props = { diff --git a/web/services/pluginService.ts b/web/services/pluginService.ts index 812bc3a792..d885e6d8de 100644 --- a/web/services/pluginService.ts +++ b/web/services/pluginService.ts @@ -8,10 +8,11 @@ import { DataService, InferenceService, ModelManagementService, + StoreService, } from '@janhq/core' export const isCorePluginInstalled = () => { - if (!extensionPoints.get(DataService.GetConversations)) { + if (!extensionPoints.get(StoreService.CreateCollection)) { return false } if (!extensionPoints.get(InferenceService.InitModel)) { @@ -32,7 +33,7 @@ export const setupBasePlugins = async () => { const basePlugins = await window.electronAPI.basePlugins() if ( - !extensionPoints.get(DataService.GetConversations) || + !extensionPoints.get(StoreService.CreateCollection) || !extensionPoints.get(InferenceService.InitModel) || !extensionPoints.get(ModelManagementService.DownloadModel) ) { @@ -45,7 +46,7 @@ export const setupBasePlugins = async () => { export const execute = (name: CoreService, args?: any) => { if (!extensionPoints.get(name)) { - alert('Missing extension for function: ' + name) + // alert('Missing extension for function: ' + name) return undefined } return extensionPoints.execute(name, args) @@ -53,7 +54,7 @@ export const execute = (name: CoreService, args?: any) => { export const executeSerial = (name: CoreService, args?: any) => { if (!extensionPoints.get(name)) { - alert('Missing extension for function: ' + name) + // alert('Missing extension for function: ' + name) return Promise.resolve(undefined) } return extensionPoints.executeSerial(name, args) diff --git a/web/types/index.d.ts b/web/types/index.d.ts index 07e261a3ce..3d19bd5400 100644 --- a/web/types/index.d.ts +++ b/web/types/index.d.ts @@ -1,7 +1,7 @@ export {} -declare const PLUGIN_CATALOGS: string[] declare global { + declare const PLUGIN_CATALOG: string interface Window { electronAPI?: any | undefined corePlugin?: any | undefined