diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml index 8e050404e..be409e020 100644 --- a/.github/release-drafter.yml +++ b/.github/release-drafter.yml @@ -1,20 +1,18 @@ categories: - - title: '🚀 Features' + - title: "🚀 Features" labels: - - 'type: enhancement' - - 'type: epic' - - 'type: feature request' - - title: '🐛 Bug Fixes' + - "type: feature request" + - "type: enhancement" + - "type: epic" + - title: "🐛 Fixes" labels: - - 'type: bug' - - title: '🧰 Maintenance' - labels: - - 'type: chore' - - 'type: ci' - - title: '📖 Documentaion' - labels: - - 'type: documentation' -change-template: '- $TITLE @$AUTHOR (#$NUMBER)' + - "type: bug" + - title: "🧰 Maintenance" + labels: + - "type: chore" + - "type: ci" + - "type: documentation" +change-template: "- $TITLE @$AUTHOR (#$NUMBER)" change-title-escapes: '\<*_&' # You can add # and @ to disable mentions, and add ` to disable code blocks. template: | ## Changes diff --git a/.github/workflows/auto-assign-author.yml b/.github/workflows/auto-assign-author.yml new file mode 100644 index 000000000..0e861df00 --- /dev/null +++ b/.github/workflows/auto-assign-author.yml @@ -0,0 +1,14 @@ +# Auto assign author, tags, and reviewers to pull requests +name: "Auto Assign Author" +on: + pull_request: + types: [opened] +jobs: + assign-author: + runs-on: ubuntu-latest + permissions: + pull-requests: write + steps: + - uses: toshimaru/auto-author-assign@v1.1.0 + with: + repo-token: "${{ secrets.GITHUB_TOKEN }}" diff --git a/.github/workflows/auto-label-conventional-commits.yaml b/.github/workflows/auto-label-conventional-commits.yaml new file mode 100644 index 000000000..3a915dd83 --- /dev/null +++ b/.github/workflows/auto-label-conventional-commits.yaml @@ -0,0 +1,35 @@ +name: "Auto Label Conventional Commits" +on: + pull_request: + types: + - reopened + - opened +jobs: + label_prs: + runs-on: ubuntu-latest + permissions: + pull-requests: write + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Label PRs + run: | + ISSUE_TITLE=$(gh issue view ${{ github.event.number }} --json title -q ".title") + case "$ISSUE_TITLE" in + chore:*) LABEL="type: chore" ;; + feat:*) LABEL="type: feature request" ;; + perf:*) LABEL="type: enhancement" ;; + fix:*) LABEL="type: bug" ;; + docs:*) LABEL="type: documentation" ;; + ci:*) LABEL="type: ci" ;; + build:*) LABEL="type: ci" ;; + test:*) LABEL="type: chore" ;; + style:*) LABEL="type: chore" ;; + refactor:*) LABEL="type: chore" ;; + *) LABEL="" ;; + esac + if [ -n "$LABEL" ]; then + gh issue edit ${{ github.event.number }} --add-label "$LABEL" + fi + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/jan-electron-build-nightly.yml b/.github/workflows/jan-electron-build-nightly.yml index 2dc361863..1a34fee4d 100644 --- a/.github/workflows/jan-electron-build-nightly.yml +++ b/.github/workflows/jan-electron-build-nightly.yml @@ -2,14 +2,9 @@ name: Jan Build Electron App Nightly on: schedule: - - cron: '0 16 * * *' # At 4 PM UTC, which is 11 AM UTC+7 + - cron: '0 20 * * *' # At 8 PM UTC, which is 3 AM UTC+7 workflow_dispatch: - inputs: - branch: - description: 'Branch to build' - required: true - default: 'main' jobs: build-macos: @@ -73,13 +68,13 @@ jobs: - name: Upload Artifact uses: actions/upload-artifact@v2 with: - name: jan-mac-x64-${{ steps.version_update.outputs.new_version }}.dmg.zip + name: jan-mac-x64-${{ steps.version_update.outputs.new_version }}.dmg path: ./electron/dist/jan-mac-x64-${{ steps.version_update.outputs.new_version }}.dmg - name: Upload Artifact uses: actions/upload-artifact@v2 with: - name: jan-mac-arm64-${{ steps.version_update.outputs.new_version }}.dmg.zip + name: jan-mac-arm64-${{ steps.version_update.outputs.new_version }}.dmg path: ./electron/dist/jan-mac-arm64-${{ steps.version_update.outputs.new_version }}.dmg build-windows-x64: @@ -129,7 +124,7 @@ jobs: - name: Upload Artifact uses: actions/upload-artifact@v2 with: - name: jan-win-x64-${{ steps.version_update.outputs.new_version }}.exe.zip + name: jan-win-x64-${{ steps.version_update.outputs.new_version }}.exe path: ./electron/dist/*.exe build-linux-x64: @@ -175,7 +170,7 @@ jobs: - name: Upload Artifact uses: actions/upload-artifact@v2 with: - name: jan-linux-amd64-${{ steps.version_update.outputs.new_version }}.deb.zip + name: jan-linux-amd64-${{ steps.version_update.outputs.new_version }}.deb path: ./electron/dist/*.deb noti-discord: @@ -187,4 +182,4 @@ jobs: with: args: "Nightly build artifact: https://github.com/janhq/jan/actions/runs/{{ GITHUB_RUN_ID }}" env: - DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }} \ No newline at end of file + DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }} diff --git a/.gitignore b/.gitignore index dc634deb1..4bfb0576f 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,6 @@ .env # Jan inference -models/** error.log node_modules *.tgz @@ -11,14 +10,14 @@ dist build .DS_Store electron/renderer +electron/models package-lock.json *.log -plugin-core/lib core/lib/** # Nitro binary files -plugins/inference-plugin/nitro/*/nitro -plugins/inference-plugin/nitro/*/*.exe -plugins/inference-plugin/nitro/*/*.dll -plugins/inference-plugin/nitro/*/*.metal \ No newline at end of file +extensions/inference-extension/nitro/*/nitro +extensions/inference-extension/nitro/*/*.exe +extensions/inference-extension/nitro/*/*.dll +extensions/inference-extension/nitro/*/*.metal \ No newline at end of file diff --git a/Makefile b/Makefile index 05565be90..76dff5614 100644 --- a/Makefile +++ b/Makefile @@ -12,7 +12,7 @@ else cd uikit && yarn install && yarn build endif -# Installs yarn dependencies and builds core and plugins +# Installs yarn dependencies and builds core and extensions install-and-build: build-uikit ifeq ($(OS),Windows_NT) powershell -Command "yarn config set network-timeout 300000; \ @@ -23,7 +23,7 @@ else endif yarn build:core yarn install - yarn build:plugins + yarn build:extensions dev: install-and-build yarn dev diff --git a/README.md b/README.md index 75214d940..7a1daacdd 100644 --- a/README.md +++ b/README.md @@ -110,7 +110,6 @@ Contributions are welcome! Please read the [CONTRIBUTING.md](CONTRIBUTING.md) fi ``` This will start the development server and open the desktop app. - In this step, there are a few notification about installing base plugin, just click `OK` and `Next` to continue. ### For production build diff --git a/core/package.json b/core/package.json index 0622c6521..69be586f3 100644 --- a/core/package.json +++ b/core/package.json @@ -1,14 +1,13 @@ { "name": "@janhq/core", "version": "0.1.10", - "description": "Plugin core lib", + "description": "Jan app core lib", "keywords": [ "jan", - "plugin", "core" ], - "homepage": "https://github.com/janhq", - "license": "MIT", + "homepage": "https://jan.ai", + "license": "AGPL-3.0", "main": "lib/index.js", "types": "lib/index.d.ts", "directories": { @@ -16,8 +15,7 @@ "test": "__tests__" }, "exports": { - ".": "./lib/index.js", - "./plugin": "./lib/plugins/index.js" + ".": "./lib/index.js" }, "files": [ "lib", diff --git a/core/src/@global/index.d.ts b/core/src/@global/index.d.ts index 3b0ade151..0e52252e3 100644 --- a/core/src/@global/index.d.ts +++ b/core/src/@global/index.d.ts @@ -1,13 +1,7 @@ export {}; declare global { - interface CorePlugin { - store?: any | undefined; - events?: any | undefined; - } interface Window { - corePlugin?: CorePlugin; - coreAPI?: any | undefined; - electronAPI?: any | undefined; + core?: any; } } diff --git a/core/src/core.ts b/core/src/core.ts index 1c4ca1a0a..0e032f4d9 100644 --- a/core/src/core.ts +++ b/core/src/core.ts @@ -1,18 +1,18 @@ /** - * Execute a plugin module function in main process + * Execute a extension module function in main process * - * @param plugin plugin name to import + * @param extension extension name to import * @param method function name to execute * @param args arguments to pass to the function * @returns Promise * */ const executeOnMain: ( - plugin: string, + extension: string, method: string, ...args: any[] -) => Promise = (plugin, method, ...args) => - window.coreAPI?.invokePluginFunc(plugin, method, ...args) +) => Promise = (extension, method, ...args) => + window.core?.api?.invokeExtensionFunc(extension, method, ...args); /** * Downloads a file from a URL and saves it to the local file system. @@ -23,7 +23,7 @@ const executeOnMain: ( const downloadFile: (url: string, fileName: string) => Promise = ( url, fileName -) => window.coreAPI?.downloadFile(url, fileName); +) => window.core?.api?.downloadFile(url, fileName); /** * Aborts the download of a specific file. @@ -31,20 +31,20 @@ const downloadFile: (url: string, fileName: string) => Promise = ( * @returns {Promise} A promise that resolves when the download has been aborted. */ const abortDownload: (fileName: string) => Promise = (fileName) => - window.coreAPI?.abortDownload(fileName); + window.core.api?.abortDownload(fileName); /** * Retrieves the path to the app data directory using the `coreAPI` object. * If the `coreAPI` object is not available, the function returns `undefined`. * @returns A Promise that resolves with the path to the app data directory, or `undefined` if the `coreAPI` object is not available. */ -const appDataPath: () => Promise = () => window.coreAPI?.appDataPath(); +const appDataPath: () => Promise = () => window.core.api?.appDataPath(); /** * Gets the user space path. * @returns {Promise} A Promise that resolves with the user space path. */ -const getUserSpace = (): Promise => window.coreAPI?.getUserSpace(); +const getUserSpace = (): Promise => window.core.api?.getUserSpace(); /** * Opens the file explorer at a specific path. @@ -52,7 +52,10 @@ const getUserSpace = (): Promise => window.coreAPI?.getUserSpace(); * @returns {Promise} A promise that resolves when the file explorer is opened. */ const openFileExplorer: (path: string) => Promise = (path) => - window.coreAPI?.openFileExplorer(path); + window.core.api?.openFileExplorer(path); + +const getResourcePath: () => Promise = () => + window.core.api?.getResourcePath(); /** * Register extension point function type definition @@ -74,4 +77,5 @@ export { appDataPath, getUserSpace, openFileExplorer, + getResourcePath, }; diff --git a/core/src/events.ts b/core/src/events.ts index fa0bef15c..f62aa1113 100644 --- a/core/src/events.ts +++ b/core/src/events.ts @@ -2,14 +2,12 @@ * The `EventName` enumeration contains the names of all the available events in the Jan platform. */ export enum EventName { - OnNewConversation = "onNewConversation", - OnNewMessageRequest = "onNewMessageRequest", - OnNewMessageResponse = "onNewMessageResponse", - OnMessageResponseUpdate = "onMessageResponseUpdate", - OnMessageResponseFinished = "onMessageResponseFinished", - OnDownloadUpdate = "onDownloadUpdate", - OnDownloadSuccess = "onDownloadSuccess", - OnDownloadError = "onDownloadError", + /** The `OnMessageSent` event is emitted when a message is sent. */ + OnMessageSent = "OnMessageSent", + /** The `OnMessageResponse` event is emitted when a message is received. */ + OnMessageResponse = "OnMessageResponse", + /** The `OnMessageUpdate` event is emitted when a message is updated. */ + OnMessageUpdate = "OnMessageUpdate", } /** @@ -22,7 +20,7 @@ const on: (eventName: string, handler: Function) => void = ( eventName, handler ) => { - window.corePlugin?.events?.on(eventName, handler); + window.core?.events?.on(eventName, handler); }; /** @@ -35,7 +33,7 @@ const off: (eventName: string, handler: Function) => void = ( eventName, handler ) => { - window.corePlugin?.events?.off(eventName, handler); + window.core?.events?.off(eventName, handler); }; /** @@ -45,7 +43,7 @@ const off: (eventName: string, handler: Function) => void = ( * @param object The object to pass to the event callback. */ const emit: (eventName: string, object: any) => void = (eventName, object) => { - window.corePlugin?.events?.emit(eventName, object); + window.core?.events?.emit(eventName, object); }; export const events = { diff --git a/core/src/extension.ts b/core/src/extension.ts new file mode 100644 index 000000000..fc1031a53 --- /dev/null +++ b/core/src/extension.ts @@ -0,0 +1,30 @@ +export enum ExtensionType { + Assistant = "assistant", + Conversational = "conversational", + Inference = "inference", + Model = "model", + SystemMonitoring = "systemMonitoring", +} + +/** + * Represents a base extension. + * This class should be extended by any class that represents an extension. + */ +export abstract class BaseExtension { + /** + * Returns the type of the extension. + * @returns {ExtensionType} The type of the extension + * Undefined means its not extending any known extension by the application. + */ + abstract type(): ExtensionType | undefined; + /** + * Called when the extension is loaded. + * Any initialization logic for the extension should be put here. + */ + abstract onLoad(): void; + /** + * Called when the extension is unloaded. + * Any cleanup logic for the extension should be put here. + */ + abstract onUnload(): void; +} diff --git a/core/src/plugins/assistant.ts b/core/src/extensions/assistant.ts similarity index 81% rename from core/src/plugins/assistant.ts rename to core/src/extensions/assistant.ts index 296c6c73a..56c1f27b7 100644 --- a/core/src/plugins/assistant.ts +++ b/core/src/extensions/assistant.ts @@ -1,11 +1,11 @@ import { Assistant } from "../index"; -import { JanPlugin } from "../plugin"; +import { BaseExtension } from "../extension"; /** - * Abstract class for assistant plugins. - * @extends JanPlugin + * Assistant extension for managing assistants. + * @extends BaseExtension */ -export abstract class AssistantPlugin extends JanPlugin { +export abstract class AssistantExtension extends BaseExtension { /** * Creates a new assistant. * @param {Assistant} assistant - The assistant object to be created. diff --git a/core/src/plugins/conversational.ts b/core/src/extensions/conversational.ts similarity index 89% rename from core/src/plugins/conversational.ts rename to core/src/extensions/conversational.ts index dc87fdf9b..291346531 100644 --- a/core/src/plugins/conversational.ts +++ b/core/src/extensions/conversational.ts @@ -1,12 +1,12 @@ import { Thread, ThreadMessage } from "../index"; -import { JanPlugin } from "../plugin"; +import { BaseExtension } from "../extension"; /** - * Abstract class for Thread plugins. + * Conversational extension. Persists and retrieves conversations. * @abstract - * @extends JanPlugin + * @extends BaseExtension */ -export abstract class ConversationalPlugin extends JanPlugin { +export abstract class ConversationalExtension extends BaseExtension { /** * Returns a list of thread. * @abstract diff --git a/core/src/extensions/index.ts b/core/src/extensions/index.ts new file mode 100644 index 000000000..1796c1618 --- /dev/null +++ b/core/src/extensions/index.ts @@ -0,0 +1,25 @@ +/** + * Conversational extension. Persists and retrieves conversations. + * @module + */ +export { ConversationalExtension } from "./conversational"; + +/** + * Inference extension. Start, stop and inference models. + */ +export { InferenceExtension } from "./inference"; + +/** + * Monitoring extension for system monitoring. + */ +export { MonitoringExtension } from "./monitoring"; + +/** + * Assistant extension for managing assistants. + */ +export { AssistantExtension } from "./assistant"; + +/** + * Model extension for managing models. + */ +export { ModelExtension } from "./model"; diff --git a/core/src/plugins/inference.ts b/core/src/extensions/inference.ts similarity index 70% rename from core/src/plugins/inference.ts rename to core/src/extensions/inference.ts index 8cbf2717e..483ba1339 100644 --- a/core/src/plugins/inference.ts +++ b/core/src/extensions/inference.ts @@ -1,18 +1,18 @@ import { MessageRequest, ModelSettingParams, ThreadMessage } from "../index"; -import { JanPlugin } from "../plugin"; +import { BaseExtension } from "../extension"; /** - * An abstract class representing an Inference Plugin for Jan. + * Inference extension. Start, stop and inference models. */ -export abstract class InferencePlugin extends JanPlugin { +export abstract class InferenceExtension extends BaseExtension { /** - * Initializes the model for the plugin. + * Initializes the model for the extension. * @param modelId - The ID of the model to initialize. */ abstract initModel(modelId: string, settings?: ModelSettingParams): Promise; /** - * Stops the model for the plugin. + * Stops the model for the extension. */ abstract stopModel(): Promise; diff --git a/core/src/plugins/model.ts b/core/src/extensions/model.ts similarity index 76% rename from core/src/plugins/model.ts rename to core/src/extensions/model.ts index 53d3d4565..276d15dcc 100644 --- a/core/src/plugins/model.ts +++ b/core/src/extensions/model.ts @@ -1,14 +1,10 @@ -/** - * Represents a plugin for managing machine learning models. - * @abstract - */ -import { JanPlugin } from "../plugin"; -import { Model, ModelCatalog } from "../types/index"; +import { BaseExtension } from "../extension"; +import { Model } from "../types/index"; /** - * An abstract class representing a plugin for managing machine learning models. + * Model extension for managing models. */ -export abstract class ModelPlugin extends JanPlugin { +export abstract class ModelExtension extends BaseExtension { /** * Downloads a model. * @param model - The model to download. @@ -47,5 +43,5 @@ export abstract class ModelPlugin extends JanPlugin { * Gets a list of configured models. * @returns A Promise that resolves with an array of configured models. */ - abstract getConfiguredModels(): Promise; + abstract getConfiguredModels(): Promise; } diff --git a/core/src/plugins/monitoring.ts b/core/src/extensions/monitoring.ts similarity index 67% rename from core/src/plugins/monitoring.ts rename to core/src/extensions/monitoring.ts index ea608b7b2..f3d66e658 100644 --- a/core/src/plugins/monitoring.ts +++ b/core/src/extensions/monitoring.ts @@ -1,10 +1,10 @@ -import { JanPlugin } from "../plugin"; +import { BaseExtension } from "../extension"; /** - * Abstract class for monitoring plugins. - * @extends JanPlugin + * Monitoring extension for system monitoring. + * @extends BaseExtension */ -export abstract class MonitoringPlugin extends JanPlugin { +export abstract class MonitoringExtension extends BaseExtension { /** * Returns information about the system resources. * @returns {Promise} A promise that resolves with the system resources information. diff --git a/core/src/fs.ts b/core/src/fs.ts index 5c2e06275..b428a7841 100644 --- a/core/src/fs.ts +++ b/core/src/fs.ts @@ -1,9 +1,5 @@ const fetchRetry = require("fetch-retry")(global.fetch); -const PORT = 1337; -const LOCAL_HOST = "127.0.0.1"; -const JAN_HTTP_SERVER_URL = `http://${LOCAL_HOST}:${PORT}`; -const JAN_FS_API = `${JAN_HTTP_SERVER_URL}/fs`; /** * Writes data to a file at the specified path. * @param {string} path - The path to the file. @@ -11,7 +7,7 @@ const JAN_FS_API = `${JAN_HTTP_SERVER_URL}/fs`; * @returns {Promise} A Promise that resolves when the file is written successfully. */ const writeFile: (path: string, data: string) => Promise = (path, data) => - window.coreAPI?.writeFile(path, data); + window.core.api?.writeFile(path, data); /** * Checks whether the path is a directory. @@ -19,8 +15,7 @@ const writeFile: (path: string, data: string) => Promise = (path, data) => * @returns {boolean} A boolean indicating whether the path is a directory. */ const isDirectory = (path: string): Promise => - window.coreAPI?.isDirectory(path); - + window.core.api?.isDirectory(path); /** * Reads the contents of a file at the specified path. @@ -28,7 +23,7 @@ const isDirectory = (path: string): Promise => * @returns {Promise} A Promise that resolves with the contents of the file. */ const readFile: (path: string) => Promise = (path) => - window.coreAPI?.readFile(path); + window.core.api?.readFile(path); /** * List the directory files @@ -36,8 +31,7 @@ const readFile: (path: string) => Promise = (path) => * @returns {Promise} A Promise that resolves with the contents of the directory. */ const listFiles: (path: string) => Promise = (path) => - window.coreAPI?.listFiles(path); - + window.core.api?.listFiles(path); /** * Creates a directory at the specified path. @@ -45,7 +39,7 @@ const listFiles: (path: string) => Promise = (path) => * @returns {Promise} A Promise that resolves when the directory is created successfully. */ const mkdir: (path: string) => Promise = (path) => - window.coreAPI?.mkdir(path); + window.core.api?.mkdir(path); /** * Removes a directory at the specified path. @@ -53,15 +47,14 @@ const mkdir: (path: string) => Promise = (path) => * @returns {Promise} A Promise that resolves when the directory is removed successfully. */ const rmdir: (path: string) => Promise = (path) => - window.coreAPI?.rmdir(path); - + window.core.api?.rmdir(path); /** * Deletes a file from the local file system. * @param {string} path - The path of the file to delete. * @returns {Promise} A Promise that resolves when the file is deleted. */ const deleteFile: (path: string) => Promise = (path) => - window.coreAPI?.deleteFile(path); + window.core.api?.deleteFile(path); /** * Appends data to a file at the specified path. @@ -69,7 +62,10 @@ const deleteFile: (path: string) => Promise = (path) => * @param data data to append */ const appendFile: (path: string, data: string) => Promise = (path, data) => - window.coreAPI?.appendFile(path, data); + window.core.api?.appendFile(path, data); + +const copyFile: (src: string, dest: string) => Promise = (src, dest) => + window.core.api?.copyFile(src, dest); /** * Reads a file line by line. @@ -77,8 +73,7 @@ const appendFile: (path: string, data: string) => Promise = (path, data) => * @returns {Promise} A promise that resolves to the lines of the file. */ const readLineByLine: (path: string) => Promise = (path) => - window.coreAPI?.readLineByLine(path); - + window.core.api?.readLineByLine(path); export const fs = { isDirectory, @@ -90,4 +85,5 @@ export const fs = { deleteFile, appendFile, readLineByLine, + copyFile, }; diff --git a/core/src/index.ts b/core/src/index.ts index 8d398f8b5..ff233ffb3 100644 --- a/core/src/index.ts +++ b/core/src/index.ts @@ -1,29 +1,35 @@ /** - * Core module exports. - * @module - */ -export * from "./core"; - -/** - * Events events exports. - * @module - */ -export * from "./events"; - -/** - * Events types exports. + * Export all types. * @module */ export * from "./types/index"; /** - * Filesystem module exports. + * Export Core module + * @module + */ +export * from "./core"; + +/** + * Export Event module. + * @module + */ +export * from "./events"; + +/** + * Export Filesystem module. * @module */ export * from "./fs"; /** - * Plugin base module export. + * Export Extension module. * @module */ -export * from "./plugin"; +export * from "./extension"; + +/** + * Export all base extensions. + * @module + */ +export * from "./extensions/index"; diff --git a/core/src/plugin.ts b/core/src/plugin.ts deleted file mode 100644 index 046c8bf5e..000000000 --- a/core/src/plugin.ts +++ /dev/null @@ -1,14 +0,0 @@ -export enum PluginType { - Conversational = "conversational", - Inference = "inference", - Preference = "preference", - SystemMonitoring = "systemMonitoring", - Model = "model", - Assistant = "assistant", -} - -export abstract class JanPlugin { - abstract type(): PluginType; - abstract onLoad(): void; - abstract onUnload(): void; -} diff --git a/core/src/plugins/index.ts b/core/src/plugins/index.ts deleted file mode 100644 index 4ca712db3..000000000 --- a/core/src/plugins/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -/** - * Conversational plugin. Persists and retrieves conversations. - * @module - */ -export { ConversationalPlugin } from "./conversational"; - -/** - * Inference plugin. Start, stop and inference models. - */ -export { InferencePlugin } from "./inference"; - -/** - * Monitoring plugin for system monitoring. - */ -export { MonitoringPlugin } from "./monitoring"; - -/** - * Assistant plugin for managing assistants. - */ -export { AssistantPlugin } from "./assistant"; - -/** - * Model plugin for managing models. - */ -export { ModelPlugin } from "./model"; \ No newline at end of file diff --git a/core/src/types/index.ts b/core/src/types/index.ts index 15e83772f..bbd1e98de 100644 --- a/core/src/types/index.ts +++ b/core/src/types/index.ts @@ -180,7 +180,7 @@ export interface Model { /** * The version of the model. */ - version: string; + version: number; /** * The model download source. It can be an external url or a local filepath. @@ -197,12 +197,6 @@ export interface Model { */ name: string; - /** - * The organization that owns the model (you!) - * Default: "you" - */ - owned_by: string; - /** * The Unix timestamp (in seconds) for when the model was created */ @@ -236,11 +230,16 @@ export interface Model { metadata: ModelMetadata; } +export type ModelMetadata = { + author: string; + tags: string[]; + size: number; +}; + /** * The Model transition states. */ export enum ModelState { - ToDownload = "to_download", Downloading = "downloading", Ready = "ready", Running = "running", @@ -250,65 +249,27 @@ export enum ModelState { * The available model settings. */ export type ModelSettingParams = { - ctx_len: number; - ngl: number; - embedding: boolean; - n_parallel: number; + ctx_len?: number; + ngl?: number; + embedding?: boolean; + n_parallel?: number; + system_prompt?: string; + user_prompt?: string; + ai_prompt?: string; }; /** * The available model runtime parameters. */ export type ModelRuntimeParam = { - temperature: number; - token_limit: number; - top_k: number; - top_p: number; - stream: boolean; + temperature?: number; + token_limit?: number; + top_k?: number; + top_p?: number; + stream?: boolean; + max_tokens?: number; }; -/** - * The metadata of the model. - */ -export type ModelMetadata = { - engine: string; - quantization: string; - size: number; - binaries: string[]; - maxRamRequired: number; - author: string; - avatarUrl: string; -}; - -/** - * Model type of the presentation object which will be presented to the user - * @data_transfer_object - */ -export interface ModelCatalog { - /** The unique id of the model.*/ - id: string; - /** The name of the model.*/ - name: string; - /** The avatar url of the model.*/ - avatarUrl: string; - /** The short description of the model.*/ - shortDescription: string; - /** The long description of the model.*/ - longDescription: string; - /** The author name of the model.*/ - author: string; - /** The version of the model.*/ - version: string; - /** The origin url of the model repo.*/ - modelUrl: string; - /** The timestamp indicating when this model was released.*/ - releaseDate: number; - /** The tags attached to the model description **/ - tags: string[]; - /** The available versions of this model to download. */ - availableVersions: Model[]; -} - /** * Assistant type defines the shape of an assistant object. * @stored diff --git a/electron/.eslintrc.js b/electron/.eslintrc.js index 46d385185..25a98348f 100644 --- a/electron/.eslintrc.js +++ b/electron/.eslintrc.js @@ -1,44 +1,38 @@ module.exports = { root: true, - parser: "@typescript-eslint/parser", - plugins: ["@typescript-eslint"], + parser: '@typescript-eslint/parser', + plugins: ['@typescript-eslint'], env: { node: true, }, extends: [ - "eslint:recommended", - "plugin:@typescript-eslint/recommended", - "plugin:react/recommended", + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:react/recommended', ], rules: { - "@typescript-eslint/no-non-null-assertion": "off", - "react/prop-types": "off", // In favor of strong typing - no need to dedupe - "@typescript-eslint/no-var-requires": "off", - "@typescript-eslint/ban-ts-comment": "off", - "@typescript-eslint/no-unused-vars": "off", - "@typescript-eslint/no-explicit-any": "off", + '@typescript-eslint/no-non-null-assertion': 'off', + 'react/prop-types': 'off', // In favor of strong typing - no need to dedupe + '@typescript-eslint/no-var-requires': 'off', + '@typescript-eslint/ban-ts-comment': 'off', + '@typescript-eslint/no-unused-vars': 'off', + '@typescript-eslint/no-explicit-any': 'off', }, settings: { react: { - createClass: "createReactClass", // Regex for Component Factory to use, + createClass: 'createReactClass', // Regex for Component Factory to use, // default to "createReactClass" - pragma: "React", // Pragma to use, default to "React" - version: "detect", // React version. "detect" automatically picks the version you have installed. + pragma: 'React', // Pragma to use, default to "React" + version: 'detect', // React version. "detect" automatically picks the version you have installed. // You can also use `16.0`, `16.3`, etc, if you want to override the detected value. // default to latest and warns if missing // It will default to "detect" in the future }, linkComponents: [ // Components used as alternatives to for linking, eg. - "Hyperlink", - { name: "Link", linkAttribute: "to" }, + 'Hyperlink', + { name: 'Link', linkAttribute: 'to' }, ], }, - ignorePatterns: [ - "build", - "renderer", - "node_modules", - "core/plugins", - "core/**/*.test.js", - ], -}; + ignorePatterns: ['build', 'renderer', 'node_modules'], +} diff --git a/electron/core/plugin/facade.ts b/electron/core/plugin/facade.ts deleted file mode 100644 index bd1089109..000000000 --- a/electron/core/plugin/facade.ts +++ /dev/null @@ -1,30 +0,0 @@ -const { ipcRenderer, contextBridge } = require("electron"); - -export function useFacade() { - const interfaces = { - install(plugins: any[]) { - return ipcRenderer.invoke("pluggable:install", plugins); - }, - uninstall(plugins: any[], reload: boolean) { - return ipcRenderer.invoke("pluggable:uninstall", plugins, reload); - }, - getActive() { - return ipcRenderer.invoke("pluggable:getActivePlugins"); - }, - update(plugins: any[], reload: boolean) { - return ipcRenderer.invoke("pluggable:update", plugins, reload); - }, - updatesAvailable(plugin: any) { - return ipcRenderer.invoke("pluggable:updatesAvailable", plugin); - }, - toggleActive(plugin: any, active: boolean) { - return ipcRenderer.invoke("pluggable:togglePluginActive", plugin, active); - }, - }; - - if (contextBridge) { - contextBridge.exposeInMainWorld("pluggableElectronIpc", interfaces); - } - - return interfaces; -} diff --git a/electron/core/plugin/globals.ts b/electron/core/plugin/globals.ts deleted file mode 100644 index 69df7925c..000000000 --- a/electron/core/plugin/globals.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { existsSync, mkdirSync, writeFileSync } from "fs"; -import { join, resolve } from "path"; - -export let pluginsPath: string | undefined = undefined; - -/** - * @private - * Set path to plugins directory and create the directory if it does not exist. - * @param {string} plgPath path to plugins directory - */ -export function setPluginsPath(plgPath: string) { - // Create folder if it does not exist - let plgDir; - try { - plgDir = resolve(plgPath); - if (plgDir.length < 2) throw new Error(); - - if (!existsSync(plgDir)) mkdirSync(plgDir); - - const pluginsJson = join(plgDir, "plugins.json"); - if (!existsSync(pluginsJson)) writeFileSync(pluginsJson, "{}", "utf8"); - - pluginsPath = plgDir; - } catch (error) { - throw new Error("Invalid path provided to the plugins folder"); - } -} - -/** - * @private - * Get the path to the plugins.json file. - * @returns location of plugins.json - */ -export function getPluginsFile() { - return join(pluginsPath ?? "", "plugins.json"); -} \ No newline at end of file diff --git a/electron/core/plugin/index.ts b/electron/core/plugin/index.ts deleted file mode 100644 index e8c64747b..000000000 --- a/electron/core/plugin/index.ts +++ /dev/null @@ -1,149 +0,0 @@ -import { readFileSync } from "fs"; -import { protocol } from "electron"; -import { normalize } from "path"; - -import Plugin from "./plugin"; -import { - getAllPlugins, - removePlugin, - persistPlugins, - installPlugins, - getPlugin, - getActivePlugins, - addPlugin, -} from "./store"; -import { - pluginsPath as storedPluginsPath, - setPluginsPath, - getPluginsFile, -} from "./globals"; -import router from "./router"; - -/** - * Sets up the required communication between the main and renderer processes. - * Additionally sets the plugins up using {@link usePlugins} if a pluginsPath is provided. - * @param {Object} options configuration for setting up the renderer facade. - * @param {confirmInstall} [options.confirmInstall] Function to validate that a plugin should be installed. - * @param {Boolean} [options.useFacade=true] Whether to make a facade to the plugins available in the renderer. - * @param {string} [options.pluginsPath] Optional path to the plugins folder. - * @returns {pluginManager|Object} A set of functions used to manage the plugin lifecycle if usePlugins is provided. - * @function - */ -export function init(options: any) { - if ( - !Object.prototype.hasOwnProperty.call(options, "useFacade") || - options.useFacade - ) { - // Enable IPC to be used by the facade - router(); - } - - // Create plugins protocol to serve plugins to renderer - registerPluginProtocol(); - - // perform full setup if pluginsPath is provided - if (options.pluginsPath) { - return usePlugins(options.pluginsPath); - } - - return {}; -} - -/** - * Create plugins protocol to provide plugins to renderer - * @private - * @returns {boolean} Whether the protocol registration was successful - */ -function registerPluginProtocol() { - return protocol.registerFileProtocol("plugin", (request, callback) => { - const entry = request.url.substr(8); - const url = normalize(storedPluginsPath + entry); - callback({ path: url }); - }); -} - -/** - * Set Pluggable Electron up to run from the pluginPath folder if it is provided and - * load plugins persisted in that folder. - * @param {string} pluginsPath Path to the plugins folder. Required if not yet set up. - * @returns {pluginManager} A set of functions used to manage the plugin lifecycle. - */ -export function usePlugins(pluginsPath: string) { - if (!pluginsPath) - throw Error( - "A path to the plugins folder is required to use Pluggable Electron" - ); - // Store the path to the plugins folder - setPluginsPath(pluginsPath); - - // Remove any registered plugins - for (const plugin of getAllPlugins()) { - if (plugin.name) removePlugin(plugin.name, false); - } - - // Read plugin list from plugins folder - const plugins = JSON.parse(readFileSync(getPluginsFile(), "utf-8")); - try { - // Create and store a Plugin instance for each plugin in list - for (const p in plugins) { - loadPlugin(plugins[p]); - } - persistPlugins(); - } catch (error) { - // Throw meaningful error if plugin loading fails - throw new Error( - "Could not successfully rebuild list of installed plugins.\n" + - error + - "\nPlease check the plugins.json file in the plugins folder." - ); - } - - // Return the plugin lifecycle functions - return getStore(); -} - -/** - * Check the given plugin object. If it is marked for uninstalling, the plugin files are removed. - * Otherwise a Plugin instance for the provided object is created and added to the store. - * @private - * @param {Object} plg Plugin info - */ -function loadPlugin(plg: any) { - // Create new plugin, populate it with plg details and save it to the store - const plugin = new Plugin(); - - for (const key in plg) { - if (Object.prototype.hasOwnProperty.call(plg, key)) { - // Use Object.defineProperty to set the properties as writable - Object.defineProperty(plugin, key, { - value: plg[key], - writable: true, - enumerable: true, - configurable: true, - }); - } - } - - addPlugin(plugin, false); - plugin.subscribe("pe-persist", persistPlugins); -} - -/** - * Returns the publicly available store functions. - * @returns {pluginManager} A set of functions used to manage the plugin lifecycle. - */ -export function getStore() { - if (!storedPluginsPath) { - throw new Error( - "The plugin path has not yet been set up. Please run usePlugins before accessing the store" - ); - } - - return { - installPlugins, - getPlugin, - getAllPlugins, - getActivePlugins, - removePlugin, - }; -} diff --git a/electron/core/plugin/plugin.ts b/electron/core/plugin/plugin.ts deleted file mode 100644 index f0fc073d7..000000000 --- a/electron/core/plugin/plugin.ts +++ /dev/null @@ -1,213 +0,0 @@ -import { rmdir } from "fs/promises"; -import { resolve, join } from "path"; -import { manifest, extract } from "pacote"; -import * as Arborist from "@npmcli/arborist"; - -import { pluginsPath } from "./globals"; - -/** - * An NPM package that can be used as a Pluggable Electron plugin. - * Used to hold all the information and functions necessary to handle the plugin lifecycle. - */ -class Plugin { - /** - * @property {string} origin Original specification provided to fetch the package. - * @property {Object} installOptions Options provided to pacote when fetching the manifest. - * @property {name} name The name of the plugin as defined in the manifest. - * @property {string} url Electron URL where the package can be accessed. - * @property {string} version Version of the package as defined in the manifest. - * @property {Array} activationPoints List of {@link ./Execution-API#activationPoints|activation points}. - * @property {string} main The entry point as defined in the main entry of the manifest. - * @property {string} description The description of plugin as defined in the manifest. - * @property {string} icon The icon of plugin as defined in the manifest. - */ - origin?: string; - installOptions: any; - name?: string; - url?: string; - version?: string; - activationPoints?: Array; - main?: string; - description?: string; - icon?: string; - - /** @private */ - _active = false; - - /** - * @private - * @property {Object.} #listeners A list of callbacks to be executed when the Plugin is updated. - */ - listeners: Record void> = {}; - - /** - * Set installOptions with defaults for options that have not been provided. - * @param {string} [origin] Original specification provided to fetch the package. - * @param {Object} [options] Options provided to pacote when fetching the manifest. - */ - constructor(origin?: string, options = {}) { - const defaultOpts = { - version: false, - fullMetadata: false, - Arborist, - }; - - this.origin = origin; - this.installOptions = { ...defaultOpts, ...options }; - } - - /** - * Package name with version number. - * @type {string} - */ - get specifier() { - return ( - this.origin + - (this.installOptions.version ? "@" + this.installOptions.version : "") - ); - } - - /** - * Whether the plugin should be registered with its activation points. - * @type {boolean} - */ - get active() { - return this._active; - } - - /** - * Set Package details based on it's manifest - * @returns {Promise.} Resolves to true when the action completed - */ - async getManifest() { - // Get the package's manifest (package.json object) - try { - const mnf = await manifest(this.specifier, this.installOptions); - - // set the Package properties based on the it's manifest - this.name = mnf.name; - this.version = mnf.version; - this.activationPoints = mnf.activationPoints - ? (mnf.activationPoints as string[]) - : undefined; - this.main = mnf.main; - this.description = mnf.description; - this.icon = mnf.icon as any; - } catch (error) { - throw new Error( - `Package ${this.origin} does not contain a valid manifest: ${error}` - ); - } - - return true; - } - - /** - * Extract plugin to plugins folder. - * @returns {Promise.} This plugin - * @private - */ - async _install() { - try { - // import the manifest details - await this.getManifest(); - - // Install the package in a child folder of the given folder - await extract( - this.specifier, - join(pluginsPath ?? "", this.name ?? ""), - this.installOptions - ); - - if (!Array.isArray(this.activationPoints)) - throw new Error("The plugin does not contain any activation points"); - - // Set the url using the custom plugins protocol - this.url = `plugin://${this.name}/${this.main}`; - - this.emitUpdate(); - } catch (err) { - // Ensure the plugin is not stored and the folder is removed if the installation fails - this.setActive(false); - throw err; - } - - return [this]; - } - - /** - * Subscribe to updates of this plugin - * @param {string} name name of the callback to register - * @param {callback} cb The function to execute on update - */ - subscribe(name: string, cb: () => void) { - this.listeners[name] = cb; - } - - /** - * Remove subscription - * @param {string} name name of the callback to remove - */ - unsubscribe(name: string) { - delete this.listeners[name]; - } - - /** - * Execute listeners - */ - emitUpdate() { - for (const cb in this.listeners) { - this.listeners[cb].call(null, this); - } - } - - /** - * Check for updates and install if available. - * @param {string} version The version to update to. - * @returns {boolean} Whether an update was performed. - */ - async update(version = false) { - if (await this.isUpdateAvailable()) { - this.installOptions.version = version; - await this._install(); - return true; - } - - return false; - } - - /** - * Check if a new version of the plugin is available at the origin. - * @returns the latest available version if a new version is available or false if not. - */ - async isUpdateAvailable() { - if (this.origin) { - const mnf = await manifest(this.origin); - return mnf.version !== this.version ? mnf.version : false; - } - } - - /** - * Remove plugin and refresh renderers. - * @returns {Promise} - */ - async uninstall() { - const plgPath = resolve(pluginsPath ?? "", this.name ?? ""); - await rmdir(plgPath, { recursive: true }); - - this.emitUpdate(); - } - - /** - * Set a plugin's active state. This determines if a plugin should be loaded on initialisation. - * @param {boolean} active State to set _active to - * @returns {Plugin} This plugin - */ - setActive(active: boolean) { - this._active = active; - this.emitUpdate(); - return this; - } -} - -export default Plugin; diff --git a/electron/core/plugin/router.ts b/electron/core/plugin/router.ts deleted file mode 100644 index 09c79485b..000000000 --- a/electron/core/plugin/router.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { ipcMain, webContents } from "electron"; - -import { - getPlugin, - getActivePlugins, - installPlugins, - removePlugin, - getAllPlugins, -} from "./store"; -import { pluginsPath } from "./globals"; -import Plugin from "./plugin"; - -// Throw an error if pluginsPath has not yet been provided by usePlugins. -const checkPluginsPath = () => { - if (!pluginsPath) - throw Error("Path to plugins folder has not yet been set up."); -}; -let active = false; -/** - * Provide the renderer process access to the plugins. - **/ -export default function () { - if (active) return; - // Register IPC route to install a plugin - ipcMain.handle("pluggable:install", async (e, plugins) => { - checkPluginsPath(); - - // Install and activate all provided plugins - const installed = await installPlugins(plugins); - return JSON.parse(JSON.stringify(installed)); - }); - - // Register IPC route to uninstall a plugin - ipcMain.handle("pluggable:uninstall", async (e, plugins, reload) => { - checkPluginsPath(); - - // Uninstall all provided plugins - for (const plg of plugins) { - const plugin = getPlugin(plg); - await plugin.uninstall(); - if (plugin.name) removePlugin(plugin.name); - } - - // Reload all renderer pages if needed - reload && webContents.getAllWebContents().forEach((wc) => wc.reload()); - return true; - }); - - // Register IPC route to update a plugin - ipcMain.handle("pluggable:update", async (e, plugins, reload) => { - checkPluginsPath(); - - // Update all provided plugins - const updated: Plugin[] = []; - for (const plg of plugins) { - const plugin = getPlugin(plg); - const res = await plugin.update(); - if (res) updated.push(plugin); - } - - // Reload all renderer pages if needed - if (updated.length && reload) - webContents.getAllWebContents().forEach((wc) => wc.reload()); - - return JSON.parse(JSON.stringify(updated)); - }); - - // Register IPC route to check if updates are available for a plugin - ipcMain.handle("pluggable:updatesAvailable", (e, names) => { - checkPluginsPath(); - - const plugins = names - ? names.map((name: string) => getPlugin(name)) - : getAllPlugins(); - - const updates: Record = {}; - for (const plugin of plugins) { - updates[plugin.name] = plugin.isUpdateAvailable(); - } - return updates; - }); - - // Register IPC route to get the list of active plugins - ipcMain.handle("pluggable:getActivePlugins", () => { - checkPluginsPath(); - return JSON.parse(JSON.stringify(getActivePlugins())); - }); - - // Register IPC route to toggle the active state of a plugin - ipcMain.handle("pluggable:togglePluginActive", (e, plg, active) => { - checkPluginsPath(); - const plugin = getPlugin(plg); - return JSON.parse(JSON.stringify(plugin.setActive(active))); - }); - - active = true; -} diff --git a/electron/core/plugin/store.ts b/electron/core/plugin/store.ts deleted file mode 100644 index cfd25e5ca..000000000 --- a/electron/core/plugin/store.ts +++ /dev/null @@ -1,131 +0,0 @@ -/** - * Provides access to the plugins stored by Pluggable Electron - * @typedef {Object} pluginManager - * @prop {getPlugin} getPlugin - * @prop {getAllPlugins} getAllPlugins - * @prop {getActivePlugins} getActivePlugins - * @prop {installPlugins} installPlugins - * @prop {removePlugin} removePlugin - */ - -import { writeFileSync } from "fs"; -import Plugin from "./plugin"; -import { getPluginsFile } from "./globals"; - -/** - * @module store - * @private - */ - -/** - * Register of installed plugins - * @type {Object.} plugin - List of installed plugins - */ -const plugins: Record = {}; - -/** - * Get a plugin from the stored plugins. - * @param {string} name Name of the plugin to retrieve - * @returns {Plugin} Retrieved plugin - * @alias pluginManager.getPlugin - */ -export function getPlugin(name: string) { - if (!Object.prototype.hasOwnProperty.call(plugins, name)) { - throw new Error(`Plugin ${name} does not exist`); - } - - return plugins[name]; -} - -/** - * Get list of all plugin objects. - * @returns {Array.} All plugin objects - * @alias pluginManager.getAllPlugins - */ -export function getAllPlugins() { - return Object.values(plugins); -} - -/** - * Get list of active plugin objects. - * @returns {Array.} Active plugin objects - * @alias pluginManager.getActivePlugins - */ -export function getActivePlugins() { - return Object.values(plugins).filter((plugin) => plugin.active); -} - -/** - * Remove plugin from store and maybe save stored plugins to file - * @param {string} name Name of the plugin to remove - * @param {boolean} persist Whether to save the changes to plugins to file - * @returns {boolean} Whether the delete was successful - * @alias pluginManager.removePlugin - */ -export function removePlugin(name: string, persist = true) { - const del = delete plugins[name]; - if (persist) persistPlugins(); - return del; -} - -/** - * Add plugin to store and maybe save stored plugins to file - * @param {Plugin} plugin Plugin to add to store - * @param {boolean} persist Whether to save the changes to plugins to file - * @returns {void} - */ -export function addPlugin(plugin: Plugin, persist = true) { - if (plugin.name) plugins[plugin.name] = plugin; - if (persist) { - persistPlugins(); - plugin.subscribe("pe-persist", persistPlugins); - } -} - -/** - * Save stored plugins to file - * @returns {void} - */ -export function persistPlugins() { - const persistData: Record = {}; - for (const name in plugins) { - persistData[name] = plugins[name]; - } - writeFileSync(getPluginsFile(), JSON.stringify(persistData), "utf8"); -} - -/** - * Create and install a new plugin for the given specifier. - * @param {Array.} plugins A list of NPM specifiers, or installation configuration objects. - * @param {boolean} [store=true] Whether to store the installed plugins in the store - * @returns {Promise.>} New plugin - * @alias pluginManager.installPlugins - */ -export async function installPlugins(plugins: any, store = true) { - const installed: Plugin[] = []; - for (const plg of plugins) { - // Set install options and activation based on input type - const isObject = typeof plg === "object"; - const spec = isObject ? [plg.specifier, plg] : [plg]; - const activate = isObject ? plg.activate !== false : true; - - // Install and possibly activate plugin - const plugin = new Plugin(...spec); - await plugin._install(); - if (activate) plugin.setActive(true); - - // Add plugin to store if needed - if (store) addPlugin(plugin); - installed.push(plugin); - } - - // Return list of all installed plugins - return installed; -} - -/** - * @typedef {Object.} installOptions The {@link https://www.npmjs.com/package/pacote|pacote} - * options used to install the plugin with some extra options. - * @param {string} specifier the NPM specifier that identifies the package. - * @param {boolean} [activate] Whether this plugin should be activated after installation. Defaults to true. - */ diff --git a/electron/extension/extension.ts b/electron/extension/extension.ts new file mode 100644 index 000000000..1bd11611d --- /dev/null +++ b/electron/extension/extension.ts @@ -0,0 +1,204 @@ +import { rmdir } from 'fs/promises' +import { resolve, join } from 'path' +import { manifest, extract } from 'pacote' +import * as Arborist from '@npmcli/arborist' +import { ExtensionManager } from './../managers/extension' + +/** + * An NPM package that can be used as an extension. + * Used to hold all the information and functions necessary to handle the extension lifecycle. + */ +class Extension { + /** + * @property {string} origin Original specification provided to fetch the package. + * @property {Object} installOptions Options provided to pacote when fetching the manifest. + * @property {name} name The name of the extension as defined in the manifest. + * @property {string} url Electron URL where the package can be accessed. + * @property {string} version Version of the package as defined in the manifest. + * @property {string} main The entry point as defined in the main entry of the manifest. + * @property {string} description The description of extension as defined in the manifest. + */ + origin?: string + installOptions: any + name?: string + url?: string + version?: string + main?: string + description?: string + + /** @private */ + _active = false + + /** + * @private + * @property {Object.} #listeners A list of callbacks to be executed when the Extension is updated. + */ + listeners: Record void> = {} + + /** + * Set installOptions with defaults for options that have not been provided. + * @param {string} [origin] Original specification provided to fetch the package. + * @param {Object} [options] Options provided to pacote when fetching the manifest. + */ + constructor(origin?: string, options = {}) { + const defaultOpts = { + version: false, + fullMetadata: false, + Arborist, + } + + this.origin = origin + this.installOptions = { ...defaultOpts, ...options } + } + + /** + * Package name with version number. + * @type {string} + */ + get specifier() { + return ( + this.origin + + (this.installOptions.version ? '@' + this.installOptions.version : '') + ) + } + + /** + * Whether the extension should be registered with its activation points. + * @type {boolean} + */ + get active() { + return this._active + } + + /** + * Set Package details based on it's manifest + * @returns {Promise.} Resolves to true when the action completed + */ + async getManifest() { + // Get the package's manifest (package.json object) + try { + const mnf = await manifest(this.specifier, this.installOptions) + + // set the Package properties based on the it's manifest + this.name = mnf.name + this.version = mnf.version + this.main = mnf.main + this.description = mnf.description + } catch (error) { + throw new Error( + `Package ${this.origin} does not contain a valid manifest: ${error}` + ) + } + + return true + } + + /** + * Extract extension to extensions folder. + * @returns {Promise.} This extension + * @private + */ + async _install() { + try { + // import the manifest details + await this.getManifest() + + // Install the package in a child folder of the given folder + await extract( + this.specifier, + join(ExtensionManager.instance.extensionsPath ?? '', this.name ?? ''), + this.installOptions + ) + + // Set the url using the custom extensions protocol + this.url = `extension://${this.name}/${this.main}` + + this.emitUpdate() + } catch (err) { + // Ensure the extension is not stored and the folder is removed if the installation fails + this.setActive(false) + throw err + } + + return [this] + } + + /** + * Subscribe to updates of this extension + * @param {string} name name of the callback to register + * @param {callback} cb The function to execute on update + */ + subscribe(name: string, cb: () => void) { + this.listeners[name] = cb + } + + /** + * Remove subscription + * @param {string} name name of the callback to remove + */ + unsubscribe(name: string) { + delete this.listeners[name] + } + + /** + * Execute listeners + */ + emitUpdate() { + for (const cb in this.listeners) { + this.listeners[cb].call(null, this) + } + } + + /** + * Check for updates and install if available. + * @param {string} version The version to update to. + * @returns {boolean} Whether an update was performed. + */ + async update(version = false) { + if (await this.isUpdateAvailable()) { + this.installOptions.version = version + await this._install() + return true + } + + return false + } + + /** + * Check if a new version of the extension is available at the origin. + * @returns the latest available version if a new version is available or false if not. + */ + async isUpdateAvailable() { + if (this.origin) { + const mnf = await manifest(this.origin) + return mnf.version !== this.version ? mnf.version : false + } + } + + /** + * Remove extension and refresh renderers. + * @returns {Promise} + */ + async uninstall() { + const extPath = resolve( + ExtensionManager.instance.extensionsPath ?? '', + this.name ?? '' + ) + await rmdir(extPath, { recursive: true }) + + this.emitUpdate() + } + + /** + * Set a extension's active state. This determines if a extension should be loaded on initialisation. + * @param {boolean} active State to set _active to + * @returns {Extension} This extension + */ + setActive(active: boolean) { + this._active = active + this.emitUpdate() + return this + } +} + +export default Extension diff --git a/electron/extension/index.ts b/electron/extension/index.ts new file mode 100644 index 000000000..c6a6cc0c0 --- /dev/null +++ b/electron/extension/index.ts @@ -0,0 +1,137 @@ +import { readFileSync } from 'fs' +import { protocol } from 'electron' +import { normalize } from 'path' + +import Extension from './extension' +import { + getAllExtensions, + removeExtension, + persistExtensions, + installExtensions, + getExtension, + getActiveExtensions, + addExtension, +} from './store' +import { ExtensionManager } from './../managers/extension' + +/** + * Sets up the required communication between the main and renderer processes. + * Additionally sets the extensions up using {@link useExtensions} if a extensionsPath is provided. + * @param {Object} options configuration for setting up the renderer facade. + * @param {confirmInstall} [options.confirmInstall] Function to validate that a extension should be installed. + * @param {Boolean} [options.useFacade=true] Whether to make a facade to the extensions available in the renderer. + * @param {string} [options.extensionsPath] Optional path to the extensions folder. + * @returns {extensionManager|Object} A set of functions used to manage the extension lifecycle if useExtensions is provided. + * @function + */ +export function init(options: any) { + // Create extensions protocol to serve extensions to renderer + registerExtensionProtocol() + + // perform full setup if extensionsPath is provided + if (options.extensionsPath) { + return useExtensions(options.extensionsPath) + } + + return {} +} + +/** + * Create extensions protocol to provide extensions to renderer + * @private + * @returns {boolean} Whether the protocol registration was successful + */ +function registerExtensionProtocol() { + return protocol.registerFileProtocol('extension', (request, callback) => { + const entry = request.url.substr('extension://'.length - 1) + + const url = normalize(ExtensionManager.instance.extensionsPath + entry) + callback({ path: url }) + }) +} + +/** + * Set extensions up to run from the extensionPath folder if it is provided and + * load extensions persisted in that folder. + * @param {string} extensionsPath Path to the extensions folder. Required if not yet set up. + * @returns {extensionManager} A set of functions used to manage the extension lifecycle. + */ +export function useExtensions(extensionsPath: string) { + if (!extensionsPath) + throw Error('A path to the extensions folder is required to use extensions') + // Store the path to the extensions folder + ExtensionManager.instance.setExtensionsPath(extensionsPath) + + // Remove any registered extensions + for (const extension of getAllExtensions()) { + if (extension.name) removeExtension(extension.name, false) + } + + // Read extension list from extensions folder + const extensions = JSON.parse( + readFileSync(ExtensionManager.instance.getExtensionsFile(), 'utf-8') + ) + try { + // Create and store a Extension instance for each extension in list + for (const p in extensions) { + loadExtension(extensions[p]) + } + persistExtensions() + } catch (error) { + // Throw meaningful error if extension loading fails + throw new Error( + 'Could not successfully rebuild list of installed extensions.\n' + + error + + '\nPlease check the extensions.json file in the extensions folder.' + ) + } + + // Return the extension lifecycle functions + return getStore() +} + +/** + * Check the given extension object. If it is marked for uninstalling, the extension files are removed. + * Otherwise a Extension instance for the provided object is created and added to the store. + * @private + * @param {Object} ext Extension info + */ +function loadExtension(ext: any) { + // Create new extension, populate it with ext details and save it to the store + const extension = new Extension() + + for (const key in ext) { + if (Object.prototype.hasOwnProperty.call(ext, key)) { + // Use Object.defineProperty to set the properties as writable + Object.defineProperty(extension, key, { + value: ext[key], + writable: true, + enumerable: true, + configurable: true, + }) + } + } + + addExtension(extension, false) + extension.subscribe('pe-persist', persistExtensions) +} + +/** + * Returns the publicly available store functions. + * @returns {extensionManager} A set of functions used to manage the extension lifecycle. + */ +export function getStore() { + if (!ExtensionManager.instance.extensionsPath) { + throw new Error( + 'The extension path has not yet been set up. Please run useExtensions before accessing the store' + ) + } + + return { + installExtensions, + getExtension, + getAllExtensions, + getActiveExtensions, + removeExtension, + } +} diff --git a/electron/extension/store.ts b/electron/extension/store.ts new file mode 100644 index 000000000..4857ef27a --- /dev/null +++ b/electron/extension/store.ts @@ -0,0 +1,135 @@ +/** + * Provides access to the extensions stored by Extension Store + * @typedef {Object} extensionManager + * @prop {getExtension} getExtension + * @prop {getAllExtensions} getAllExtensions + * @prop {getActiveExtensions} getActiveExtensions + * @prop {installExtensions} installExtensions + * @prop {removeExtension} removeExtension + */ + +import { writeFileSync } from 'fs' +import Extension from './extension' +import { ExtensionManager } from './../managers/extension' + +/** + * @module store + * @private + */ + +/** + * Register of installed extensions + * @type {Object.} extension - List of installed extensions + */ +const extensions: Record = {} + +/** + * Get a extension from the stored extensions. + * @param {string} name Name of the extension to retrieve + * @returns {Extension} Retrieved extension + * @alias extensionManager.getExtension + */ +export function getExtension(name: string) { + if (!Object.prototype.hasOwnProperty.call(extensions, name)) { + throw new Error(`Extension ${name} does not exist`) + } + + return extensions[name] +} + +/** + * Get list of all extension objects. + * @returns {Array.} All extension objects + * @alias extensionManager.getAllExtensions + */ +export function getAllExtensions() { + return Object.values(extensions) +} + +/** + * Get list of active extension objects. + * @returns {Array.} Active extension objects + * @alias extensionManager.getActiveExtensions + */ +export function getActiveExtensions() { + return Object.values(extensions).filter((extension) => extension.active) +} + +/** + * Remove extension from store and maybe save stored extensions to file + * @param {string} name Name of the extension to remove + * @param {boolean} persist Whether to save the changes to extensions to file + * @returns {boolean} Whether the delete was successful + * @alias extensionManager.removeExtension + */ +export function removeExtension(name: string, persist = true) { + const del = delete extensions[name] + if (persist) persistExtensions() + return del +} + +/** + * Add extension to store and maybe save stored extensions to file + * @param {Extension} extension Extension to add to store + * @param {boolean} persist Whether to save the changes to extensions to file + * @returns {void} + */ +export function addExtension(extension: Extension, persist = true) { + if (extension.name) extensions[extension.name] = extension + if (persist) { + persistExtensions() + extension.subscribe('pe-persist', persistExtensions) + } +} + +/** + * Save stored extensions to file + * @returns {void} + */ +export function persistExtensions() { + const persistData: Record = {} + for (const name in extensions) { + persistData[name] = extensions[name] + } + writeFileSync( + ExtensionManager.instance.getExtensionsFile(), + JSON.stringify(persistData), + 'utf8' + ) +} + +/** + * Create and install a new extension for the given specifier. + * @param {Array.} extensions A list of NPM specifiers, or installation configuration objects. + * @param {boolean} [store=true] Whether to store the installed extensions in the store + * @returns {Promise.>} New extension + * @alias extensionManager.installExtensions + */ +export async function installExtensions(extensions: any, store = true) { + const installed: Extension[] = [] + for (const ext of extensions) { + // Set install options and activation based on input type + const isObject = typeof ext === 'object' + const spec = isObject ? [ext.specifier, ext] : [ext] + const activate = isObject ? ext.activate !== false : true + + // Install and possibly activate extension + const extension = new Extension(...spec) + await extension._install() + if (activate) extension.setActive(true) + + // Add extension to store if needed + if (store) addExtension(extension) + installed.push(extension) + } + + // Return list of all installed extensions + return installed +} + +/** + * @typedef {Object.} installOptions The {@link https://www.npmjs.com/package/pacote|pacote} + * options used to install the extension with some extra options. + * @param {string} specifier the NPM specifier that identifies the package. + * @param {boolean} [activate] Whether this extension should be activated after installation. Defaults to true. + */ diff --git a/electron/handlers/app.ts b/electron/handlers/app.ts index 3738a7970..adbc875b2 100644 --- a/electron/handlers/app.ts +++ b/electron/handlers/app.ts @@ -1,8 +1,9 @@ -import { app, ipcMain, shell } from "electron"; -import { ModuleManager } from "../managers/module"; -import { join } from "path"; -import { PluginManager } from "../managers/plugin"; -import { WindowManager } from "../managers/window"; +import { app, ipcMain, shell } from 'electron' +import { ModuleManager } from './../managers/module' +import { join } from 'path' +import { ExtensionManager } from './../managers/extension' +import { WindowManager } from './../managers/window' +import { userSpacePath } from './../utils/path' export function handleAppIPCs() { /** @@ -10,57 +11,58 @@ export function handleAppIPCs() { * If the `coreAPI` object is not available, the function returns `undefined`. * @returns A Promise that resolves with the path to the app data directory, or `undefined` if the `coreAPI` object is not available. */ - ipcMain.handle("appDataPath", async (_event) => { - return app.getPath("userData"); - }); + ipcMain.handle('appDataPath', async (_event) => { + return app.getPath('userData') + }) /** * Returns the version of the app. * @param _event - The IPC event object. * @returns The version of the app. */ - ipcMain.handle("appVersion", async (_event) => { - return app.getVersion(); - }); + ipcMain.handle('appVersion', async (_event) => { + return app.getVersion() + }) /** * Handles the "openAppDirectory" IPC message by opening the app's user data directory. * The `shell.openPath` method is used to open the directory in the user's default file explorer. * @param _event - The IPC event object. */ - ipcMain.handle("openAppDirectory", async (_event) => { - const userSpacePath = join(app.getPath('home'), 'jan') - shell.openPath(userSpacePath); - }); + ipcMain.handle('openAppDirectory', async (_event) => { + shell.openPath(userSpacePath) + }) /** * Opens a URL in the user's default browser. * @param _event - The IPC event object. * @param url - The URL to open. */ - ipcMain.handle("openExternalUrl", async (_event, url) => { - shell.openExternal(url); - }); + ipcMain.handle('openExternalUrl', async (_event, url) => { + shell.openExternal(url) + }) /** * Relaunches the app in production - reload window in development. * @param _event - The IPC event object. * @param url - The URL to reload. */ - ipcMain.handle("relaunch", async (_event, url) => { - ModuleManager.instance.clearImportedModules(); + ipcMain.handle('relaunch', async (_event, url) => { + ModuleManager.instance.clearImportedModules() if (app.isPackaged) { - app.relaunch(); - app.exit(); + app.relaunch() + app.exit() } else { for (const modulePath in ModuleManager.instance.requiredModules) { delete require.cache[ - require.resolve(join(app.getPath("userData"), "plugins", modulePath)) - ]; + require.resolve( + join(userSpacePath, 'extensions', modulePath) + ) + ] } - PluginManager.instance.setupPlugins(); - WindowManager.instance.currentWindow?.reload(); + ExtensionManager.instance.setupExtensions() + WindowManager.instance.currentWindow?.reload() } - }); + }) } diff --git a/electron/handlers/download.ts b/electron/handlers/download.ts index 316576e89..1776fccd9 100644 --- a/electron/handlers/download.ts +++ b/electron/handlers/download.ts @@ -1,9 +1,10 @@ import { app, ipcMain } from 'electron' -import { DownloadManager } from '../managers/download' +import { DownloadManager } from './../managers/download' import { resolve, join } from 'path' -import { WindowManager } from '../managers/window' +import { WindowManager } from './../managers/window' import request from 'request' -import { createWriteStream, unlink } from 'fs' +import { createWriteStream } from 'fs' +import { getResourcePath } from './../utils/path' const progress = require('request-progress') export function handleDownloaderIPCs() { @@ -37,6 +38,10 @@ export function handleDownloaderIPCs() { rq?.abort() }) + ipcMain.handle('getResourcePath', async (_event) => { + return getResourcePath() + }) + /** * Downloads a file from a given URL. * @param _event - The IPC event object. diff --git a/electron/handlers/extension.ts b/electron/handlers/extension.ts new file mode 100644 index 000000000..5c2c13ff4 --- /dev/null +++ b/electron/handlers/extension.ts @@ -0,0 +1,124 @@ +import { ipcMain, webContents } from 'electron' +import { readdirSync } from 'fs' +import { ModuleManager } from './../managers/module' +import { join, extname } from 'path' +import { + getActiveExtensions, + getAllExtensions, + installExtensions, +} from './../extension/store' +import { getExtension } from './../extension/store' +import { removeExtension } from './../extension/store' +import Extension from './../extension/extension' +import { getResourcePath, userSpacePath } from './../utils/path' + +export function handleExtensionIPCs() { + /**MARK: General handlers */ + /** + * Invokes a function from a extension module in main node process. + * @param _event - The IPC event object. + * @param modulePath - The path to the extension module. + * @param method - The name of the function to invoke. + * @param args - The arguments to pass to the function. + * @returns The result of the invoked function. + */ + ipcMain.handle( + 'extension:invokeExtensionFunc', + async (_event, modulePath, method, ...args) => { + const module = require( + /* webpackIgnore: true */ join(userSpacePath, 'extensions', modulePath) + ) + ModuleManager.instance.setModule(modulePath, module) + + if (typeof module[method] === 'function') { + return module[method](...args) + } else { + console.debug(module[method]) + console.error(`Function "${method}" does not exist in the module.`) + } + } + ) + + /** + * Returns the paths of the base extensions. + * @param _event - The IPC event object. + * @returns An array of paths to the base extensions. + */ + ipcMain.handle('extension:baseExtensions', async (_event) => { + const baseExtensionPath = join(getResourcePath(), 'pre-install') + return readdirSync(baseExtensionPath) + .filter((file) => extname(file) === '.tgz') + .map((file) => join(baseExtensionPath, file)) + }) + + /** + * Returns the path to the user's extension directory. + * @param _event - The IPC event extension. + * @returns The path to the user's extension directory. + */ + ipcMain.handle('extension:extensionPath', async (_event) => { + return join(userSpacePath, 'extensions') + }) + + /**MARK: Extension Manager handlers */ + ipcMain.handle('extension:install', async (e, extensions) => { + // Install and activate all provided extensions + const installed = await installExtensions(extensions) + return JSON.parse(JSON.stringify(installed)) + }) + + // Register IPC route to uninstall a extension + ipcMain.handle('extension:uninstall', async (e, extensions, reload) => { + // Uninstall all provided extensions + for (const ext of extensions) { + const extension = getExtension(ext) + await extension.uninstall() + if (extension.name) removeExtension(extension.name) + } + + // Reload all renderer pages if needed + reload && webContents.getAllWebContents().forEach((wc) => wc.reload()) + return true + }) + + // Register IPC route to update a extension + ipcMain.handle('extension:update', async (e, extensions, reload) => { + // Update all provided extensions + const updated: Extension[] = [] + for (const ext of extensions) { + const extension = getExtension(ext) + const res = await extension.update() + if (res) updated.push(extension) + } + + // Reload all renderer pages if needed + if (updated.length && reload) + webContents.getAllWebContents().forEach((wc) => wc.reload()) + + return JSON.parse(JSON.stringify(updated)) + }) + + // Register IPC route to check if updates are available for a extension + ipcMain.handle('extension:updatesAvailable', (e, names) => { + const extensions = names + ? names.map((name: string) => getExtension(name)) + : getAllExtensions() + + const updates: Record = {} + for (const extension of extensions) { + updates[extension.name] = extension.isUpdateAvailable() + } + return updates + }) + + // Register IPC route to get the list of active extensions + ipcMain.handle('extension:getActiveExtensions', () => { + return JSON.parse(JSON.stringify(getActiveExtensions())) + }) + + // Register IPC route to toggle the active state of a extension + ipcMain.handle('extension:toggleExtensionActive', (e, plg, active) => { + const extension = getExtension(plg) + return JSON.parse(JSON.stringify(extension.setActive(active))) + }) +} diff --git a/electron/handlers/fs.ts b/electron/handlers/fs.ts index 3e15266ad..16cef6eb6 100644 --- a/electron/handlers/fs.ts +++ b/electron/handlers/fs.ts @@ -1,14 +1,14 @@ -import { app, ipcMain } from 'electron' +import { ipcMain } from 'electron' import * as fs from 'fs' +import fse from 'fs-extra' import { join } from 'path' import readline from 'readline' +import { userSpacePath } from './../utils/path' /** * Handles file system operations. */ export function handleFsIPCs() { - const userSpacePath = join(app.getPath('home'), 'jan') - /** * Gets the path to the user data directory. * @param event - The event object. @@ -146,6 +146,12 @@ export function handleFsIPCs() { } }) + ipcMain.handle('copyFile', async (_event, src: string, dest: string) => { + console.debug(`Copying file from ${src} to ${dest}`) + + return fse.copySync(src, dest, { overwrite: false }) + }) + /** * Reads a file line by line. * @param event - The event object. diff --git a/electron/handlers/plugin.ts b/electron/handlers/plugin.ts deleted file mode 100644 index 22bf253e6..000000000 --- a/electron/handlers/plugin.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { app, ipcMain } from "electron"; -import { readdirSync, rmdir, writeFileSync } from "fs"; -import { ModuleManager } from "../managers/module"; -import { join, extname } from "path"; -import { PluginManager } from "../managers/plugin"; -import { WindowManager } from "../managers/window"; -import { manifest, tarball } from "pacote"; - -export function handlePluginIPCs() { - /** - * Invokes a function from a plugin module in main node process. - * @param _event - The IPC event object. - * @param modulePath - The path to the plugin module. - * @param method - The name of the function to invoke. - * @param args - The arguments to pass to the function. - * @returns The result of the invoked function. - */ - ipcMain.handle( - "invokePluginFunc", - async (_event, modulePath, method, ...args) => { - const module = require( - /* webpackIgnore: true */ join( - app.getPath("userData"), - "plugins", - modulePath - ) - ); - ModuleManager.instance.setModule(modulePath, module); - - if (typeof module[method] === "function") { - return module[method](...args); - } else { - console.debug(module[method]); - console.error(`Function "${method}" does not exist in the module.`); - } - } - ); - - /** - * Returns the paths of the base plugins. - * @param _event - The IPC event object. - * @returns An array of paths to the base plugins. - */ - ipcMain.handle("basePlugins", async (_event) => { - const basePluginPath = join( - __dirname, - "../", - app.isPackaged - ? "../../app.asar.unpacked/core/pre-install" - : "../core/pre-install" - ); - return readdirSync(basePluginPath) - .filter((file) => extname(file) === ".tgz") - .map((file) => join(basePluginPath, file)); - }); - - /** - * Returns the path to the user's plugin directory. - * @param _event - The IPC event object. - * @returns The path to the user's plugin directory. - */ - ipcMain.handle("pluginPath", async (_event) => { - return join(app.getPath("userData"), "plugins"); - }); - - /** - * Deletes the `plugins` directory in the user data path and disposes of required modules. - * If the app is packaged, the function relaunches the app and exits. - * Otherwise, the function deletes the cached modules and sets up the plugins and reloads the main window. - * @param _event - The IPC event object. - * @param url - The URL to reload. - */ - ipcMain.handle("reloadPlugins", async (_event, url) => { - const userDataPath = app.getPath("userData"); - const fullPath = join(userDataPath, "plugins"); - - rmdir(fullPath, { recursive: true }, function (err) { - if (err) console.error(err); - ModuleManager.instance.clearImportedModules(); - - // just relaunch if packaged, should launch manually in development mode - if (app.isPackaged) { - app.relaunch(); - app.exit(); - } else { - for (const modulePath in ModuleManager.instance.requiredModules) { - delete require.cache[ - require.resolve( - join(app.getPath("userData"), "plugins", modulePath) - ) - ]; - } - PluginManager.instance.setupPlugins(); - WindowManager.instance.currentWindow?.reload(); - } - }); - }); - - /** - * Installs a remote plugin by downloading its tarball and writing it to a tgz file. - * @param _event - The IPC event object. - * @param pluginName - The name of the remote plugin to install. - * @returns A Promise that resolves to the path of the installed plugin file. - */ - ipcMain.handle("installRemotePlugin", async (_event, pluginName) => { - const destination = join( - app.getPath("userData"), - pluginName.replace(/^@.*\//, "") + ".tgz" - ); - return manifest(pluginName) - .then(async (manifest: any) => { - await tarball(manifest._resolved).then((data: Buffer) => { - writeFileSync(destination, data); - }); - }) - .then(() => destination); - }); -} diff --git a/electron/handlers/update.ts b/electron/handlers/update.ts index 340db54b9..08d32fffe 100644 --- a/electron/handlers/update.ts +++ b/electron/handlers/update.ts @@ -1,5 +1,5 @@ import { app, dialog } from "electron"; -import { WindowManager } from "../managers/window"; +import { WindowManager } from "./../managers/window"; import { autoUpdater } from "electron-updater"; export function handleAppUpdates() { diff --git a/electron/invokers/app.ts b/electron/invokers/app.ts new file mode 100644 index 000000000..a5bc028c2 --- /dev/null +++ b/electron/invokers/app.ts @@ -0,0 +1,62 @@ +import { shell } from 'electron' + +const { ipcRenderer } = require('electron') + +export function appInvokers() { + const interfaces = { + /** + * Sets the native theme to light. + */ + setNativeThemeLight: () => ipcRenderer.invoke('setNativeThemeLight'), + + /** + * Sets the native theme to dark. + */ + setNativeThemeDark: () => ipcRenderer.invoke('setNativeThemeDark'), + + /** + * Sets the native theme to system default. + */ + setNativeThemeSystem: () => ipcRenderer.invoke('setNativeThemeSystem'), + + /** + * Retrieves the application data path. + * @returns {Promise} A promise that resolves to the application data path. + */ + appDataPath: () => ipcRenderer.invoke('appDataPath'), + + /** + * Retrieves the application version. + * @returns {Promise} A promise that resolves to the application version. + */ + appVersion: () => ipcRenderer.invoke('appVersion'), + + /** + * Opens an external URL. + * @param {string} url - The URL to open. + * @returns {Promise} A promise that resolves when the URL has been opened. + */ + openExternalUrl: (url: string) => + ipcRenderer.invoke('openExternalUrl', url), + + /** + * Relaunches the application. + * @returns {Promise} A promise that resolves when the application has been relaunched. + */ + relaunch: () => ipcRenderer.invoke('relaunch'), + + /** + * Opens the application directory. + * @returns {Promise} A promise that resolves when the application directory has been opened. + */ + openAppDirectory: () => ipcRenderer.invoke('openAppDirectory'), + + /** + * Opens the file explorer at a specific path. + * @param {string} path - The path to open in the file explorer. + */ + openFileExplorer: (path: string) => shell.openPath(path), + } + + return interfaces +} diff --git a/electron/invokers/download.ts b/electron/invokers/download.ts new file mode 100644 index 000000000..d99def3fd --- /dev/null +++ b/electron/invokers/download.ts @@ -0,0 +1,77 @@ +const { ipcRenderer } = require('electron') + +export function downloadInvokers() { + const interfaces = { + /** + * Opens the file explorer at a specific path. + * @param {string} path - The path to open in the file explorer. + */ + downloadFile: (url: string, path: string) => + ipcRenderer.invoke('downloadFile', url, path), + + /** + * Pauses the download of a file. + * @param {string} fileName - The name of the file whose download should be paused. + */ + pauseDownload: (fileName: string) => + ipcRenderer.invoke('pauseDownload', fileName), + + /** + * Pauses the download of a file. + * @param {string} fileName - The name of the file whose download should be paused. + */ + resumeDownload: (fileName: string) => + ipcRenderer.invoke('resumeDownload', fileName), + + /** + * Pauses the download of a file. + * @param {string} fileName - The name of the file whose download should be paused. + */ + abortDownload: (fileName: string) => + ipcRenderer.invoke('abortDownload', fileName), + + /** + * Pauses the download of a file. + * @param {string} fileName - The name of the file whose download should be paused. + */ + onFileDownloadUpdate: (callback: any) => + ipcRenderer.on('FILE_DOWNLOAD_UPDATE', callback), + + /** + * Listens for errors on file downloads. + * @param {Function} callback - The function to call when there is an error. + */ + onFileDownloadError: (callback: any) => + ipcRenderer.on('FILE_DOWNLOAD_ERROR', callback), + + /** + * Listens for the successful completion of file downloads. + * @param {Function} callback - The function to call when a download is complete. + */ + onFileDownloadSuccess: (callback: any) => + ipcRenderer.on('FILE_DOWNLOAD_COMPLETE', callback), + + /** + * Listens for updates on app update downloads. + * @param {Function} callback - The function to call when there is an update. + */ + onAppUpdateDownloadUpdate: (callback: any) => + ipcRenderer.on('APP_UPDATE_PROGRESS', callback), + + /** + * Listens for errors on app update downloads. + * @param {Function} callback - The function to call when there is an error. + */ + onAppUpdateDownloadError: (callback: any) => + ipcRenderer.on('APP_UPDATE_ERROR', callback), + + /** + * Listens for the successful completion of app update downloads. + * @param {Function} callback - The function to call when an update download is complete. + */ + onAppUpdateDownloadSuccess: (callback: any) => + ipcRenderer.on('APP_UPDATE_COMPLETE', callback), + } + + return interfaces +} diff --git a/electron/invokers/extension.ts b/electron/invokers/extension.ts new file mode 100644 index 000000000..c575f8add --- /dev/null +++ b/electron/invokers/extension.ts @@ -0,0 +1,78 @@ +const { ipcRenderer } = require('electron') + +export function extensionInvokers() { + const interfaces = { + /** + * Installs the given extensions. + * @param {any[]} extensions - The extensions to install. + */ + install(extensions: any[]) { + return ipcRenderer.invoke('extension:install', extensions) + }, + /** + * Uninstalls the given extensions. + * @param {any[]} extensions - The extensions to uninstall. + * @param {boolean} reload - Whether to reload after uninstalling. + */ + uninstall(extensions: any[], reload: boolean) { + return ipcRenderer.invoke('extension:uninstall', extensions, reload) + }, + /** + * Retrieves the active extensions. + */ + getActive() { + return ipcRenderer.invoke('extension:getActiveExtensions') + }, + /** + * Updates the given extensions. + * @param {any[]} extensions - The extensions to update. + * @param {boolean} reload - Whether to reload after updating. + */ + update(extensions: any[], reload: boolean) { + return ipcRenderer.invoke('extension:update', extensions, reload) + }, + /** + * Checks if updates are available for the given extension. + * @param {any} extension - The extension to check for updates. + */ + updatesAvailable(extension: any) { + return ipcRenderer.invoke('extension:updatesAvailable', extension) + }, + /** + * Toggles the active state of the given extension. + * @param {any} extension - The extension to toggle. + * @param {boolean} active - The new active state. + */ + toggleActive(extension: any, active: boolean) { + return ipcRenderer.invoke( + 'extension:toggleExtensionActive', + extension, + active + ) + }, + + /** + * Invokes a function of the given extension. + * @param {any} extension - The extension whose function should be invoked. + * @param {any} method - The function to invoke. + * @param {any[]} args - The arguments to pass to the function. + */ + invokeExtensionFunc: (extension: any, method: any, ...args: any[]) => + ipcRenderer.invoke( + 'extension:invokeExtensionFunc', + extension, + method, + ...args + ), + /** + * Retrieves the base extensions. + */ + baseExtensions: () => ipcRenderer.invoke('extension:baseExtensions'), + /** + * Retrieves the extension path. + */ + extensionPath: () => ipcRenderer.invoke('extension:extensionPath'), + } + + return interfaces +} diff --git a/electron/invokers/fs.ts b/electron/invokers/fs.ts new file mode 100644 index 000000000..309562ad6 --- /dev/null +++ b/electron/invokers/fs.ts @@ -0,0 +1,87 @@ +const { ipcRenderer } = require('electron') + +export function fsInvokers() { + const interfaces = { + /** + * Deletes a file at the specified path. + * @param {string} filePath - The path of the file to delete. + */ + deleteFile: (filePath: string) => + ipcRenderer.invoke('deleteFile', filePath), + + /** + * Checks if the path points to a directory. + * @param {string} filePath - The path to check. + */ + isDirectory: (filePath: string) => + ipcRenderer.invoke('isDirectory', filePath), + + /** + * Retrieves the user's space. + */ + getUserSpace: () => ipcRenderer.invoke('getUserSpace'), + + /** + * Reads a file at the specified path. + * @param {string} path - The path of the file to read. + */ + readFile: (path: string) => ipcRenderer.invoke('readFile', path), + + /** + * Writes data to a file at the specified path. + * @param {string} path - The path of the file to write to. + * @param {string} data - The data to write. + */ + writeFile: (path: string, data: string) => + ipcRenderer.invoke('writeFile', path, data), + + /** + * Lists the files in a directory at the specified path. + * @param {string} path - The path of the directory to list files from. + */ + listFiles: (path: string) => ipcRenderer.invoke('listFiles', path), + + /** + * Appends data to a file at the specified path. + * @param {string} path - The path of the file to append to. + * @param {string} data - The data to append. + */ + appendFile: (path: string, data: string) => + ipcRenderer.invoke('appendFile', path, data), + + /** + * Reads a file line by line at the specified path. + * @param {string} path - The path of the file to read. + */ + readLineByLine: (path: string) => + ipcRenderer.invoke('readLineByLine', path), + + /** + * Creates a directory at the specified path. + * @param {string} path - The path where the directory should be created. + */ + mkdir: (path: string) => ipcRenderer.invoke('mkdir', path), + + /** + * Removes a directory at the specified path. + * @param {string} path - The path of the directory to remove. + */ + rmdir: (path: string) => ipcRenderer.invoke('rmdir', path), + + /** + * Copies a file from the source path to the destination path. + * @param {string} src - The source path of the file to copy. + * @param {string} dest - The destination path where the file should be copied. + */ + copyFile: (src: string, dest: string) => ipcRenderer.invoke('copyFile', src, dest), + + /** + * Retrieves the resource path. + * @returns {Promise} A promise that resolves to the resource path. + */ + getResourcePath: () => ipcRenderer.invoke('getResourcePath'), + + } + + return interfaces +} diff --git a/electron/main.ts b/electron/main.ts index 5f1d6b086..189836866 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -1,28 +1,30 @@ import { app, BrowserWindow } from 'electron' import { join } from 'path' import { setupMenu } from './utils/menu' -import { handleFsIPCs } from './handlers/fs' +import { createUserSpace, getResourcePath } from './utils/path' /** * Managers **/ import { WindowManager } from './managers/window' import { ModuleManager } from './managers/module' -import { PluginManager } from './managers/plugin' +import { ExtensionManager } from './managers/extension' /** * IPC Handlers **/ import { handleDownloaderIPCs } from './handlers/download' import { handleThemesIPCs } from './handlers/theme' -import { handlePluginIPCs } from './handlers/plugin' +import { handleExtensionIPCs } from './handlers/extension' import { handleAppIPCs } from './handlers/app' import { handleAppUpdates } from './handlers/update' +import { handleFsIPCs } from './handlers/fs' app .whenReady() - .then(PluginManager.instance.migratePlugins) - .then(PluginManager.instance.setupPlugins) + .then(createUserSpace) + .then(ExtensionManager.instance.migrateExtensions) + .then(ExtensionManager.instance.setupExtensions) .then(setupMenu) .then(handleIPCs) .then(handleAppUpdates) @@ -56,7 +58,7 @@ function createMainWindow() { }) const startURL = app.isPackaged - ? `file://${join(__dirname, '../renderer/index.html')}` + ? `file://${join(__dirname, '..', 'renderer', 'index.html')}` : 'http://localhost:3000' /* Load frontend app to the window */ @@ -78,6 +80,6 @@ function handleIPCs() { handleFsIPCs() handleDownloaderIPCs() handleThemesIPCs() - handlePluginIPCs() + handleExtensionIPCs() handleAppIPCs() } diff --git a/electron/managers/extension.ts b/electron/managers/extension.ts new file mode 100644 index 000000000..7eef24877 --- /dev/null +++ b/electron/managers/extension.ts @@ -0,0 +1,85 @@ +import { app } from 'electron' +import { init } from './../extension' +import { join, resolve } from 'path' +import { rmdir } from 'fs' +import Store from 'electron-store' +import { existsSync, mkdirSync, writeFileSync } from 'fs' +import { userSpacePath } from './../utils/path' +/** + * Manages extension installation and migration. + */ +export class ExtensionManager { + public static instance: ExtensionManager = new ExtensionManager() + + extensionsPath: string | undefined = undefined + + constructor() { + if (ExtensionManager.instance) { + return ExtensionManager.instance + } + } + + /** + * Sets up the extensions by initializing the `extensions` module with the `confirmInstall` and `extensionsPath` options. + * The `confirmInstall` function always returns `true` to allow extension installation. + * The `extensionsPath` option specifies the path to install extensions to. + */ + setupExtensions() { + init({ + // Function to check from the main process that user wants to install a extension + confirmInstall: async (_extensions: string[]) => { + return true + }, + // Path to install extension to + extensionsPath: join(userSpacePath, 'extensions'), + }) + } + + /** + * Migrates the extensions by deleting the `extensions` directory in the user data path. + * If the `migrated_version` key in the `Store` object does not match the current app version, + * the function deletes the `extensions` directory and sets the `migrated_version` key to the current app version. + * @returns A Promise that resolves when the migration is complete. + */ + migrateExtensions() { + return new Promise((resolve) => { + const store = new Store() + if (store.get('migrated_version') !== app.getVersion()) { + console.debug('start migration:', store.get('migrated_version')) + const fullPath = join(userSpacePath, 'extensions') + + rmdir(fullPath, { recursive: true }, function (err) { + if (err) console.error(err) + store.set('migrated_version', app.getVersion()) + console.debug('migrate extensions done') + resolve(undefined) + }) + } else { + resolve(undefined) + } + }) + } + + setExtensionsPath(extPath: string) { + // Create folder if it does not exist + let extDir + try { + extDir = resolve(extPath) + if (extDir.length < 2) throw new Error() + + if (!existsSync(extDir)) mkdirSync(extDir) + + const extensionsJson = join(extDir, 'extensions.json') + if (!existsSync(extensionsJson)) + writeFileSync(extensionsJson, '{}', 'utf8') + + this.extensionsPath = extDir + } catch (error) { + throw new Error('Invalid path provided to the extensions folder') + } + } + + getExtensionsFile() { + return join(this.extensionsPath ?? '', 'extensions.json') + } +} diff --git a/electron/managers/module.ts b/electron/managers/module.ts index 43dda0fb6..dc16d0d22 100644 --- a/electron/managers/module.ts +++ b/electron/managers/module.ts @@ -1,4 +1,4 @@ -import { dispose } from "../utils/disposable"; +import { dispose } from "./../utils/disposable"; /** * Manages imported modules. diff --git a/electron/managers/plugin.ts b/electron/managers/plugin.ts deleted file mode 100644 index 227eab34e..000000000 --- a/electron/managers/plugin.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { app } from "electron"; -import { init } from "../core/plugin/index"; -import { join } from "path"; -import { rmdir } from "fs"; -import Store from "electron-store"; - -/** - * Manages plugin installation and migration. - */ -export class PluginManager { - public static instance: PluginManager = new PluginManager(); - - constructor() { - if (PluginManager.instance) { - return PluginManager.instance; - } - } - - /** - * Sets up the plugins by initializing the `plugins` module with the `confirmInstall` and `pluginsPath` options. - * The `confirmInstall` function always returns `true` to allow plugin installation. - * The `pluginsPath` option specifies the path to install plugins to. - */ - setupPlugins() { - init({ - // Function to check from the main process that user wants to install a plugin - confirmInstall: async (_plugins: string[]) => { - return true; - }, - // Path to install plugin to - pluginsPath: join(app.getPath("userData"), "plugins"), - }); - } - - /** - * Migrates the plugins by deleting the `plugins` directory in the user data path. - * If the `migrated_version` key in the `Store` object does not match the current app version, - * the function deletes the `plugins` directory and sets the `migrated_version` key to the current app version. - * @returns A Promise that resolves when the migration is complete. - */ - migratePlugins() { - return new Promise((resolve) => { - const store = new Store(); - if (store.get("migrated_version") !== app.getVersion()) { - console.debug("start migration:", store.get("migrated_version")); - const userDataPath = app.getPath("userData"); - const fullPath = join(userDataPath, "plugins"); - - rmdir(fullPath, { recursive: true }, function (err) { - if (err) console.error(err); - store.set("migrated_version", app.getVersion()); - console.debug("migrate plugins done"); - resolve(undefined); - }); - } else { - resolve(undefined); - } - }); - } -} diff --git a/electron/package.json b/electron/package.json index 9b172eebd..627f5ad54 100644 --- a/electron/package.json +++ b/electron/package.json @@ -13,10 +13,12 @@ "renderer/**/*", "build/*.{js,map}", "build/**/*.{js,map}", - "core/pre-install" + "pre-install", + "models/**/*" ], "asarUnpack": [ - "core/pre-install" + "pre-install", + "models" ], "publish": [ { @@ -70,6 +72,7 @@ "@uiball/loaders": "^1.3.0", "electron-store": "^8.1.0", "electron-updater": "^6.1.4", + "fs-extra": "^11.2.0", "pacote": "^17.0.4", "request": "^2.88.2", "request-progress": "^3.0.0", diff --git a/electron/playwright.config.ts b/electron/playwright.config.ts index fb23c8cd1..98b2c7b45 100644 --- a/electron/playwright.config.ts +++ b/electron/playwright.config.ts @@ -2,7 +2,6 @@ import { PlaywrightTestConfig } from "@playwright/test"; const config: PlaywrightTestConfig = { testDir: "./tests", - testIgnore: "./core/**", retries: 0, timeout: 120000, }; diff --git a/electron/core/pre-install/.gitkeep b/electron/pre-install/.gitkeep similarity index 100% rename from electron/core/pre-install/.gitkeep rename to electron/pre-install/.gitkeep diff --git a/electron/preload.ts b/electron/preload.ts index a72d6a5cb..a335f6ce2 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -1,147 +1,21 @@ /** * Exposes a set of APIs to the renderer process via the contextBridge object. - * @remarks - * This module is used to make Pluggable Electron's facade available to the renderer on window.plugins. * @module preload */ -/** - * Exposes a set of APIs to the renderer process via the contextBridge object. - * @remarks - * This module is used to make Pluggable Electron's facade available to the renderer on window.plugins. - * @function useFacade - * @memberof module:preload - * @returns {void} - */ +// TODO: Refactor this file for less dependencies and more modularity +// TODO: Most of the APIs should be done using RestAPIs from extensions -/** - * Exposes a set of APIs to the renderer process via the contextBridge object. - * @remarks - * This module is used to make Pluggable Electron's facade available to the renderer on window.plugins. - * @namespace electronAPI - * @memberof module:preload - * @property {Function} invokePluginFunc - Invokes a plugin function with the given arguments. - * @property {Function} setNativeThemeLight - Sets the native theme to light. - * @property {Function} setNativeThemeDark - Sets the native theme to dark. - * @property {Function} setNativeThemeSystem - Sets the native theme to system. - * @property {Function} basePlugins - Returns the base plugins. - * @property {Function} pluginPath - Returns the plugin path. - * @property {Function} appDataPath - Returns the app data path. - * @property {Function} reloadPlugins - Reloads the plugins. - * @property {Function} appVersion - Returns the app version. - * @property {Function} openExternalUrl - Opens the given URL in the default browser. - * @property {Function} relaunch - Relaunches the app. - * @property {Function} openAppDirectory - Opens the app directory. - * @property {Function} deleteFile - Deletes the file at the given path. - * @property {Function} isDirectory - Returns true if the file at the given path is a directory. - * @property {Function} getUserSpace - Returns the user space. - * @property {Function} readFile - Reads the file at the given path. - * @property {Function} writeFile - Writes the given data to the file at the given path. - * @property {Function} listFiles - Lists the files in the directory at the given path. - * @property {Function} appendFile - Appends the given data to the file at the given path. - * @property {Function} mkdir - Creates a directory at the given path. - * @property {Function} rmdir - Removes a directory at the given path recursively. - * @property {Function} installRemotePlugin - Installs the remote plugin with the given name. - * @property {Function} downloadFile - Downloads the file at the given URL to the given path. - * @property {Function} pauseDownload - Pauses the download of the file with the given name. - * @property {Function} resumeDownload - Resumes the download of the file with the given name. - * @property {Function} abortDownload - Aborts the download of the file with the given name. - * @property {Function} onFileDownloadUpdate - Registers a callback to be called when a file download is updated. - * @property {Function} onFileDownloadError - Registers a callback to be called when a file download encounters an error. - * @property {Function} onFileDownloadSuccess - Registers a callback to be called when a file download is completed successfully. - * @property {Function} onAppUpdateDownloadUpdate - Registers a callback to be called when an app update download is updated. - * @property {Function} onAppUpdateDownloadError - Registers a callback to be called when an app update download encounters an error. - * @property {Function} onAppUpdateDownloadSuccess - Registers a callback to be called when an app update download is completed successfully. - */ +import { fsInvokers } from './invokers/fs' +import { appInvokers } from './invokers/app' +import { downloadInvokers } from './invokers/download' +import { extensionInvokers } from './invokers/extension' -// Make Pluggable Electron's facade available to the renderer on window.plugins -import { useFacade } from './core/plugin/facade' - -useFacade() - -const { contextBridge, ipcRenderer, shell } = require('electron') +const { contextBridge } = require('electron') contextBridge.exposeInMainWorld('electronAPI', { - invokePluginFunc: (plugin: any, method: any, ...args: any[]) => - ipcRenderer.invoke('invokePluginFunc', plugin, method, ...args), - - setNativeThemeLight: () => ipcRenderer.invoke('setNativeThemeLight'), - - setNativeThemeDark: () => ipcRenderer.invoke('setNativeThemeDark'), - - setNativeThemeSystem: () => ipcRenderer.invoke('setNativeThemeSystem'), - - basePlugins: () => ipcRenderer.invoke('basePlugins'), - - pluginPath: () => ipcRenderer.invoke('pluginPath'), - - appDataPath: () => ipcRenderer.invoke('appDataPath'), - - reloadPlugins: () => ipcRenderer.invoke('reloadPlugins'), - - appVersion: () => ipcRenderer.invoke('appVersion'), - - openExternalUrl: (url: string) => ipcRenderer.invoke('openExternalUrl', url), - - relaunch: () => ipcRenderer.invoke('relaunch'), - - openAppDirectory: () => ipcRenderer.invoke('openAppDirectory'), - - deleteFile: (filePath: string) => ipcRenderer.invoke('deleteFile', filePath), - - isDirectory: (filePath: string) => - ipcRenderer.invoke('isDirectory', filePath), - - getUserSpace: () => ipcRenderer.invoke('getUserSpace'), - - readFile: (path: string) => ipcRenderer.invoke('readFile', path), - - writeFile: (path: string, data: string) => - ipcRenderer.invoke('writeFile', path, data), - - listFiles: (path: string) => ipcRenderer.invoke('listFiles', path), - - appendFile: (path: string, data: string) => - ipcRenderer.invoke('appendFile', path, data), - - readLineByLine: (path: string) => ipcRenderer.invoke('readLineByLine', path), - - mkdir: (path: string) => ipcRenderer.invoke('mkdir', path), - - rmdir: (path: string) => ipcRenderer.invoke('rmdir', path), - - openFileExplorer: (path: string) => shell.openPath(path), - - installRemotePlugin: (pluginName: string) => - ipcRenderer.invoke('installRemotePlugin', pluginName), - - downloadFile: (url: string, path: string) => - ipcRenderer.invoke('downloadFile', url, path), - - pauseDownload: (fileName: string) => - ipcRenderer.invoke('pauseDownload', fileName), - - resumeDownload: (fileName: string) => - ipcRenderer.invoke('resumeDownload', fileName), - - abortDownload: (fileName: string) => - ipcRenderer.invoke('abortDownload', fileName), - - onFileDownloadUpdate: (callback: any) => - ipcRenderer.on('FILE_DOWNLOAD_UPDATE', callback), - - onFileDownloadError: (callback: any) => - ipcRenderer.on('FILE_DOWNLOAD_ERROR', callback), - - onFileDownloadSuccess: (callback: any) => - ipcRenderer.on('FILE_DOWNLOAD_COMPLETE', callback), - - onAppUpdateDownloadUpdate: (callback: any) => - ipcRenderer.on('APP_UPDATE_PROGRESS', callback), - - onAppUpdateDownloadError: (callback: any) => - ipcRenderer.on('APP_UPDATE_ERROR', callback), - - onAppUpdateDownloadSuccess: (callback: any) => - ipcRenderer.on('APP_UPDATE_COMPLETE', callback), + ...extensionInvokers(), + ...downloadInvokers(), + ...fsInvokers(), + ...appInvokers(), }) diff --git a/electron/utils/path.ts b/electron/utils/path.ts new file mode 100644 index 000000000..8f3092561 --- /dev/null +++ b/electron/utils/path.ts @@ -0,0 +1,19 @@ +import { join } from 'path' +import { app } from 'electron' +import { mkdir } from 'fs-extra' + +export async function createUserSpace(): Promise { + return mkdir(userSpacePath).catch(() => {}) +} + +export const userSpacePath = join(app.getPath('home'), 'jan') + +export function getResourcePath() { + let appPath = join(app.getAppPath(), '..', 'app.asar.unpacked') + + if (!app.isPackaged) { + // for development mode + appPath = join(__dirname, '..', '..') + } + return appPath +} diff --git a/plugins/assistant-plugin/README.md b/extensions/assistant-extension/README.md similarity index 100% rename from plugins/assistant-plugin/README.md rename to extensions/assistant-extension/README.md diff --git a/plugins/assistant-plugin/package.json b/extensions/assistant-extension/package.json similarity index 60% rename from plugins/assistant-plugin/package.json rename to extensions/assistant-extension/package.json index 1a850beb6..6f72ddaf3 100644 --- a/plugins/assistant-plugin/package.json +++ b/extensions/assistant-extension/package.json @@ -1,19 +1,14 @@ { - "name": "@janhq/assistant-plugin", - "version": "1.0.9", - "description": "Assistant", - "icon": "https://raw.githubusercontent.com/tailwindlabs/heroicons/88e98b0c2b458553fbadccddc2d2f878edc0387b/src/20/solid/cpu-chip.svg", + "name": "@janhq/assistant-extension", + "version": "1.0.0", + "description": "Assistant extension", "main": "dist/index.js", "module": "dist/module.js", "author": "Jan ", "license": "AGPL-3.0", - "url": "/plugins/assistant-plugin/index.js", - "activationPoints": [ - "init" - ], "scripts": { "build": "tsc -b . && webpack --config webpack.config.js", - "build:publish": "rimraf *.tgz --glob && npm run build && npm pack && cpx *.tgz ../../electron/core/pre-install" + "build:publish": "rimraf *.tgz --glob && npm run build && npm pack && cpx *.tgz ../../electron/pre-install" }, "devDependencies": { "rimraf": "^3.0.2", diff --git a/plugins/assistant-plugin/src/@types/global.d.ts b/extensions/assistant-extension/src/@types/global.d.ts similarity index 100% rename from plugins/assistant-plugin/src/@types/global.d.ts rename to extensions/assistant-extension/src/@types/global.d.ts diff --git a/plugins/assistant-plugin/src/index.ts b/extensions/assistant-extension/src/index.ts similarity index 80% rename from plugins/assistant-plugin/src/index.ts rename to extensions/assistant-extension/src/index.ts index a286b04bd..7321a0660 100644 --- a/plugins/assistant-plugin/src/index.ts +++ b/extensions/assistant-extension/src/index.ts @@ -1,23 +1,23 @@ -import { PluginType, fs, Assistant } from "@janhq/core"; -import { AssistantPlugin } from "@janhq/core/lib/plugins"; +import { ExtensionType, fs, Assistant } from "@janhq/core"; +import { AssistantExtension } from "@janhq/core"; import { join } from "path"; -export default class JanAssistantPlugin implements AssistantPlugin { +export default class JanAssistantExtension implements AssistantExtension { private static readonly _homeDir = "assistants"; - type(): PluginType { - return PluginType.Assistant; + type(): ExtensionType { + return ExtensionType.Assistant; } onLoad(): void { // making the assistant directory - fs.mkdir(JanAssistantPlugin._homeDir).then(() => { + fs.mkdir(JanAssistantExtension._homeDir).then(() => { this.createJanAssistant(); }); } /** - * Called when the plugin is unloaded. + * Called when the extension is unloaded. */ onUnload(): void {} @@ -26,7 +26,7 @@ export default class JanAssistantPlugin implements AssistantPlugin { // TODO: check if the directory already exists, then ignore creation for now - const assistantDir = join(JanAssistantPlugin._homeDir, assistant.id); + const assistantDir = join(JanAssistantExtension._homeDir, assistant.id); await fs.mkdir(assistantDir); // store the assistant metadata json @@ -46,10 +46,10 @@ export default class JanAssistantPlugin implements AssistantPlugin { // get all the assistant metadata json const results: Assistant[] = []; const allFileName: string[] = await fs.listFiles( - JanAssistantPlugin._homeDir + JanAssistantExtension._homeDir ); for (const fileName of allFileName) { - const filePath = join(JanAssistantPlugin._homeDir, fileName); + const filePath = join(JanAssistantExtension._homeDir, fileName); const isDirectory = await fs.isDirectory(filePath); if (!isDirectory) { // if not a directory, ignore @@ -81,7 +81,7 @@ export default class JanAssistantPlugin implements AssistantPlugin { } // remove the directory - const assistantDir = join(JanAssistantPlugin._homeDir, assistant.id); + const assistantDir = join(JanAssistantExtension._homeDir, assistant.id); await fs.rmdir(assistantDir); return Promise.resolve(); } diff --git a/plugins/assistant-plugin/tsconfig.json b/extensions/assistant-extension/tsconfig.json similarity index 100% rename from plugins/assistant-plugin/tsconfig.json rename to extensions/assistant-extension/tsconfig.json diff --git a/plugins/assistant-plugin/webpack.config.js b/extensions/assistant-extension/webpack.config.js similarity index 100% rename from plugins/assistant-plugin/webpack.config.js rename to extensions/assistant-extension/webpack.config.js diff --git a/plugins/conversational-json/.prettierrc b/extensions/conversational-extension/.prettierrc similarity index 100% rename from plugins/conversational-json/.prettierrc rename to extensions/conversational-extension/.prettierrc diff --git a/plugins/conversational-json/package.json b/extensions/conversational-extension/package.json similarity index 77% rename from plugins/conversational-json/package.json rename to extensions/conversational-extension/package.json index 520970664..0e45e83fd 100644 --- a/plugins/conversational-json/package.json +++ b/extensions/conversational-extension/package.json @@ -1,16 +1,13 @@ { - "name": "@janhq/conversational-json", + "name": "@janhq/conversational-extension", "version": "1.0.0", - "description": "Conversational Plugin - Stores jan app conversations as JSON", + "description": "Conversational Extension - Stores jan app threads and messages in JSON files", "main": "dist/index.js", "author": "Jan ", "license": "MIT", - "activationPoints": [ - "init" - ], "scripts": { "build": "tsc -b . && webpack --config webpack.config.js", - "build:publish": "rimraf *.tgz --glob && npm run build && npm pack && cpx *.tgz ../../electron/core/pre-install" + "build:publish": "rimraf *.tgz --glob && npm run build && npm pack && cpx *.tgz ../../electron/pre-install" }, "exports": { ".": "./dist/index.js", diff --git a/plugins/conversational-json/src/index.ts b/extensions/conversational-extension/src/index.ts similarity index 72% rename from plugins/conversational-json/src/index.ts rename to extensions/conversational-extension/src/index.ts index 500fcb526..4461cfaf7 100644 --- a/plugins/conversational-json/src/index.ts +++ b/extensions/conversational-extension/src/index.ts @@ -1,37 +1,37 @@ -import { PluginType, fs } from '@janhq/core' -import { ConversationalPlugin } from '@janhq/core/lib/plugins' -import { Thread, ThreadMessage } from '@janhq/core/lib/types' +import { ExtensionType, fs } from '@janhq/core' +import { ConversationalExtension } from '@janhq/core' +import { Thread, ThreadMessage } from '@janhq/core' import { join } from 'path' /** - * JSONConversationalPlugin is a ConversationalPlugin implementation that provides + * JSONConversationalExtension is a ConversationalExtension implementation that provides * functionality for managing threads. */ -export default class JSONConversationalPlugin implements ConversationalPlugin { +export default class JSONConversationalExtension implements ConversationalExtension { private static readonly _homeDir = 'threads' private static readonly _threadInfoFileName = 'thread.json' private static readonly _threadMessagesFileName = 'messages.jsonl' /** - * Returns the type of the plugin. + * Returns the type of the extension. */ - type(): PluginType { - return PluginType.Conversational + type(): ExtensionType { + return ExtensionType.Conversational } /** - * Called when the plugin is loaded. + * Called when the extension is loaded. */ onLoad() { - fs.mkdir(JSONConversationalPlugin._homeDir) - console.debug('JSONConversationalPlugin loaded') + fs.mkdir(JSONConversationalExtension._homeDir) + console.debug('JSONConversationalExtension loaded') } /** - * Called when the plugin is unloaded. + * Called when the extension is unloaded. */ onUnload() { - console.debug('JSONConversationalPlugin unloaded') + console.debug('JSONConversationalExtension unloaded') } /** @@ -67,10 +67,10 @@ export default class JSONConversationalPlugin implements ConversationalPlugin { */ async saveThread(thread: Thread): Promise { try { - const threadDirPath = join(JSONConversationalPlugin._homeDir, thread.id) + const threadDirPath = join(JSONConversationalExtension._homeDir, thread.id) const threadJsonPath = join( threadDirPath, - JSONConversationalPlugin._threadInfoFileName + JSONConversationalExtension._threadInfoFileName ) await fs.mkdir(threadDirPath) await fs.writeFile(threadJsonPath, JSON.stringify(thread, null, 2)) @@ -85,18 +85,18 @@ export default class JSONConversationalPlugin implements ConversationalPlugin { * @param threadId The ID of the thread to delete. */ deleteThread(threadId: string): Promise { - return fs.rmdir(join(JSONConversationalPlugin._homeDir, `${threadId}`)) + return fs.rmdir(join(JSONConversationalExtension._homeDir, `${threadId}`)) } async addNewMessage(message: ThreadMessage): Promise { try { const threadDirPath = join( - JSONConversationalPlugin._homeDir, + JSONConversationalExtension._homeDir, message.thread_id ) const threadMessagePath = join( threadDirPath, - JSONConversationalPlugin._threadMessagesFileName + JSONConversationalExtension._threadMessagesFileName ) await fs.mkdir(threadDirPath) await fs.appendFile(threadMessagePath, JSON.stringify(message) + '\n') @@ -111,10 +111,10 @@ export default class JSONConversationalPlugin implements ConversationalPlugin { messages: ThreadMessage[] ): Promise { try { - const threadDirPath = join(JSONConversationalPlugin._homeDir, threadId) + const threadDirPath = join(JSONConversationalExtension._homeDir, threadId) const threadMessagePath = join( threadDirPath, - JSONConversationalPlugin._threadMessagesFileName + JSONConversationalExtension._threadMessagesFileName ) await fs.mkdir(threadDirPath) await fs.writeFile( @@ -135,9 +135,9 @@ export default class JSONConversationalPlugin implements ConversationalPlugin { private async readThread(threadDirName: string): Promise { return fs.readFile( join( - JSONConversationalPlugin._homeDir, + JSONConversationalExtension._homeDir, threadDirName, - JSONConversationalPlugin._threadInfoFileName + JSONConversationalExtension._threadInfoFileName ) ) } @@ -148,12 +148,12 @@ export default class JSONConversationalPlugin implements ConversationalPlugin { */ private async getValidThreadDirs(): Promise { const fileInsideThread: string[] = await fs.listFiles( - JSONConversationalPlugin._homeDir + JSONConversationalExtension._homeDir ) const threadDirs: string[] = [] for (let i = 0; i < fileInsideThread.length; i++) { - const path = join(JSONConversationalPlugin._homeDir, fileInsideThread[i]) + const path = join(JSONConversationalExtension._homeDir, fileInsideThread[i]) const isDirectory = await fs.isDirectory(path) if (!isDirectory) { console.debug(`Ignore ${path} because it is not a directory`) @@ -161,7 +161,7 @@ export default class JSONConversationalPlugin implements ConversationalPlugin { } const isHavingThreadInfo = (await fs.listFiles(path)).includes( - JSONConversationalPlugin._threadInfoFileName + JSONConversationalExtension._threadInfoFileName ) if (!isHavingThreadInfo) { console.debug(`Ignore ${path} because it does not have thread info`) @@ -175,20 +175,20 @@ export default class JSONConversationalPlugin implements ConversationalPlugin { async getAllMessages(threadId: string): Promise { try { - const threadDirPath = join(JSONConversationalPlugin._homeDir, threadId) + const threadDirPath = join(JSONConversationalExtension._homeDir, threadId) const isDir = await fs.isDirectory(threadDirPath) if (!isDir) { throw Error(`${threadDirPath} is not directory`) } const files: string[] = await fs.listFiles(threadDirPath) - if (!files.includes(JSONConversationalPlugin._threadMessagesFileName)) { + if (!files.includes(JSONConversationalExtension._threadMessagesFileName)) { throw Error(`${threadDirPath} not contains message file`) } const messageFilePath = join( threadDirPath, - JSONConversationalPlugin._threadMessagesFileName + JSONConversationalExtension._threadMessagesFileName ) const result = await fs.readLineByLine(messageFilePath) diff --git a/plugins/conversational-json/tsconfig.json b/extensions/conversational-extension/tsconfig.json similarity index 100% rename from plugins/conversational-json/tsconfig.json rename to extensions/conversational-extension/tsconfig.json diff --git a/plugins/conversational-json/webpack.config.js b/extensions/conversational-extension/webpack.config.js similarity index 100% rename from plugins/conversational-json/webpack.config.js rename to extensions/conversational-extension/webpack.config.js diff --git a/plugins/inference-plugin/README.md b/extensions/inference-extension/README.md similarity index 100% rename from plugins/inference-plugin/README.md rename to extensions/inference-extension/README.md diff --git a/plugins/inference-plugin/download.bat b/extensions/inference-extension/download.bat similarity index 100% rename from plugins/inference-plugin/download.bat rename to extensions/inference-extension/download.bat diff --git a/plugins/inference-plugin/nitro/linux-cpu/.gitkeep b/extensions/inference-extension/nitro/linux-cpu/.gitkeep similarity index 100% rename from plugins/inference-plugin/nitro/linux-cpu/.gitkeep rename to extensions/inference-extension/nitro/linux-cpu/.gitkeep diff --git a/plugins/inference-plugin/nitro/linux-cuda/.gitkeep b/extensions/inference-extension/nitro/linux-cuda/.gitkeep similarity index 100% rename from plugins/inference-plugin/nitro/linux-cuda/.gitkeep rename to extensions/inference-extension/nitro/linux-cuda/.gitkeep diff --git a/plugins/inference-plugin/nitro/linux-start.sh b/extensions/inference-extension/nitro/linux-start.sh similarity index 100% rename from plugins/inference-plugin/nitro/linux-start.sh rename to extensions/inference-extension/nitro/linux-start.sh diff --git a/plugins/inference-plugin/nitro/mac-arm64/.gitkeep b/extensions/inference-extension/nitro/mac-arm64/.gitkeep similarity index 100% rename from plugins/inference-plugin/nitro/mac-arm64/.gitkeep rename to extensions/inference-extension/nitro/mac-arm64/.gitkeep diff --git a/plugins/inference-plugin/nitro/mac-x64/.gitkeep b/extensions/inference-extension/nitro/mac-x64/.gitkeep similarity index 100% rename from plugins/inference-plugin/nitro/mac-x64/.gitkeep rename to extensions/inference-extension/nitro/mac-x64/.gitkeep diff --git a/plugins/inference-plugin/nitro/version.txt b/extensions/inference-extension/nitro/version.txt similarity index 100% rename from plugins/inference-plugin/nitro/version.txt rename to extensions/inference-extension/nitro/version.txt diff --git a/plugins/inference-plugin/nitro/win-cpu/.gitkeep b/extensions/inference-extension/nitro/win-cpu/.gitkeep similarity index 100% rename from plugins/inference-plugin/nitro/win-cpu/.gitkeep rename to extensions/inference-extension/nitro/win-cpu/.gitkeep diff --git a/plugins/inference-plugin/nitro/win-cuda/.gitkeep b/extensions/inference-extension/nitro/win-cuda/.gitkeep similarity index 100% rename from plugins/inference-plugin/nitro/win-cuda/.gitkeep rename to extensions/inference-extension/nitro/win-cuda/.gitkeep diff --git a/plugins/inference-plugin/nitro/win-start.bat b/extensions/inference-extension/nitro/win-start.bat similarity index 100% rename from plugins/inference-plugin/nitro/win-start.bat rename to extensions/inference-extension/nitro/win-start.bat diff --git a/plugins/inference-plugin/package.json b/extensions/inference-extension/package.json similarity index 85% rename from plugins/inference-plugin/package.json rename to extensions/inference-extension/package.json index 97d9fc7c0..798d2e46d 100644 --- a/plugins/inference-plugin/package.json +++ b/extensions/inference-extension/package.json @@ -1,25 +1,20 @@ { - "name": "@janhq/inference-plugin", - "version": "1.0.21", - "description": "Inference Plugin, powered by @janhq/nitro, bring a high-performance Llama model inference in pure C++.", + "name": "@janhq/inference-extension", + "version": "1.0.0", + "description": "Inference Extension, powered by @janhq/nitro, bring a high-performance Llama model inference in pure C++.", "main": "dist/index.js", "module": "dist/module.js", "author": "Jan ", "license": "AGPL-3.0", - "supportCloudNative": true, - "url": "/plugins/inference-plugin/index.js", - "activationPoints": [ - "init" - ], "scripts": { "build": "tsc -b . && webpack --config webpack.config.js", "downloadnitro:linux": "NITRO_VERSION=$(cat ./nitro/version.txt) && download https://github.com/janhq/nitro/releases/download/v${NITRO_VERSION}/nitro-${NITRO_VERSION}-linux-amd64.tar.gz -e --strip 1 -o ./nitro/linux-cpu && chmod +x ./nitro/linux-cpu/nitro && chmod +x ./nitro/linux-start.sh && download https://github.com/janhq/nitro/releases/download/v${NITRO_VERSION}/nitro-${NITRO_VERSION}-linux-amd64-cuda.tar.gz -e --strip 1 -o ./nitro/linux-cuda && chmod +x ./nitro/linux-cuda/nitro && chmod +x ./nitro/linux-start.sh", "downloadnitro:darwin": "NITRO_VERSION=$(cat ./nitro/version.txt) && download https://github.com/janhq/nitro/releases/download/v${NITRO_VERSION}/nitro-${NITRO_VERSION}-mac-arm64.tar.gz -e --strip 1 -o ./nitro/mac-arm64 && chmod +x ./nitro/mac-arm64/nitro && download https://github.com/janhq/nitro/releases/download/v${NITRO_VERSION}/nitro-${NITRO_VERSION}-mac-amd64.tar.gz -e --strip 1 -o ./nitro/mac-x64 && chmod +x ./nitro/mac-x64/nitro", "downloadnitro:win32": "download.bat", "downloadnitro": "run-script-os", - "build:publish:darwin": "rimraf *.tgz --glob && npm run build && npm run downloadnitro && ../../.github/scripts/auto-sign.sh && cpx \"nitro/**\" \"dist/nitro\" && npm pack && cpx *.tgz ../../electron/core/pre-install", - "build:publish:win32": "rimraf *.tgz --glob && npm run build && npm run downloadnitro && cpx \"nitro/**\" \"dist/nitro\" && npm pack && cpx *.tgz ../../electron/core/pre-install", - "build:publish:linux": "rimraf *.tgz --glob && npm run build && npm run downloadnitro && cpx \"nitro/**\" \"dist/nitro\" && npm pack && cpx *.tgz ../../electron/core/pre-install", + "build:publish:darwin": "rimraf *.tgz --glob && npm run build && npm run downloadnitro && ../../.github/scripts/auto-sign.sh && cpx \"nitro/**\" \"dist/nitro\" && npm pack && cpx *.tgz ../../electron/pre-install", + "build:publish:win32": "rimraf *.tgz --glob && npm run build && npm run downloadnitro && cpx \"nitro/**\" \"dist/nitro\" && npm pack && cpx *.tgz ../../electron/pre-install", + "build:publish:linux": "rimraf *.tgz --glob && npm run build && npm run downloadnitro && cpx \"nitro/**\" \"dist/nitro\" && npm pack && cpx *.tgz ../../electron/pre-install", "build:publish": "run-script-os" }, "exports": { diff --git a/plugins/inference-plugin/src/@types/global.d.ts b/extensions/inference-extension/src/@types/global.d.ts similarity index 100% rename from plugins/inference-plugin/src/@types/global.d.ts rename to extensions/inference-extension/src/@types/global.d.ts diff --git a/plugins/inference-plugin/src/helpers/sse.ts b/extensions/inference-extension/src/helpers/sse.ts similarity index 100% rename from plugins/inference-plugin/src/helpers/sse.ts rename to extensions/inference-extension/src/helpers/sse.ts diff --git a/plugins/inference-plugin/src/index.ts b/extensions/inference-extension/src/index.ts similarity index 83% rename from plugins/inference-plugin/src/index.ts rename to extensions/inference-extension/src/index.ts index 7a1f85186..e8e7758bb 100644 --- a/plugins/inference-plugin/src/index.ts +++ b/extensions/inference-extension/src/index.ts @@ -1,9 +1,9 @@ /** - * @file This file exports a class that implements the InferencePlugin interface from the @janhq/core package. + * @file This file exports a class that implements the InferenceExtension interface from the @janhq/core package. * The class provides methods for initializing and stopping a model, and for making inference requests. * It also subscribes to events emitted by the @janhq/core package and handles new message requests. * @version 1.0.0 - * @module inference-plugin/src/index + * @module inference-extension/src/index */ import { @@ -13,40 +13,40 @@ import { MessageRequest, MessageStatus, ModelSettingParams, - PluginType, + ExtensionType, ThreadContent, ThreadMessage, events, executeOnMain, getUserSpace, } from "@janhq/core"; -import { InferencePlugin } from "@janhq/core/lib/plugins"; +import { InferenceExtension } from "@janhq/core"; import { requestInference } from "./helpers/sse"; import { ulid } from "ulid"; import { join } from "path"; /** - * A class that implements the InferencePlugin interface from the @janhq/core package. + * A class that implements the InferenceExtension interface from the @janhq/core package. * The class provides methods for initializing and stopping a model, and for making inference requests. * It also subscribes to events emitted by the @janhq/core package and handles new message requests. */ -export default class JanInferencePlugin implements InferencePlugin { +export default class JanInferenceExtension implements InferenceExtension { controller = new AbortController(); isCancelled = false; /** - * Returns the type of the plugin. - * @returns {PluginType} The type of the plugin. + * Returns the type of the extension. + * @returns {ExtensionType} The type of the extension. */ - type(): PluginType { - return PluginType.Inference; + type(): ExtensionType { + return ExtensionType.Inference; } /** * Subscribes to events emitted by the @janhq/core package. */ onLoad(): void { - events.on(EventName.OnNewMessageRequest, (data) => - JanInferencePlugin.handleMessageRequest(data, this) + events.on(EventName.OnMessageSent, (data) => + JanInferenceExtension.handleMessageRequest(data, this) ); } @@ -131,7 +131,7 @@ export default class JanInferencePlugin implements InferencePlugin { */ private static async handleMessageRequest( data: MessageRequest, - instance: JanInferencePlugin + instance: JanInferenceExtension ) { const timestamp = Date.now(); const message: ThreadMessage = { @@ -145,8 +145,7 @@ export default class JanInferencePlugin implements InferencePlugin { updated: timestamp, object: "thread.message", }; - events.emit(EventName.OnNewMessageResponse, message); - console.log(JSON.stringify(data, null, 2)); + events.emit(EventName.OnMessageResponse, message); instance.isCancelled = false; instance.controller = new AbortController(); @@ -161,11 +160,11 @@ export default class JanInferencePlugin implements InferencePlugin { }, }; message.content = [messageContent]; - events.emit(EventName.OnMessageResponseUpdate, message); + events.emit(EventName.OnMessageUpdate, message); }, complete: async () => { message.status = MessageStatus.Ready; - events.emit(EventName.OnMessageResponseFinished, message); + events.emit(EventName.OnMessageUpdate, message); }, error: async (err) => { const messageContent: ThreadContent = { @@ -177,7 +176,7 @@ export default class JanInferencePlugin implements InferencePlugin { }; message.content = [messageContent]; message.status = MessageStatus.Ready; - events.emit(EventName.OnMessageResponseUpdate, message); + events.emit(EventName.OnMessageUpdate, message); }, }); } diff --git a/plugins/inference-plugin/src/module.ts b/extensions/inference-extension/src/module.ts similarity index 100% rename from plugins/inference-plugin/src/module.ts rename to extensions/inference-extension/src/module.ts diff --git a/plugins/inference-plugin/tsconfig.json b/extensions/inference-extension/tsconfig.json similarity index 100% rename from plugins/inference-plugin/tsconfig.json rename to extensions/inference-extension/tsconfig.json diff --git a/plugins/inference-plugin/webpack.config.js b/extensions/inference-extension/webpack.config.js similarity index 100% rename from plugins/inference-plugin/webpack.config.js rename to extensions/inference-extension/webpack.config.js diff --git a/plugins/model-plugin/.prettierrc b/extensions/model-extension/.prettierrc similarity index 100% rename from plugins/model-plugin/.prettierrc rename to extensions/model-extension/.prettierrc diff --git a/plugins/model-plugin/README.md b/extensions/model-extension/README.md similarity index 100% rename from plugins/model-plugin/README.md rename to extensions/model-extension/README.md diff --git a/plugins/model-plugin/package.json b/extensions/model-extension/package.json similarity index 58% rename from plugins/model-plugin/package.json rename to extensions/model-extension/package.json index 43d1ffa8e..73d791de5 100644 --- a/plugins/model-plugin/package.json +++ b/extensions/model-extension/package.json @@ -1,20 +1,14 @@ { - "name": "@janhq/model-plugin", + "name": "@janhq/model-extension", "version": "1.0.13", - "description": "Model Management Plugin provides model exploration and seamless downloads", - "icon": "https://raw.githubusercontent.com/tailwindlabs/heroicons/88e98b0c2b458553fbadccddc2d2f878edc0387b/src/20/solid/queue-list.svg", + "description": "Model Management Extension provides model exploration and seamless downloads", "main": "dist/index.js", "module": "dist/module.js", "author": "Jan ", "license": "AGPL-3.0", - "supportCloudNative": true, - "url": "/plugins/model-plugin/index.js", - "activationPoints": [ - "init" - ], "scripts": { "build": "tsc -b . && webpack --config webpack.config.js", - "build:publish": "rimraf *.tgz --glob && npm run build && npm pack && cpx *.tgz ../../electron/core/pre-install" + "build:publish": "rimraf *.tgz --glob && npm run build && npm pack && cpx *.tgz ../../electron/pre-install" }, "devDependencies": { "cpx": "^1.5.0", diff --git a/extensions/model-extension/src/@types/global.d.ts b/extensions/model-extension/src/@types/global.d.ts new file mode 100644 index 000000000..bb030c762 --- /dev/null +++ b/extensions/model-extension/src/@types/global.d.ts @@ -0,0 +1,2 @@ +declare const PLUGIN_NAME: string +declare const MODULE_PATH: string diff --git a/extensions/model-extension/src/index.ts b/extensions/model-extension/src/index.ts new file mode 100644 index 000000000..d0267b84e --- /dev/null +++ b/extensions/model-extension/src/index.ts @@ -0,0 +1,219 @@ +import { + ExtensionType, + fs, + downloadFile, + abortDownload, + getResourcePath, + getUserSpace, +} from '@janhq/core' +import { ModelExtension, Model, ModelState } from '@janhq/core' +import { join } from 'path' + +/** + * A extension for models + */ +export default class JanModelExtension implements ModelExtension { + private static readonly _homeDir = 'models' + private static readonly _modelMetadataFileName = 'model.json' + + /** + * Implements type from JanExtension. + * @override + * @returns The type of the extension. + */ + type(): ExtensionType { + return ExtensionType.Model + } + + /** + * Called when the extension is loaded. + * @override + */ + onLoad(): void { + this.copyModelsToHomeDir() + } + + /** + * Called when the extension is unloaded. + * @override + */ + onUnload(): void {} + + private async copyModelsToHomeDir() { + try { + // list all of the files under the home directory + const files = await fs.listFiles('') + + if (files.includes(JanModelExtension._homeDir)) { + // ignore if the model is already downloaded + console.debug('Model already downloaded') + return + } + + // copy models folder from resources to home directory + const resourePath = await getResourcePath() + const srcPath = join(resourePath, 'models') + + const userSpace = await getUserSpace() + const destPath = join(userSpace, JanModelExtension._homeDir) + + await fs.copyFile(srcPath, destPath) + } catch (err) { + console.error(err) + } + } + + /** + * Downloads a machine learning model. + * @param model - The model to download. + * @returns A Promise that resolves when the model is downloaded. + */ + async downloadModel(model: Model): Promise { + // create corresponding directory + const directoryPath = join(JanModelExtension._homeDir, model.id) + await fs.mkdir(directoryPath) + + // path to model binary + const path = join(directoryPath, model.id) + downloadFile(model.source_url, path) + } + + /** + * Cancels the download of a specific machine learning model. + * @param {string} modelId - The ID of the model whose download is to be cancelled. + * @returns {Promise} A promise that resolves when the download has been cancelled. + */ + async cancelModelDownload(modelId: string): Promise { + return abortDownload( + join(JanModelExtension._homeDir, modelId, modelId) + ).then(() => { + fs.deleteFile(join(JanModelExtension._homeDir, modelId, modelId)) + }) + } + + /** + * Deletes a machine learning model. + * @param filePath - The path to the model file to delete. + * @returns A Promise that resolves when the model is deleted. + */ + async deleteModel(modelId: string): Promise { + try { + const dirPath = join(JanModelExtension._homeDir, modelId) + + // remove all files under dirPath except model.json + const files = await fs.listFiles(dirPath) + const deletePromises = files.map((fileName: string) => { + if (fileName !== JanModelExtension._modelMetadataFileName) { + return fs.deleteFile(join(dirPath, fileName)) + } + }) + await Promise.allSettled(deletePromises) + + // update the state as default + const jsonFilePath = join( + dirPath, + JanModelExtension._modelMetadataFileName + ) + const json = await fs.readFile(jsonFilePath) + const model = JSON.parse(json) as Model + delete model.state + + await fs.writeFile(jsonFilePath, JSON.stringify(model, null, 2)) + } catch (err) { + console.error(err) + } + } + + /** + * Saves a machine learning model. + * @param model - The model to save. + * @returns A Promise that resolves when the model is saved. + */ + async saveModel(model: Model): Promise { + const jsonFilePath = join( + JanModelExtension._homeDir, + model.id, + JanModelExtension._modelMetadataFileName + ) + + try { + await fs.writeFile( + jsonFilePath, + JSON.stringify( + { + ...model, + state: ModelState.Ready, + }, + null, + 2 + ) + ) + } catch (err) { + console.error(err) + } + } + + /** + * Gets all downloaded models. + * @returns A Promise that resolves with an array of all models. + */ + async getDownloadedModels(): Promise { + const models = await this.getModelsMetadata() + return models.filter((model) => model.state === ModelState.Ready) + } + + private async getModelsMetadata(): Promise { + try { + const filesUnderJanRoot = await fs.listFiles('') + if (!filesUnderJanRoot.includes(JanModelExtension._homeDir)) { + console.debug('model folder not found') + return [] + } + + const files: string[] = await fs.listFiles(JanModelExtension._homeDir) + + const allDirectories: string[] = [] + for (const file of files) { + const isDirectory = await fs.isDirectory( + join(JanModelExtension._homeDir, file) + ) + if (isDirectory) { + allDirectories.push(file) + } + } + + const readJsonPromises = allDirectories.map((dirName) => { + const jsonPath = join( + JanModelExtension._homeDir, + dirName, + JanModelExtension._modelMetadataFileName + ) + return this.readModelMetadata(jsonPath) + }) + const results = await Promise.allSettled(readJsonPromises) + const modelData = results.map((result) => { + if (result.status === 'fulfilled') { + return JSON.parse(result.value) as Model + } else { + console.error(result.reason) + } + }) + return modelData + } catch (err) { + console.error(err) + return [] + } + } + + private readModelMetadata(path: string) { + return fs.readFile(join(path)) + } + + /** + * Gets all available models. + * @returns A Promise that resolves with an array of all models. + */ + async getConfiguredModels(): Promise { + return this.getModelsMetadata() + } +} diff --git a/plugins/model-plugin/tsconfig.json b/extensions/model-extension/tsconfig.json similarity index 100% rename from plugins/model-plugin/tsconfig.json rename to extensions/model-extension/tsconfig.json diff --git a/plugins/model-plugin/webpack.config.js b/extensions/model-extension/webpack.config.js similarity index 89% rename from plugins/model-plugin/webpack.config.js rename to extensions/model-extension/webpack.config.js index 3475516ed..a9332da99 100644 --- a/plugins/model-plugin/webpack.config.js +++ b/extensions/model-extension/webpack.config.js @@ -19,9 +19,6 @@ module.exports = { new webpack.DefinePlugin({ PLUGIN_NAME: JSON.stringify(packageJson.name), MODULE_PATH: JSON.stringify(`${packageJson.name}/${packageJson.module}`), - MODEL_CATALOG_URL: JSON.stringify( - 'https://cdn.jsdelivr.net/npm/@janhq/models@latest/dist/index.js' - ), }), ], output: { diff --git a/plugins/monitoring-plugin/README.md b/extensions/monitoring-extension/README.md similarity index 100% rename from plugins/monitoring-plugin/README.md rename to extensions/monitoring-extension/README.md diff --git a/plugins/monitoring-plugin/package.json b/extensions/monitoring-extension/package.json similarity index 68% rename from plugins/monitoring-plugin/package.json rename to extensions/monitoring-extension/package.json index 4054b8b4d..9070fc922 100644 --- a/plugins/monitoring-plugin/package.json +++ b/extensions/monitoring-extension/package.json @@ -1,20 +1,14 @@ { - "name": "@janhq/monitoring-plugin", + "name": "@janhq/monitoring-extension", "version": "1.0.9", "description": "Utilizing systeminformation, it provides essential System and OS information retrieval", - "icon": "https://raw.githubusercontent.com/tailwindlabs/heroicons/88e98b0c2b458553fbadccddc2d2f878edc0387b/src/20/solid/cpu-chip.svg", "main": "dist/index.js", "module": "dist/module.js", "author": "Jan ", "license": "AGPL-3.0", - "supportCloudNative": true, - "url": "/plugins/monitoring-plugin/index.js", - "activationPoints": [ - "init" - ], "scripts": { "build": "tsc -b . && webpack --config webpack.config.js", - "build:publish": "rimraf *.tgz --glob && npm run build && npm pack && cpx *.tgz ../../electron/core/pre-install" + "build:publish": "rimraf *.tgz --glob && npm run build && npm pack && cpx *.tgz ../../electron/pre-install" }, "devDependencies": { "rimraf": "^3.0.2", diff --git a/plugins/monitoring-plugin/src/@types/global.d.ts b/extensions/monitoring-extension/src/@types/global.d.ts similarity index 100% rename from plugins/monitoring-plugin/src/@types/global.d.ts rename to extensions/monitoring-extension/src/@types/global.d.ts diff --git a/plugins/monitoring-plugin/src/index.ts b/extensions/monitoring-extension/src/index.ts similarity index 52% rename from plugins/monitoring-plugin/src/index.ts rename to extensions/monitoring-extension/src/index.ts index 4b392596c..2e5e50ffa 100644 --- a/plugins/monitoring-plugin/src/index.ts +++ b/extensions/monitoring-extension/src/index.ts @@ -1,27 +1,27 @@ -import { PluginType } from "@janhq/core"; -import { MonitoringPlugin } from "@janhq/core/lib/plugins"; +import { ExtensionType } from "@janhq/core"; +import { MonitoringExtension } from "@janhq/core"; import { executeOnMain } from "@janhq/core"; /** - * JanMonitoringPlugin is a plugin that provides system monitoring functionality. - * It implements the MonitoringPlugin interface from the @janhq/core package. + * JanMonitoringExtension is a extension that provides system monitoring functionality. + * It implements the MonitoringExtension interface from the @janhq/core package. */ -export default class JanMonitoringPlugin implements MonitoringPlugin { +export default class JanMonitoringExtension implements MonitoringExtension { /** - * Returns the type of the plugin. - * @returns The PluginType.SystemMonitoring value. + * Returns the type of the extension. + * @returns The ExtensionType.SystemMonitoring value. */ - type(): PluginType { - return PluginType.SystemMonitoring; + type(): ExtensionType { + return ExtensionType.SystemMonitoring; } /** - * Called when the plugin is loaded. + * Called when the extension is loaded. */ onLoad(): void {} /** - * Called when the plugin is unloaded. + * Called when the extension is unloaded. */ onUnload(): void {} diff --git a/plugins/monitoring-plugin/src/module.ts b/extensions/monitoring-extension/src/module.ts similarity index 100% rename from plugins/monitoring-plugin/src/module.ts rename to extensions/monitoring-extension/src/module.ts diff --git a/plugins/monitoring-plugin/tsconfig.json b/extensions/monitoring-extension/tsconfig.json similarity index 100% rename from plugins/monitoring-plugin/tsconfig.json rename to extensions/monitoring-extension/tsconfig.json diff --git a/plugins/monitoring-plugin/webpack.config.js b/extensions/monitoring-extension/webpack.config.js similarity index 100% rename from plugins/monitoring-plugin/webpack.config.js rename to extensions/monitoring-extension/webpack.config.js diff --git a/models/capybara-34b/model.json b/models/capybara-34b/model.json new file mode 100644 index 000000000..562bcbe93 --- /dev/null +++ b/models/capybara-34b/model.json @@ -0,0 +1,24 @@ +{ + "source_url": "https://huggingface.co/TheBloke/Nous-Capybara-34B-GGUF/resolve/main/nous-capybara-34b.Q5_K_M.gguf", + "id": "capybara-34b", + "object": "model", + "name": "Capybara 200k 34B", + "version": 1.0, + "description": "Nous Capybara 34B, a variant of the Yi-34B model, is the first Nous model with a 200K context length, trained for three epochs on the innovative Capybara dataset.", + "format": "gguf", + "settings": { + "ctx_len": 4096, + "system_prompt": "", + "user_prompt": "USER: ", + "ai_prompt": "ASSISTANT: " + }, + "parameters": { + "max_tokens": 4096 + }, + "metadata": { + "author": "NousResearch, The Bloke", + "tags": ["General", "Big Context Length"], + "size": 24320000000 + } + } + \ No newline at end of file diff --git a/models/deepseek-coder-1.3b/model.json b/models/deepseek-coder-1.3b/model.json new file mode 100644 index 000000000..2ff6d6e7b --- /dev/null +++ b/models/deepseek-coder-1.3b/model.json @@ -0,0 +1,23 @@ +{ + "source_url": "https://huggingface.co/TheBloke/deepseek-coder-1.3b-base-GGUF/resolve/main/deepseek-coder-1.3b-base.Q4_K_M.gguf", + "id": "deepseek-coder-1.3b", + "object": "model", + "name": "Deepseek Coder 1.3B", + "version": "1.0", + "description": "", + "format": "gguf", + "settings": { + "ctx_len": 4096, + "system_prompt": "", + "user_prompt": "", + "ai_prompt": "" + }, + "parameters": { + "max_tokens": 4096 + }, + "metadata": { + "author": "deepseek, The Bloke", + "tags": ["Code"], + "size": 870000000 + } + } diff --git a/models/dolphin-yi-34b/model.json b/models/dolphin-yi-34b/model.json new file mode 100644 index 000000000..3b1bf3619 --- /dev/null +++ b/models/dolphin-yi-34b/model.json @@ -0,0 +1,24 @@ +{ + "source_url": "https://huggingface.co/TheBloke/dolphin-2_2-yi-34b-GGUF/resolve/main/dolphin-2_2-yi-34b.Q5_K_M.gguf", + "id": "dolphin-yi-34b", + "object": "model", + "name": "Dolphin Yi 34B", + "version": "1.0", + "description": "Dolphin, based on the Yi-34B model and enhanced with features like conversation and empathy, is trained on a unique dataset for advanced multi-turn conversations. Notably uncensored, it requires careful implementation of an alignment layer for ethical use.", + "format": "gguf", + "settings": { + "ctx_len": 4096, + "system_prompt": "<|im_start|>system\n", + "user_prompt": "<|im_end|>\n<|im_start|>user\n", + "ai_prompt": "<|im_end|>\n<|im_start|>assistant\n" + }, + "parameters": { + "max_tokens": 4096 + }, + "metadata": { + "author": "ehartford, The Bloke", + "tags": ["General Use", "Role-playing"], + "size": 24320000000 + } + } + \ No newline at end of file diff --git a/models/islm-3b/model.json b/models/islm-3b/model.json new file mode 100644 index 000000000..916d7c50e --- /dev/null +++ b/models/islm-3b/model.json @@ -0,0 +1,24 @@ +{ + "source_url": "https://huggingface.co/UmbrellaCorp/IS-LM-3B_GGUF/resolve/main/IS-LM-Q4_K_M.gguf", + "id": "islm-3b", + "object": "model", + "name": "IS LM 3B", + "version": "1.0", + "description": "IS LM 3B, based on the StableLM 3B model is specifically finetuned for economic analysis using DataForge Economics and QLoRA over three epochs, enhancing its proficiency in economic forecasting and analysis.", + "format": "gguf", + "settings": { + "ctx_len": 4096, + "system_prompt": "", + "user_prompt": "USER: ", + "ai_prompt": "ASSISTANT: " + }, + "parameters": { + "max_tokens": 4096 + }, + "metadata": { + "author": "UmbrellaCorp, The Bloke", + "tags": ["General Use", "Economics"], + "size": 1710000000 + } + } + \ No newline at end of file diff --git a/models/lzlv-70b/model.json b/models/lzlv-70b/model.json new file mode 100644 index 000000000..4aaee79b3 --- /dev/null +++ b/models/lzlv-70b/model.json @@ -0,0 +1,24 @@ +{ + "source_url": "https://huggingface.co/TheBloke/lzlv_70B-GGUF/resolve/main/lzlv_70b_fp16_hf.Q5_K_M.gguf", + "id": "lzlv-70b", + "object": "model", + "name": "Lzlv 70B", + "version": "1.0", + "description": "lzlv_70B is a sophisticated AI model designed for roleplaying and creative tasks. This merge aims to combine intelligence with creativity, seemingly outperforming its individual components in complex scenarios and creative outputs.", + "format": "gguf", + "settings": { + "ctx_len": 4096, + "system_prompt": "", + "user_prompt": "USER: ", + "ai_prompt": "ASSISTANT: " + }, + "parameters": { + "max_tokens": 4096 + }, + "metadata": { + "author": "lizpreciatior, The Bloke", + "tags": ["General Use", "Role-playing"], + "size": 48750000000 + } + } + \ No newline at end of file diff --git a/models/marx-3b/model.json b/models/marx-3b/model.json new file mode 100644 index 000000000..78617d5c3 --- /dev/null +++ b/models/marx-3b/model.json @@ -0,0 +1,23 @@ +{ + "source_url": "https://huggingface.co/TheBloke/Marx-3B-v3-GGUF/resolve/main/marx-3b-v3.Q4_K_M.gguf", + "id": "marx-3b", + "object": "model", + "name": "Marx 3B", + "version": "1.0", + "description": "Marx 3B, based on the StableLM 3B model is specifically finetuned for chating using EverythingLM data and QLoRA over two epochs, enhancing its proficiency in understand general knowledege.", + "format": "gguf", + "settings": { + "ctx_len": 4096, + "system_prompt": "", + "user_prompt": "### HUMAN: ", + "ai_prompt": "### RESPONSE: " + }, + "parameters": { + "max_tokens": 4096 + }, + "metadata": { + "author": "Bohan Du, The Bloke", + "tags": ["General Use"], + "size": 1620000000 + } + } \ No newline at end of file diff --git a/models/mythomax-13b/model.json b/models/mythomax-13b/model.json new file mode 100644 index 000000000..455f73968 --- /dev/null +++ b/models/mythomax-13b/model.json @@ -0,0 +1,24 @@ +{ + "source_url": "https://huggingface.co/TheBloke/MythoMax-L2-13B-GGUF/resolve/main/mythomax-l2-13b.Q5_K_M.gguf", + "id": "mythomax-13b", + "object": "model", + "name": "Mythomax L2 13B", + "version": "1.0", + "description": "Mythomax L2 13b, an advanced AI model derived from MythoMix, merges MythoLogic-L2's deep comprehension with Huginn's writing skills through a unique tensor merge technique, excelling in roleplaying and storytelling.", + "format": "gguf", + "settings": { + "ctx_len": 4096, + "system_prompt": "", + "user_prompt": "### Instruction: ", + "ai_prompt": "### Response: " + }, + "parameters": { + "max_tokens": 4096 + }, + "metadata": { + "author": "Gryphe, The Bloke", + "tags": ["Role-playing"], + "size": 9230000000 + } + } + \ No newline at end of file diff --git a/models/neural-chat-7b/model.json b/models/neural-chat-7b/model.json new file mode 100644 index 000000000..f4f4f14d4 --- /dev/null +++ b/models/neural-chat-7b/model.json @@ -0,0 +1,24 @@ +{ + "source_url": "https://huggingface.co/TheBloke/neural-chat-7B-v3-1-GGUF/resolve/main/neural-chat-7b-v3-1.Q4_K_M.gguf", + "id": "neural-chat-7b", + "object": "model", + "name": "Neural Chat 7B", + "version": "1.0", + "description": "The Neural Chat 7B model, developed on the foundation of mistralai/Mistral-7B-v0.1, has been fine-tuned using the Open-Orca/SlimOrca dataset and aligned with the Direct Preference Optimization (DPO) algorithm. It has demonstrated substantial improvements in various AI tasks and performance well on the open_llm_leaderboard.", + "format": "gguf", + "settings": { + "ctx_len": 4096, + "system_prompt": "### System: ", + "user_prompt": "### User: ", + "ai_prompt": "### Assistant: " + }, + "parameters": { + "max_tokens": 4096 + }, + "metadata": { + "author": "Intel, The Bloke", + "tags": ["General Use", "Role-playing", "Big Context Length"], + "size": 4370000000 + } + } + \ No newline at end of file diff --git a/models/neuralhermes-7b/model.json b/models/neuralhermes-7b/model.json new file mode 100644 index 000000000..07cca58d4 --- /dev/null +++ b/models/neuralhermes-7b/model.json @@ -0,0 +1,24 @@ +{ + "source_url": "https://huggingface.co/TheBloke/NeuralHermes-2.5-Mistral-7B-GGUF/resolve/main/neuralhermes-2.5-mistral-7b.Q4_K_M.gguf", + "id": "neuralhermes-7b", + "object": "model", + "name": "NeuralHermes 7B", + "version": "1.0", + "description": "NeuralHermes 2.5 has been enhanced using Direct Preference Optimization. This fine-tuning, inspired by the RLHF process of Neural-chat-7b and OpenHermes-2.5-Mistral-7B, has led to improved performance across several benchmarks.", + "format": "gguf", + "settings": { + "ctx_len": 4096, + "system_prompt": "<|im_start|>system\n", + "user_prompt": "<|im_end|>\n<|im_start|>user\n", + "ai_prompt": "<|im_end|>\n<|im_start|>assistant\n" + }, + "parameters": { + "max_tokens": 4096 + }, + "metadata": { + "author": "Intel, The Bloke", + "tags": ["General Use", "Code", "Big Context Length"], + "size": 4370000000 + } + } + \ No newline at end of file diff --git a/models/noromaid-20b/model.json b/models/noromaid-20b/model.json new file mode 100644 index 000000000..86291e4f5 --- /dev/null +++ b/models/noromaid-20b/model.json @@ -0,0 +1,24 @@ +{ + "source_url": "https://huggingface.co/TheBloke/Noromaid-20B-v0.1.1-GGUF/resolve/main/noromaid-20b-v0.1.1.Q4_K_M.gguf", + "id": "noromaid-20b", + "object": "model", + "name": "Noromaid 20B", + "version": "1.0", + "description": "The Noromaid 20b model is designed for role-playing and general use, featuring a unique touch with the no_robots dataset that enhances human-like behavior.", + "format": "gguf", + "settings": { + "ctx_len": 4096, + "system_prompt": "", + "user_prompt": "### Instruction: ", + "ai_prompt": "### Response: " + }, + "parameters": { + "max_tokens": 4096 + }, + "metadata": { + "author": "NeverSleep, The Bloke", + "tags": ["Role-playing"], + "size": 12040000000 + } + } + \ No newline at end of file diff --git a/models/openchat-7b/model.json b/models/openchat-7b/model.json new file mode 100644 index 000000000..1fd6bb259 --- /dev/null +++ b/models/openchat-7b/model.json @@ -0,0 +1,24 @@ +{ + "source_url": "https://huggingface.co/TheBloke/openchat_3.5-GGUF/resolve/main/openchat_3.5.Q4_K_M.gguf", + "id": "openchat-7b", + "object": "model", + "name": "Open Chat 3.5 7B", + "version": "1.0", + "description": "OpenChat represents a breakthrough in the realm of open-source language models. By implementing the C-RLFT fine-tuning strategy, inspired by offline reinforcement learning, this 7B model achieves results on par with ChatGPT (March).", + "format": "gguf", + "settings": { + "ctx_len": 4096, + "system_prompt": "", + "user_prompt": "GPT4 User: ", + "ai_prompt": "<|end_of_turn|>\nGPT4 Assistant: " + }, + "parameters": { + "max_tokens": 4096 + }, + "metadata": { + "author": "OpenChat, The Bloke", + "tags": ["General", "Code"], + "size": 4370000000 + } + } + \ No newline at end of file diff --git a/models/openhermes-mistral-7b/model.json b/models/openhermes-mistral-7b/model.json new file mode 100644 index 000000000..6b64363d5 --- /dev/null +++ b/models/openhermes-mistral-7b/model.json @@ -0,0 +1,24 @@ +{ + "source_url": "https://huggingface.co/TheBloke/OpenHermes-2.5-Mistral-7B-GGUF/resolve/main/openhermes-2.5-mistral-7b.Q4_K_M.gguf", + "id": "openhermes-mistral-7b", + "object": "model", + "name": "Openhermes 2.5 Mistral 7B", + "version": "1.0", + "description": "The OpenHermes 2.5 Mistral 7B incorporates additional code datasets, more than a million GPT-4 generated data examples, and other high-quality open datasets. This enhancement led to significant improvement in benchmarks, highlighting its improved skill in handling code-centric tasks.", + "format": "gguf", + "settings": { + "ctx_len": 4096, + "system_prompt": "<|im_start|>system\n", + "user_prompt": "<|im_end|>\n<|im_start|>user\n", + "ai_prompt": "<|im_end|>\n<|im_start|>assistant\n" + }, + "parameters": { + "max_tokens": 4096 + }, + "metadata": { + "author": "Teknium, The Bloke", + "tags": ["General", "Roleplay"], + "size": 4370000000 + } + } + \ No newline at end of file diff --git a/models/openorca-13b/model.json b/models/openorca-13b/model.json new file mode 100644 index 000000000..02a555430 --- /dev/null +++ b/models/openorca-13b/model.json @@ -0,0 +1,24 @@ +{ + "source_url": "https://huggingface.co/TheBloke/Orca-2-13B-GGUF/resolve/main/orca-2-13b.Q5_K_M.gguf", + "id": "openorca-13b", + "object": "model", + "name": "Orca 2 13B", + "version": "1.0", + "description": "Orca 2 is a finetuned version of LLAMA-2, designed primarily for single-turn responses in reasoning, reading comprehension, math problem solving, and text summarization.", + "format": "gguf", + "settings": { + "ctx_len": 4096, + "system_prompt": "<|im_start|>system\n", + "user_prompt": "<|im_end|>\n<|im_start|>user\n", + "ai_prompt": "<|im_end|>\n<|im_start|>assistant\n" + }, + "parameters": { + "max_tokens": 4096 + }, + "metadata": { + "author": "Microsoft, The Bloke", + "tags": ["General Use"], + "size": 9230000000 + } + } + \ No newline at end of file diff --git a/models/openorca-7b/model.json b/models/openorca-7b/model.json new file mode 100644 index 000000000..42c88212c --- /dev/null +++ b/models/openorca-7b/model.json @@ -0,0 +1,24 @@ +{ + "source_url": "https://huggingface.co/TheBloke/Mistral-7B-OpenOrca-GGUF/resolve/main/mistral-7b-openorca.Q4_K_M.gguf", + "id": "openorca-7b", + "object": "model", + "name": "OpenOrca 7B", + "version": "1.0", + "description": "OpenOrca 8k 7B is a model based on Mistral 7B, fine-tuned using the OpenOrca dataset. Notably ranked first on the HF Leaderboard for models under 30B, it excels in efficiency and accessibility.", + "format": "gguf", + "settings": { + "ctx_len": 4096, + "system_prompt": "<|im_start|>system\n", + "user_prompt": "<|im_end|>\n<|im_start|>user\n", + "ai_prompt": "<|im_end|>\n<|im_start|>assistant\n" + }, + "parameters": { + "max_tokens": 4096 + }, + "metadata": { + "author": "OpenOrca, The Bloke", + "tags": ["General", "Code"], + "size": 4370000000 + } + } + \ No newline at end of file diff --git a/models/phind-34b/model.json b/models/phind-34b/model.json new file mode 100644 index 000000000..4391ae08d --- /dev/null +++ b/models/phind-34b/model.json @@ -0,0 +1,24 @@ +{ + "source_url": "https://huggingface.co/TheBloke/Phind-CodeLlama-34B-v2-GGUF/resolve/main/phind-codellama-34b-v2.Q5_K_M.gguf", + "id": "phind-34b", + "object": "model", + "name": "Phind 34B", + "version": "1.0", + "description": "Phind-CodeLlama-34B-v2 is an AI model fine-tuned on 1.5B tokens of high-quality programming data. It's a SOTA open-source model in coding. This multi-lingual model excels in various programming languages, including Python, C/C++, TypeScript, Java, and is designed to be steerable and user-friendly.", + "format": "gguf", + "settings": { + "ctx_len": 4096, + "system_prompt": "### System Prompt\n", + "user_prompt": "### User Message\n", + "ai_prompt": "### Assistant\n" + }, + "parameters": { + "max_tokens": 4096 + }, + "metadata": { + "author": "Phind, The Bloke", + "tags": ["Code", "Big Context Length"], + "size": 24320000000 + } + } + \ No newline at end of file diff --git a/models/rocket-3b/model.json b/models/rocket-3b/model.json new file mode 100644 index 000000000..b00eb1f44 --- /dev/null +++ b/models/rocket-3b/model.json @@ -0,0 +1,23 @@ +{ + "source_url": "https://huggingface.co/TheBloke/rocket-3B-GGUF/resolve/main/rocket-3b.Q4_K_M.gguf", + "id": "rocket-3b", + "object": "model", + "name": "Rocket 3B", + "version": "1.0", + "description": "Rocket-3B is a GPT-like model, primarily English, fine-tuned on diverse public datasets. It outperforms larger models in benchmarks, showcasing superior understanding and text generation, making it an effective chat model for its size.", + "format": "gguf", + "settings": { + "ctx_len": 4096, + "system_prompt": "<|im_start|>system\n", + "user_prompt": "<|im_end|>\n<|im_start|>user\n", + "ai_prompt": "<|im_end|>\n<|im_start|>assistant\n" + }, + "parameters": { + "max_tokens": 4096 + }, + "metadata": { + "author": "pansophic, The Bloke", + "tags": ["General Use"], + "size": 1710000000 + } + } \ No newline at end of file diff --git a/models/starling-7b/model.json b/models/starling-7b/model.json new file mode 100644 index 000000000..c029ea7d9 --- /dev/null +++ b/models/starling-7b/model.json @@ -0,0 +1,24 @@ +{ + "source_url": "https://huggingface.co/TheBloke/Starling-LM-7B-alpha-GGUF/resolve/main/starling-lm-7b-alpha.Q4_K_M.gguf", + "id": "starling-7b", + "object": "model", + "name": "Strarling alpha 7B", + "version": "1.0", + "description": "Starling-RM-7B-alpha is a language model finetuned with Reinforcement Learning from AI Feedback from Openchat 3.5. It stands out for its impressive performance using GPT-4 as a judge, making it one of the top-performing models in its category.", + "format": "gguf", + "settings": { + "ctx_len": 4096, + "system_prompt": "", + "user_prompt": "GPT4 User: ", + "ai_prompt": "<|end_of_turn|>\nGPT4 Assistant: " + }, + "parameters": { + "max_tokens": 4096 + }, + "metadata": { + "author": "Berkeley-nest, The Bloke", + "tags": ["General", "Code"], + "size": 4370000000 + } + } + \ No newline at end of file diff --git a/models/storytelling-70b/model.json b/models/storytelling-70b/model.json new file mode 100644 index 000000000..76e6f7922 --- /dev/null +++ b/models/storytelling-70b/model.json @@ -0,0 +1,24 @@ +{ + "source_url": "https://huggingface.co/TheBloke/GOAT-70B-Storytelling-GGUF/resolve/main/goat-70b-storytelling.Q5_K_M.gguf", + "id": "storytelling-70b", + "object": "model", + "name": "Storytelling 70B", + "version": "1.0", + "description": "The GOAT-70B-Storytelling model is designed for autonomous story-writing, including crafting books and movie scripts. Based on the LLaMA 2 70B architecture, this model excels in generating cohesive and engaging narratives using inputs like plot outlines and character profiles.", + "format": "gguf", + "settings": { + "ctx_len": 4096, + "system_prompt": "", + "user_prompt": "### USER: ", + "ai_prompt": "\n### ASSISTANT: " + }, + "parameters": { + "max_tokens": 4096 + }, + "metadata": { + "author": "GOAT-AI, The Bloke", + "tags": ["General Use", "Writing"], + "size": 48750000000 + } + } + \ No newline at end of file diff --git a/models/tiefighter-13b/model.json b/models/tiefighter-13b/model.json new file mode 100644 index 000000000..b1d354ce3 --- /dev/null +++ b/models/tiefighter-13b/model.json @@ -0,0 +1,24 @@ +{ + "source_url": "https://huggingface.co/TheBloke/LLaMA2-13B-Tiefighter-GGUF/resolve/main/llama2-13b-tiefighter.Q5_K_M.gguf", + "id": "tiefighter-13b", + "object": "model", + "name": "Tiefighter 13B", + "version": "1.0", + "description": "Tiefighter-13B is a highly creative, merged AI model achieved by combining various 'LORAs' on top of an existing merge, particularly focusing on storytelling and improvisation. This model excels in story writing, chatbots, and adventuring, and is designed to perform better with less detailed inputs, leveraging its inherent creativity.", + "format": "gguf", + "settings": { + "ctx_len": 4096, + "system_prompt": "", + "user_prompt": "### Instruction: ", + "ai_prompt": "\n### Response: " + }, + "parameters": { + "max_tokens": 4096 + }, + "metadata": { + "author": "KoboldAI, The Bloke", + "tags": ["General Use", "Role-playing", "Writing"], + "size": 9230000000 + } + } + \ No newline at end of file diff --git a/models/tinyllama-1.1b/model.json b/models/tinyllama-1.1b/model.json new file mode 100644 index 000000000..f561eb25d --- /dev/null +++ b/models/tinyllama-1.1b/model.json @@ -0,0 +1,23 @@ +{ + "source_url": "https://huggingface.co/TinyLlama/TinyLlama-1.1B-Chat-v0.6/resolve/main/ggml-model-q4_0.gguf", + "id": "tinyllama-1.1b", + "object": "model", + "name": "TinyLlama Chat 1.1B", + "version": "1.0", + "description": "The TinyLlama project, featuring a 1.1B parameter Llama model, is pretrained on an expansive 3 trillion token dataset. Its design ensures easy integration with various Llama-based open-source projects. Despite its smaller size, it efficiently utilizes lower computational and memory resources, drawing on GPT-4's analytical prowess to enhance its conversational abilities and versatility.", + "format": "gguf", + "settings": { + "ctx_len": 2048, + "system_prompt": "<|system|>\n", + "user_prompt": "<|user|>\n", + "ai_prompt": "<|assistant|>\n" + }, + "parameters": { + "max_tokens": 2048 + }, + "metadata": { + "author": "TinyLlama", + "tags": ["General Use"], + "size": 637000000 + } +} \ No newline at end of file diff --git a/models/wizardcoder-13b/model.json b/models/wizardcoder-13b/model.json new file mode 100644 index 000000000..944b5632b --- /dev/null +++ b/models/wizardcoder-13b/model.json @@ -0,0 +1,24 @@ +{ + "source_url": "https://huggingface.co/TheBloke/WizardCoder-Python-13B-V1.0-GGUF/resolve/main/wizardcoder-python-13b-v1.0.Q5_K_M.gguf", + "id": "wizardcoder-13b", + "object": "model", + "name": "Wizard Coder Python 13B", + "version": "1.0", + "description": "WizardCoder-Python-13B is a Python coding model major models like ChatGPT-3.5. This model based on the Llama2 architecture, demonstrate high proficiency in specific domains like coding and mathematics.", + "format": "gguf", + "settings": { + "ctx_len": 4096, + "system_prompt": "", + "user_prompt": "### Instruction:\n", + "ai_prompt": "### Response:\n" + }, + "parameters": { + "max_tokens": 4096 + }, + "metadata": { + "author": "WizardLM, The Bloke", + "tags": ["Code", "Big Context Length"], + "size": 9230000000 + } + } + \ No newline at end of file diff --git a/models/wizardcoder-34b/model.json b/models/wizardcoder-34b/model.json new file mode 100644 index 000000000..aa2618e1b --- /dev/null +++ b/models/wizardcoder-34b/model.json @@ -0,0 +1,24 @@ +{ + "source_url": "https://huggingface.co/TheBloke/WizardCoder-Python-34B-V1.0-GGUF/resolve/main/wizardcoder-python-34b-v1.0.Q5_K_M.gguf", + "id": "wizardcoder-34b", + "object": "model", + "name": "Wizard Coder Python 34B", + "version": "1.0", + "description": "WizardCoder-Python-34B is a Python coding model major models like ChatGPT-3.5. This model based on the Llama2 architecture, demonstrate high proficiency in specific domains like coding and mathematics.", + "format": "gguf", + "settings": { + "ctx_len": 4096, + "system_prompt": "", + "user_prompt": "### Instruction:\n", + "ai_prompt": "### Response:\n" + }, + "parameters": { + "max_tokens": 4096 + }, + "metadata": { + "author": "WizardLM, The Bloke", + "tags": ["Code", "Big Context Length"], + "size": 24320000000 + } + } + \ No newline at end of file diff --git a/models/xwin-70b/model.json b/models/xwin-70b/model.json new file mode 100644 index 000000000..a5c1647b0 --- /dev/null +++ b/models/xwin-70b/model.json @@ -0,0 +1,24 @@ +{ + "source_url": "https://huggingface.co/TheBloke/Xwin-LM-70B-V0.1-GGUF/resolve/main/xwin-lm-70b-v0.1.Q5_K_M.gguf", + "id": "xwin-70b", + "object": "model", + "name": "Xwin LM 70B", + "version": "1.0", + "description": "Xwin-LM, based on Llama2 models, emphasizes alignment and exhibits advanced language understanding, text generation, and role-playing abilities.", + "format": "gguf", + "settings": { + "ctx_len": 4096, + "system_prompt": "", + "user_prompt": "USER: ", + "ai_prompt": "ASSISTANT: " + }, + "parameters": { + "max_tokens": 4096 + }, + "metadata": { + "author": "Xwin-LM, The Bloke", + "tags": ["General Use", "Role-playing"], + "size": 48750000000 + } + } + \ No newline at end of file diff --git a/models/yarn-70b/model.json b/models/yarn-70b/model.json new file mode 100644 index 000000000..67d8d3804 --- /dev/null +++ b/models/yarn-70b/model.json @@ -0,0 +1,21 @@ +{ + "source_url": "https://huggingface.co/TheBloke/Yarn-Llama-2-70B-32k-GGUF/resolve/main/yarn-llama-2-70b-32k.Q5_K_M.gguf", + "id": "yarn-70b", + "object": "model", + "name": "Yarn 32k 70B", + "version": "1,0", + "description": "Yarn-Llama-2-70b-32k is designed specifically for handling long contexts. It represents an extension of the Llama-2-70b-hf model, now supporting a 32k token context window.", + "format": "gguf", + "settings": { + "ctx_len": 4096 + }, + "parameters": { + "max_tokens": 4096 + }, + "metadata": { + "author": "NousResearch, The Bloke", + "tags": ["General Use", "Big Context Length"], + "size": 48750000000 + } + } + \ No newline at end of file diff --git a/models/yi-34b/model.json b/models/yi-34b/model.json new file mode 100644 index 000000000..f899bc54b --- /dev/null +++ b/models/yi-34b/model.json @@ -0,0 +1,24 @@ +{ + "source_url": "https://huggingface.co/TheBloke/Yi-34B-Chat-GGUF/resolve/main/yi-34b-chat.Q5_K_M.gguf", + "id": "yi-34b", + "object": "model", + "name": "Yi 34B", + "version": "1.0", + "description": "Yi-34B, a specialized chat model, is known for its diverse and creative responses and excels across various NLP tasks and benchmarks.", + "format": "gguf", + "settings": { + "ctx_len": 4096, + "system_prompt": "<|im_start|>system\n", + "user_prompt": "<|im_end|>\n<|im_start|>user\n", + "ai_prompt": "<|im_end|>\n<|im_start|>assistant\n" + }, + "parameters": { + "max_tokens": 4096 + }, + "metadata": { + "author": "01-ai, The Bloke", + "tags": ["General", "Role-playing", "Writing"], + "size": 24320000000 + } + } + \ No newline at end of file diff --git a/models/zephyr-beta-7b/model.json b/models/zephyr-beta-7b/model.json new file mode 100644 index 000000000..24529bc9a --- /dev/null +++ b/models/zephyr-beta-7b/model.json @@ -0,0 +1,24 @@ +{ + "source_url": "https://huggingface.co/TheBloke/zephyr-7B-beta-GGUF/resolve/main/zephyr-7b-beta.Q4_K_M.gguf", + "id": "zephyr-beta-7b", + "object": "model", + "name": "Zephyr Beta 7B", + "version": "1.0", + "description": "The Zephyr-7B-β model marks the second iteration in the Zephyr series, designed to function as an effective assistant. It has been fine-tuned from the mistralai/Mistral-7B-v0.1 base model, utilizing a combination of public and synthetic datasets with the application of Direct Preference Optimization.", + "format": "gguf", + "settings": { + "ctx_len": 4096, + "system_prompt": "<|system|>\n", + "user_prompt": "\n<|user|>\n", + "ai_prompt": "\n<|assistant|>\n" + }, + "parameters": { + "max_tokens": 4096 + }, + "metadata": { + "author": "HuggingFaceH4, The Bloke", + "tags": ["General Use", "Big Context Length"], + "size": 4370000000 + } + } + \ No newline at end of file diff --git a/package.json b/package.json index cb670a096..9192a0238 100644 --- a/package.json +++ b/package.json @@ -6,8 +6,7 @@ "uikit", "core", "electron", - "web", - "server" + "web" ], "nohoist": [ "uikit", @@ -17,15 +16,13 @@ "electron", "electron/**", "web", - "web/**", - "server", - "server/**" + "web/**" ] }, "scripts": { "lint": "yarn workspace jan lint && yarn workspace jan-web lint", "test": "yarn workspace jan test:e2e", - "dev:electron": "yarn workspace jan dev", + "dev:electron": "cpx \"models/**\" \"electron/models/\" && yarn workspace jan dev", "dev:web": "yarn workspace jan-web dev", "dev": "concurrently --kill-others \"yarn dev:web\" \"wait-on http://localhost:3000 && yarn dev:electron\"", "test-local": "yarn lint && yarn build:test && yarn test", @@ -33,9 +30,9 @@ "build:uikit": "yarn workspace @janhq/uikit install && yarn workspace @janhq/uikit build", "build:core": "cd core && yarn install && yarn run build", "build:web": "yarn workspace jan-web build && cpx \"web/out/**\" \"electron/renderer/\"", - "build:electron": "yarn workspace jan build", + "build:electron": "yarn workspace jan build && cpx \"models/**\" \"electron/models/\"", "build:electron:test": "yarn workspace jan build:test", - "build:plugins": "rimraf ./electron/core/pre-install/*.tgz && concurrently --kill-others-on-fail \"cd ./plugins/conversational-json && npm install && npm run build:publish\" \"cd ./plugins/inference-plugin && npm install && npm run build:publish\" \"cd ./plugins/model-plugin && npm install && npm run build:publish\" \"cd ./plugins/monitoring-plugin && npm install && npm run build:publish\" \"cd ./plugins/assistant-plugin && npm install && npm run build:publish\"", + "build:extensions": "rimraf ./electron/pre-install/*.tgz && concurrently --kill-others-on-fail \"cd ./extensions/conversational-extension && npm install && npm run build:publish\" \"cd ./extensions/inference-extension && npm install && npm run build:publish\" \"cd ./extensions/model-extension && npm install && npm run build:publish\" \"cd ./extensions/monitoring-extension && npm install && npm run build:publish\" \"cd ./extensions/assistant-extension && npm install && npm run build:publish\"", "build:test": "yarn build:web && yarn workspace jan build:test", "build": "yarn build:web && yarn workspace jan build", "build:publish": "yarn build:web && yarn workspace jan build:publish" diff --git a/plugins/model-plugin/src/@types/global.d.ts b/plugins/model-plugin/src/@types/global.d.ts deleted file mode 100644 index 87056c342..000000000 --- a/plugins/model-plugin/src/@types/global.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -declare const PLUGIN_NAME: string; -declare const MODULE_PATH: string; -declare const MODEL_CATALOG_URL: string; diff --git a/plugins/model-plugin/src/@types/schema.ts b/plugins/model-plugin/src/@types/schema.ts deleted file mode 100644 index 1d3c3a7d1..000000000 --- a/plugins/model-plugin/src/@types/schema.ts +++ /dev/null @@ -1,21 +0,0 @@ -interface Version { - name: string - quantMethod: string - bits: number - size: number - maxRamRequired: number - usecase: string - downloadLink: string -} -interface ModelSchema { - id: string - name: string - shortDescription: string - avatarUrl: string - longDescription: string - author: string - version: string - modelUrl: string - tags: string[] - versions: Version[] -} diff --git a/plugins/model-plugin/src/helpers/modelParser.ts b/plugins/model-plugin/src/helpers/modelParser.ts deleted file mode 100644 index 3a397fb7d..000000000 --- a/plugins/model-plugin/src/helpers/modelParser.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { ModelCatalog } from '@janhq/core' - -export const parseToModel = (modelGroup): ModelCatalog => { - const modelVersions = [] - modelGroup.versions.forEach((v) => { - const model = { - object: 'model', - version: modelGroup.version, - source_url: v.downloadLink, - id: v.name, - name: v.name, - owned_by: 'you', - created: 0, - description: modelGroup.longDescription, - state: 'to_download', - settings: v.settings, - parameters: v.parameters, - metadata: { - engine: '', - quantization: v.quantMethod, - size: v.size, - binaries: [], - maxRamRequired: v.maxRamRequired, - author: modelGroup.author, - avatarUrl: modelGroup.avatarUrl, - }, - } - modelVersions.push(model) - }) - - const modelCatalog: ModelCatalog = { - id: modelGroup.id, - name: modelGroup.name, - avatarUrl: modelGroup.avatarUrl, - shortDescription: modelGroup.shortDescription, - longDescription: modelGroup.longDescription, - author: modelGroup.author, - version: modelGroup.version, - modelUrl: modelGroup.modelUrl, - releaseDate: modelGroup.createdAt, - tags: modelGroup.tags, - availableVersions: modelVersions, - } - - return modelCatalog -} diff --git a/plugins/model-plugin/src/index.ts b/plugins/model-plugin/src/index.ts deleted file mode 100644 index 7ca63e708..000000000 --- a/plugins/model-plugin/src/index.ts +++ /dev/null @@ -1,141 +0,0 @@ -import { PluginType, fs, downloadFile, abortDownload } from '@janhq/core' -import { ModelPlugin } from '@janhq/core/lib/plugins' -import { Model, ModelCatalog } from '@janhq/core/lib/types' -import { parseToModel } from './helpers/modelParser' -import { join } from 'path' - -/** - * A plugin for managing machine learning models. - */ -export default class JanModelPlugin implements ModelPlugin { - private static readonly _homeDir = 'models' - private static readonly _modelMetadataFileName = 'model.json' - - /** - * Implements type from JanPlugin. - * @override - * @returns The type of the plugin. - */ - type(): PluginType { - return PluginType.Model - } - - /** - * Called when the plugin is loaded. - * @override - */ - onLoad(): void { - /** Cloud Native - * TODO: Fetch all downloading progresses? - **/ - fs.mkdir(JanModelPlugin._homeDir) - } - - /** - * Called when the plugin is unloaded. - * @override - */ - onUnload(): void {} - - /** - * Downloads a machine learning model. - * @param model - The model to download. - * @returns A Promise that resolves when the model is downloaded. - */ - async downloadModel(model: Model): Promise { - // create corresponding directory - const directoryPath = join(JanModelPlugin._homeDir, model.id) - await fs.mkdir(directoryPath) - - // path to model binary - const path = join(directoryPath, model.id) - downloadFile(model.source_url, path) - } - - /** - * Cancels the download of a specific machine learning model. - * @param {string} modelId - The ID of the model whose download is to be cancelled. - * @returns {Promise} A promise that resolves when the download has been cancelled. - */ - async cancelModelDownload(modelId: string): Promise { - return abortDownload(join(JanModelPlugin._homeDir, modelId, modelId)).then( - () => { - fs.rmdir(join(JanModelPlugin._homeDir, modelId)) - } - ) - } - - /** - * Deletes a machine learning model. - * @param filePath - The path to the model file to delete. - * @returns A Promise that resolves when the model is deleted. - */ - async deleteModel(modelId: string): Promise { - try { - const dirPath = join(JanModelPlugin._homeDir, modelId) - await fs.rmdir(dirPath) - } catch (err) { - console.error(err) - } - } - - /** - * Saves a machine learning model. - * @param model - The model to save. - * @returns A Promise that resolves when the model is saved. - */ - async saveModel(model: Model): Promise { - const jsonFilePath = join( - JanModelPlugin._homeDir, - model.id, - JanModelPlugin._modelMetadataFileName - ) - - try { - await fs.writeFile(jsonFilePath, JSON.stringify(model, null, 2)) - } catch (err) { - console.error(err) - } - } - - /** - * Gets all downloaded models. - * @returns A Promise that resolves with an array of all models. - */ - async getDownloadedModels(): Promise { - const results: Model[] = [] - const allDirs: string[] = await fs.listFiles(JanModelPlugin._homeDir) - for (const dir of allDirs) { - const modelDirPath = join(JanModelPlugin._homeDir, dir) - const isModelDir = await fs.isDirectory(modelDirPath) - if (!isModelDir) { - // if not a directory, ignore - continue - } - - const jsonFiles: string[] = (await fs.listFiles(modelDirPath)).filter( - (fileName: string) => fileName === JanModelPlugin._modelMetadataFileName - ) - - for (const json of jsonFiles) { - const model: Model = JSON.parse( - await fs.readFile(join(modelDirPath, json)) - ) - results.push(model) - } - } - - return results - } - - /** - * Gets all available models. - * @returns A Promise that resolves with an array of all models. - */ - getConfiguredModels(): Promise { - // Add a timestamp to the URL to prevent caching - return import( - /* webpackIgnore: true */ MODEL_CATALOG_URL + `?t=${Date.now()}` - ).then((module) => module.default.map((e) => parseToModel(e))) - } -} diff --git a/web/containers/DropdownListSidebar/index.tsx b/web/containers/DropdownListSidebar/index.tsx index 91d194fbd..b159a131e 100644 --- a/web/containers/DropdownListSidebar/index.tsx +++ b/web/containers/DropdownListSidebar/index.tsx @@ -3,7 +3,7 @@ import { Fragment, useEffect, useState } from 'react' import { Listbox, Transition } from '@headlessui/react' import { CheckIcon, ChevronUpDownIcon } from '@heroicons/react/20/solid' -import { Model } from '@janhq/core/lib/types' +import { Model } from '@janhq/core' import { atom, useSetAtom } from 'jotai' import { twMerge } from 'tailwind-merge' diff --git a/web/containers/ItemCardSidebar/index.tsx b/web/containers/ItemCardSidebar/index.tsx index b6a7bacbd..627d7f45d 100644 --- a/web/containers/ItemCardSidebar/index.tsx +++ b/web/containers/ItemCardSidebar/index.tsx @@ -1,9 +1,16 @@ type Props = { title: string description?: string + disabled?: boolean + onChange?: (text?: string) => void } -export default function ItemCardSidebar({ description, title }: Props) { +export default function ItemCardSidebar({ + description, + title, + disabled, + onChange, +}: Props) { return (
@@ -11,9 +18,11 @@ export default function ItemCardSidebar({ description, title }: Props) {
onChange?.(e.target.value)} />
) diff --git a/web/containers/Layout/BottomBar/DownloadingState/index.tsx b/web/containers/Layout/BottomBar/DownloadingState/index.tsx index 7eb28d772..1aad0fb1c 100644 --- a/web/containers/Layout/BottomBar/DownloadingState/index.tsx +++ b/web/containers/Layout/BottomBar/DownloadingState/index.tsx @@ -1,7 +1,7 @@ import { Fragment } from 'react' -import { PluginType } from '@janhq/core' -import { ModelPlugin } from '@janhq/core/lib/plugins' +import { ExtensionType } from '@janhq/core' +import { ModelExtension } from '@janhq/core' import { Progress, Modal, @@ -18,8 +18,8 @@ import { useDownloadState } from '@/hooks/useDownloadState' import { formatDownloadPercentage } from '@/utils/converter' +import { extensionManager } from '@/extension' import { downloadingModelsAtom } from '@/helpers/atoms/Model.atom' -import { pluginManager } from '@/plugin' export default function DownloadingState() { const { downloadStates } = useDownloadState() @@ -69,20 +69,16 @@ export default function DownloadingState() { />
-

{item?.fileName}

+

{item?.modelId}

{formatDownloadPercentage(item?.percent)}
+ {activeThread && ( + + )} ) } diff --git a/web/containers/ModalCancelDownload/index.tsx b/web/containers/ModalCancelDownload/index.tsx index 07d62fa0c..8619c543c 100644 --- a/web/containers/ModalCancelDownload/index.tsx +++ b/web/containers/ModalCancelDownload/index.tsx @@ -1,8 +1,7 @@ import { useMemo } from 'react' -import { PluginType } from '@janhq/core' -import { ModelPlugin } from '@janhq/core/lib/plugins' -import { Model } from '@janhq/core/lib/types' +import { ModelExtension, ExtensionType } from '@janhq/core' +import { Model } from '@janhq/core' import { Modal, @@ -21,38 +20,34 @@ import { useDownloadState } from '@/hooks/useDownloadState' import { formatDownloadPercentage } from '@/utils/converter' +import { extensionManager } from '@/extension' import { downloadingModelsAtom } from '@/helpers/atoms/Model.atom' -import { pluginManager } from '@/plugin' type Props = { - suitableModel: Model + model: Model isFromList?: boolean } -export default function ModalCancelDownload({ - suitableModel, - isFromList, -}: Props) { +export default function ModalCancelDownload({ model, isFromList }: Props) { const { modelDownloadStateAtom } = useDownloadState() const downloadAtom = useMemo( - () => atom((get) => get(modelDownloadStateAtom)[suitableModel.name]), + () => atom((get) => get(modelDownloadStateAtom)[model.id]), // eslint-disable-next-line react-hooks/exhaustive-deps - [suitableModel.name] + [model.id] ) const models = useAtomValue(downloadingModelsAtom) const downloadState = useAtomValue(downloadAtom) + const cancelText = `Cancel ${formatDownloadPercentage(downloadState.percent)}` return ( {isFromList ? ( ) : ( - + )} @@ -61,7 +56,7 @@ export default function ModalCancelDownload({

Are you sure you want to cancel the download of  - {downloadState?.fileName}? + {downloadState?.modelId}?

@@ -72,13 +67,9 @@ export default function ModalCancelDownload({ - - {show && ( - - )} -
- )}
) diff --git a/web/screens/ExploreModels/ExploreModelItemHeader/index.tsx b/web/screens/ExploreModels/ExploreModelItemHeader/index.tsx index 1993f6451..b51ec164c 100644 --- a/web/screens/ExploreModels/ExploreModelItemHeader/index.tsx +++ b/web/screens/ExploreModels/ExploreModelItemHeader/index.tsx @@ -1,7 +1,7 @@ /* eslint-disable react-hooks/exhaustive-deps */ -import { useCallback, useEffect, useMemo, useState } from 'react' +import { useCallback, useMemo, useState } from 'react' -import { Model, ModelCatalog } from '@janhq/core/lib/types' +import { Model } from '@janhq/core' import { Badge, Button } from '@janhq/uikit' import { atom, useAtomValue } from 'jotai' @@ -15,67 +15,41 @@ import { ModelPerformance, TagType } from '@/constants/tagType' import useDownloadModel from '@/hooks/useDownloadModel' import { useDownloadState } from '@/hooks/useDownloadState' import { useGetDownloadedModels } from '@/hooks/useGetDownloadedModels' -import useGetPerformanceTag from '@/hooks/useGetPerformanceTag' import { useMainViewState } from '@/hooks/useMainViewState' import { toGigabytes } from '@/utils/converter' -import { totalRamAtom } from '@/helpers/atoms/SystemBar.atom' - type Props = { - suitableModel: Model - exploreModel: ModelCatalog + model: Model } -const ExploreModelItemHeader: React.FC = ({ - suitableModel, - exploreModel, -}) => { +const ExploreModelItemHeader: React.FC = ({ model }) => { const { downloadModel } = useDownloadModel() const { downloadedModels } = useGetDownloadedModels() const { modelDownloadStateAtom, downloadStates } = useDownloadState() - const { getPerformanceForModel } = useGetPerformanceTag() const [title, setTitle] = useState('Recommended') - const totalRam = useAtomValue(totalRamAtom) + const [performanceTag, setPerformanceTag] = useState( ModelPerformance.PerformancePositive ) const downloadAtom = useMemo( - () => atom((get) => get(modelDownloadStateAtom)[suitableModel.name]), - [suitableModel.name] + () => atom((get) => get(modelDownloadStateAtom)[model.id]), + [model.id] ) const downloadState = useAtomValue(downloadAtom) const { setMainViewState } = useMainViewState() - const calculatePerformance = useCallback( - (suitableModel: Model) => async () => { - const { title, performanceTag } = await getPerformanceForModel( - suitableModel, - totalRam - ) - setPerformanceTag(performanceTag) - setTitle(title) - }, - [totalRam] - ) - - useEffect(() => { - calculatePerformance(suitableModel) - }, [suitableModel]) - const onDownloadClick = useCallback(() => { - downloadModel(suitableModel) + downloadModel(model) // eslint-disable-next-line react-hooks/exhaustive-deps - }, [suitableModel]) + }, [model]) - // TODO: Comparing between Model Id and Version Name? - const isDownloaded = - downloadedModels.find((model) => model.id === suitableModel.name) != null + const isDownloaded = downloadedModels.find((md) => md.id === model.id) != null let downloadButton = ( ) @@ -93,7 +67,7 @@ const ExploreModelItemHeader: React.FC = ({ } if (downloadState != null && downloadStates.length > 0) { - downloadButton = + downloadButton = } const renderBadge = (performance: TagType) => { @@ -115,7 +89,7 @@ const ExploreModelItemHeader: React.FC = ({ return (
- {exploreModel.name} + {model.name} {performanceTag && renderBadge(performanceTag)}
{downloadButton} diff --git a/web/screens/ExploreModels/ExploreModelList/index.tsx b/web/screens/ExploreModels/ExploreModelList/index.tsx index 37089a72f..eea9f0238 100644 --- a/web/screens/ExploreModels/ExploreModelList/index.tsx +++ b/web/screens/ExploreModels/ExploreModelList/index.tsx @@ -1,16 +1,14 @@ -import { ModelCatalog } from '@janhq/core/lib/types' +import { Model } from '@janhq/core' import ExploreModelItem from '@/screens/ExploreModels/ExploreModelItem' type Props = { - models: ModelCatalog[] + models: Model[] } const ExploreModelList: React.FC = ({ models }) => (
- {models?.map((item, i) => ( - - ))} + {models?.map((model) => )}
) diff --git a/web/screens/ExploreModels/ModelVersionItem/index.tsx b/web/screens/ExploreModels/ModelVersionItem/index.tsx index 6f21a5466..e16c477f6 100644 --- a/web/screens/ExploreModels/ModelVersionItem/index.tsx +++ b/web/screens/ExploreModels/ModelVersionItem/index.tsx @@ -1,8 +1,8 @@ /* eslint-disable react-hooks/exhaustive-deps */ import React, { useMemo } from 'react' -import { Model } from '@janhq/core/lib/types' -import { Badge, Button } from '@janhq/uikit' +import { Model } from '@janhq/core' +import { Button } from '@janhq/uikit' import { atom, useAtomValue } from 'jotai' import ModalCancelDownload from '@/containers/ModalCancelDownload' @@ -63,7 +63,7 @@ const ModelVersionItem: React.FC = ({ model }) => { } if (downloadState != null && downloadStates.length > 0) { - downloadButton = + downloadButton = } return ( @@ -74,16 +74,7 @@ const ModelVersionItem: React.FC = ({ model }) => {
-
- {`${toGigabytes( - model.metadata.maxRamRequired - )} RAM required`} - {toGigabytes(model.metadata.size)} -
+
{downloadButton}
diff --git a/web/screens/ExploreModels/ModelVersionList/index.tsx b/web/screens/ExploreModels/ModelVersionList/index.tsx index 3228a6e9e..7992b7a51 100644 --- a/web/screens/ExploreModels/ModelVersionList/index.tsx +++ b/web/screens/ExploreModels/ModelVersionList/index.tsx @@ -1,4 +1,4 @@ -import { Model } from '@janhq/core/lib/types' +import { Model } from '@janhq/core' import ModelVersionItem from '../ModelVersionItem' diff --git a/web/screens/MyModels/BlankState/index.tsx b/web/screens/MyModels/BlankState/index.tsx index a820440d0..c0d7be6bb 100644 --- a/web/screens/MyModels/BlankState/index.tsx +++ b/web/screens/MyModels/BlankState/index.tsx @@ -55,7 +55,7 @@ export default function BlankStateMyModel() { } />
-

{item?.fileName}

+

{item?.modelId}

{formatDownloadPercentage(item?.percent)}
diff --git a/web/screens/MyModels/index.tsx b/web/screens/MyModels/index.tsx index d9c2d2880..c8176f010 100644 --- a/web/screens/MyModels/index.tsx +++ b/web/screens/MyModels/index.tsx @@ -63,10 +63,7 @@ const MyModelsScreen = () => {
- + {model.metadata.author.charAt(0)} diff --git a/web/screens/Settings/CorePlugins/PluginsCatalog/index.tsx b/web/screens/Settings/CoreExtensions/ExtensionsCatalog/index.tsx similarity index 52% rename from web/screens/Settings/CorePlugins/PluginsCatalog/index.tsx rename to web/screens/Settings/CoreExtensions/ExtensionsCatalog/index.tsx index 8b107c2d6..837fc1900 100644 --- a/web/screens/Settings/CorePlugins/PluginsCatalog/index.tsx +++ b/web/screens/Settings/CoreExtensions/ExtensionsCatalog/index.tsx @@ -11,19 +11,19 @@ import { FeatureToggleContext } from '@/context/FeatureToggle' import { useGetAppVersion } from '@/hooks/useGetAppVersion' -import { formatPluginsName } from '@/utils/converter' +import { formatExtensionsName } from '@/utils/converter' -import { pluginManager } from '@/plugin' +import { extensionManager } from '@/extension' -const PluginCatalog = () => { - const [activePlugins, setActivePlugins] = useState([]) - const [pluginCatalog, setPluginCatalog] = useState([]) +const ExtensionCatalog = () => { + const [activeExtensions, setActiveExtensions] = useState([]) + const [extensionCatalog, setExtensionCatalog] = useState([]) const [isLoading, setIsLoading] = useState(false) const fileInputRef = useRef(null) const { version } = useGetAppVersion() const { experimentalFeatureEnabed } = useContext(FeatureToggleContext) /** - * Loads the plugin catalog module from a CDN and sets it as the plugin catalog state. + * Loads the extension catalog module from a CDN and sets it as the extension catalog state. */ useEffect(() => { if (!window.electronAPI) { @@ -31,74 +31,61 @@ const PluginCatalog = () => { } if (!version) return - // Get plugin manifest + // Get extension manifest import(/* webpackIgnore: true */ PLUGIN_CATALOG + `?t=${Date.now()}`).then( (data) => { if (Array.isArray(data.default) && experimentalFeatureEnabed) - setPluginCatalog(data.default) + setExtensionCatalog(data.default) } ) }, [experimentalFeatureEnabed, version]) /** - * Fetches the active plugins and their preferences from the `plugins` and `preferences` modules. + * Fetches the active extensions and their preferences from the `extensions` and `preferences` modules. * If the `experimentComponent` extension point is available, it executes the extension point and * appends the returned components to the `experimentRef` element. - * If the `PluginPreferences` extension point is available, it executes the extension point and - * fetches the preferences for each plugin using the `preferences.get` function. + * If the `ExtensionPreferences` extension point is available, it executes the extension point and + * fetches the preferences for each extension using the `preferences.get` function. */ useEffect(() => { - const getActivePlugins = async () => { - const plgs = await pluginManager.getActive() - if (Array.isArray(plgs)) setActivePlugins(plgs) + const getActiveExtensions = async () => { + const exts = await extensionManager.getActive() + if (Array.isArray(exts)) setActiveExtensions(exts) } - getActivePlugins() + getActiveExtensions() }, []) /** - * Installs a plugin by calling the `plugins.install` function with the plugin file path. + * Installs a extension by calling the `extensions.install` function with the extension file path. * If the installation is successful, the application is relaunched using the `coreAPI.relaunch` function. * @param e - The event object. */ const install = async (e: any) => { e.preventDefault() - const pluginFile = e.target.files?.[0].path + const extensionFile = e.target.files?.[0].path - // Send the filename of the to be installed plugin + // Send the filename of the to be installed extension // to the main process for installation - const installed = await pluginManager.install([pluginFile]) - if (installed) window.coreAPI?.relaunch() + const installed = await extensionManager.install([extensionFile]) + if (installed) window.core.api?.relaunch() } /** - * Uninstalls a plugin by calling the `plugins.uninstall` function with the plugin name. + * Uninstalls a extension by calling the `extensions.uninstall` function with the extension name. * If the uninstallation is successful, the application is relaunched using the `coreAPI.relaunch` function. - * @param name - The name of the plugin to uninstall. + * @param name - The name of the extension to uninstall. */ // eslint-disable-next-line @typescript-eslint/no-unused-vars const uninstall = async (name: string) => { - // Send the filename of the to be uninstalled plugin + // Send the filename of the to be uninstalled extension // to the main process for removal - const res = await pluginManager.uninstall([name]) - if (res) window.coreAPI?.relaunch() + const res = await extensionManager.uninstall([name]) + if (res) window.core.api?.relaunch() } /** - * Downloads a remote plugin tarball and installs it using the `plugins.install` function. - * If the installation is successful, the application is relaunched using the `coreAPI.relaunch` function. - * @param pluginName - The name of the remote plugin to download and install. - */ - const downloadTarball = async (pluginName: string) => { - setIsLoading(true) - const pluginPath = await window.coreAPI?.installRemotePlugin(pluginName) - const installed = await pluginManager.install([pluginPath]) - setIsLoading(false) - if (installed) window.coreAPI.relaunch() - } - - /** - * Handles the change event of the plugin file input element by setting the file name state. - * Its to be used to display the plugin file name of the selected file. + * Handles the change event of the extension file input element by setting the file name state. + * Its to be used to display the extension file name of the selected file. * @param event - The change event object. */ const handleFileChange = (event: React.ChangeEvent) => { @@ -112,23 +99,25 @@ const PluginCatalog = () => { return (
- {pluginCatalog + {extensionCatalog .concat( - activePlugins.filter( - (e) => !(pluginCatalog ?? []).some((p) => p.name === e.name) + activeExtensions.filter( + (e) => !(extensionCatalog ?? []).some((p) => p.name === e.name) ) ?? [] ) .map((item, i) => { - const isActivePlugin = activePlugins.some((x) => x.name === item.name) - const installedPlugin = activePlugins.filter( + const isActiveExtension = activeExtensions.some( + (x) => x.name === item.name + ) + const installedExtension = activeExtensions.filter( (p) => p.name === item.name )[0] - const updateVersionPlugins = Number( - installedPlugin?.version.replaceAll('.', '') + const updateVersionExtensions = Number( + installedExtension?.version.replaceAll('.', '') ) - const hasUpdateVersionPlugins = - item.version.replaceAll('.', '') > updateVersionPlugins + const hasUpdateVersionExtensions = + item.version.replaceAll('.', '') > updateVersionExtensions return (
{
- {formatPluginsName(item.name)} + {formatExtensionsName(item.name)}

v{item.version} @@ -147,38 +136,17 @@ const PluginCatalog = () => {

{item.description}

- {isActivePlugin && ( + {isActiveExtension && (

Installed{' '} - {hasUpdateVersionPlugins - ? `v${installedPlugin.version}` + {hasUpdateVersionExtensions + ? `v${installedExtension.version}` : 'the latest version'}

- {hasUpdateVersionPlugins && ( - - )}
)}
- {experimentalFeatureEnabed && ( - { - if (e === true) { - downloadTarball(item.name) - } else { - uninstall(item.name) - } - }} - /> - )}
) })} @@ -191,7 +159,7 @@ const PluginCatalog = () => {

- Select a plugin file to install (.tgz) + Select a extension file to install (.tgz)

@@ -214,4 +182,4 @@ const PluginCatalog = () => { ) } -export default PluginCatalog +export default ExtensionCatalog diff --git a/web/screens/Settings/CorePlugins/PreferencePlugins/index.tsx b/web/screens/Settings/CoreExtensions/PreferenceExtensions/index.tsx similarity index 81% rename from web/screens/Settings/CorePlugins/PreferencePlugins/index.tsx rename to web/screens/Settings/CoreExtensions/PreferenceExtensions/index.tsx index 8486c0ac0..e6df90633 100644 --- a/web/screens/Settings/CorePlugins/PreferencePlugins/index.tsx +++ b/web/screens/Settings/CoreExtensions/PreferenceExtensions/index.tsx @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ type Props = { - pluginName: string + extensionName: string preferenceValues: any preferenceItems: any } @@ -25,10 +25,10 @@ import * as z from 'zod' import { toaster } from '@/containers/Toast' -import { formatPluginsName } from '@/utils/converter' +import { formatExtensionsName } from '@/utils/converter' -const PreferencePlugins = (props: Props) => { - const { pluginName, preferenceValues, preferenceItems } = props +const PreferenceExtensions = (props: Props) => { + const { extensionName, preferenceValues, preferenceItems } = props const FormSchema = z.record( z @@ -47,11 +47,11 @@ const PreferencePlugins = (props: Props) => { const onSubmit = async (values: z.infer) => { for (const [key, value] of Object.entries(values)) { - // await preferences.set(pluginName, key, value) - // await execute(PluginService.OnPreferencesUpdate, {}) + // await preferences.set(extensionName, key, value) + // await execute(ExtensionService.OnPreferencesUpdate, {}) } toaster({ - title: formatPluginsName(pluginName), + title: formatExtensionsName(extensionName), description: 'Successfully updated preferences', }) } @@ -59,12 +59,12 @@ const PreferencePlugins = (props: Props) => { return (
- {formatPluginsName(pluginName)} + {formatExtensionsName(extensionName)}
{preferenceItems - .filter((x: any) => x.pluginName === pluginName) + .filter((x: any) => x.extensionName === extensionName) ?.map((e: any) => ( { ) } -export default PreferencePlugins +export default PreferenceExtensions diff --git a/web/screens/Settings/index.tsx b/web/screens/Settings/index.tsx index 900ddef9b..63c343add 100644 --- a/web/screens/Settings/index.tsx +++ b/web/screens/Settings/index.tsx @@ -9,10 +9,10 @@ import { twMerge } from 'tailwind-merge' import Advanced from '@/screens/Settings/Advanced' import AppearanceOptions from '@/screens/Settings/Appearance' -import PluginCatalog from '@/screens/Settings/CorePlugins/PluginsCatalog' -import PreferencePlugins from '@/screens/Settings/CorePlugins/PreferencePlugins' +import ExtensionCatalog from '@/screens/Settings/CoreExtensions/ExtensionsCatalog' +import PreferenceExtensions from '@/screens/Settings/CoreExtensions/PreferenceExtensions' -import { formatPluginsName } from '@/utils/converter' +import { formatExtensionsName } from '@/utils/converter' const SettingsScreen = () => { const [activeStaticMenu, setActiveStaticMenu] = useState('Appearance') @@ -24,48 +24,24 @@ const SettingsScreen = () => { const menu = ['Appearance'] if (typeof window !== 'undefined' && window.electronAPI) { - menu.push('Core Plugins') + menu.push('Core Extensions') } menu.push('Advanced') setMenus(menu) }, []) - /** - * Fetches the active plugins and their preferences from the `plugins` and `preferences` modules. - * If the `experimentComponent` extension point is available, it executes the extension point and - * appends the returned components to the `experimentRef` element. - * If the `PluginPreferences` extension point is available, it executes the extension point and - * fetches the preferences for each plugin using the `preferences.get` function. - */ - useEffect(() => { - const getActivePluginPreferences = async () => { - // setPreferenceItems(Array.isArray(data) ? data : []) - // TODO: Add back with new preferences mechanism - // Promise.all( - // (Array.isArray(data) ? data : []).map((e) => - // preferences - // .get(e.pluginName, e.preferenceKey) - // .then((k) => ({ key: e.preferenceKey, value: k })) - // ) - // ).then((data) => { - // setPreferenceValues(data) - // }) - } - getActivePluginPreferences() - }, []) - - const preferencePlugins = preferenceItems - .map((x) => x.pluginName) + const preferenceExtensions = preferenceItems + .map((x) => x.extensionnName) .filter((x, i) => { - // return prefere/nceItems.map((x) => x.pluginName).indexOf(x) === i + // return prefere/nceItems.map((x) => x.extensionName).indexOf(x) === i }) - const [activePreferencePlugin, setActivePreferencePlugin] = useState('') + const [activePreferenceExtension, setActivePreferenceExtension] = useState('') const handleShowOptions = (menu: string) => { switch (menu) { - case 'Core Plugins': - return + case 'Core Extensions': + return case 'Appearance': return @@ -75,8 +51,8 @@ const SettingsScreen = () => { default: return ( - @@ -101,7 +77,7 @@ const SettingsScreen = () => {
{ setActiveStaticMenu(menu) - setActivePreferencePlugin('') + setActivePreferenceExtension('') }} className="block w-full cursor-pointer" > @@ -122,19 +98,19 @@ const SettingsScreen = () => {
- {preferencePlugins.length > 0 && ( + {preferenceExtensions.length > 0 && ( )}
- {preferencePlugins.map((menu, i) => { - const isActive = activePreferencePlugin === menu + {preferenceExtensions.map((menu, i) => { + const isActive = activePreferenceExtension === menu return (
{ - setActivePreferencePlugin(menu) + setActivePreferenceExtension(menu) setActiveStaticMenu('') }} className="block w-full cursor-pointer" @@ -145,7 +121,7 @@ const SettingsScreen = () => { isActive && 'relative z-10' )} > - {formatPluginsName(String(menu))} + {formatExtensionsName(String(menu))}
{isActive ? ( @@ -166,7 +142,7 @@ const SettingsScreen = () => {
- {handleShowOptions(activeStaticMenu || activePreferencePlugin)} + {handleShowOptions(activeStaticMenu || activePreferenceExtension)}
diff --git a/web/services/cloudNativeService.ts b/web/services/cloudNativeService.ts index 55164751b..a300ac02d 100644 --- a/web/services/cloudNativeService.ts +++ b/web/services/cloudNativeService.ts @@ -10,12 +10,12 @@ export async function appVersion() { return Promise.resolve(VERSION) } -export function invokePluginFunc( +export function invokeExtensionFunc( modulePath: string, - pluginFunc: string, + extensionFunc: string, ...args: any ): Promise { - return fetchApi(modulePath, pluginFunc, args).catch((err: Error) => { + return fetchApi(modulePath, extensionFunc, args).catch((err: Error) => { throw err }) } @@ -37,14 +37,14 @@ export async function deleteFile(fileName: string) { export async function fetchApi( modulePath: string, - pluginFunc: string, + extensionFunc: string, args: any ): Promise { const response = await fetch(API_BASE_PATH + '/invokeFunction', { method: 'POST', body: JSON.stringify({ modulePath: modulePath, - method: pluginFunc, + method: extensionFunc, args: args, }), headers: { contentType: 'application/json', Authorization: '' }, diff --git a/web/services/coreService.ts b/web/services/coreService.ts index 66f4b72d6..89b6dcfd1 100644 --- a/web/services/coreService.ts +++ b/web/services/coreService.ts @@ -7,13 +7,12 @@ export const setupCoreServices = () => { } else { console.debug('Setting up core services') } - if (!window.corePlugin) { - window.corePlugin = { + if (!window.core) { + window.core = { events: new EventEmitter(), - } - window.coreAPI = {} - window.coreAPI = window.electronAPI ?? { - ...restAPI, + api: window.electronAPI ?? { + ...restAPI, + }, } } } diff --git a/web/services/extensionService.ts b/web/services/extensionService.ts new file mode 100644 index 000000000..d79bda065 --- /dev/null +++ b/web/services/extensionService.ts @@ -0,0 +1,36 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +'use client' +import { ExtensionType } from '@janhq/core' + +import { extensionManager } from '@/extension/ExtensionManager' + +export const isCoreExtensionInstalled = () => { + if (!extensionManager.get(ExtensionType.Conversational)) { + return false + } + if (!extensionManager.get(ExtensionType.Inference)) return false + if (!extensionManager.get(ExtensionType.Model)) { + return false + } + return true +} +export const setupBaseExtensions = async () => { + if ( + typeof window === 'undefined' || + typeof window.electronAPI === 'undefined' + ) { + return + } + const baseExtensions = await window.electronAPI.baseExtensions() + + if ( + !extensionManager.get(ExtensionType.Conversational) || + !extensionManager.get(ExtensionType.Inference) || + !extensionManager.get(ExtensionType.Model) + ) { + const installed = await extensionManager.install(baseExtensions) + if (installed) { + window.location.reload() + } + } +} diff --git a/web/services/pluginService.ts b/web/services/pluginService.ts deleted file mode 100644 index 53e5c0777..000000000 --- a/web/services/pluginService.ts +++ /dev/null @@ -1,36 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -'use client' -import { PluginType } from '@janhq/core' - -import { pluginManager } from '@/plugin/PluginManager' - -export const isCorePluginInstalled = () => { - if (!pluginManager.get(PluginType.Conversational)) { - return false - } - if (!pluginManager.get(PluginType.Inference)) return false - if (!pluginManager.get(PluginType.Model)) { - return false - } - return true -} -export const setupBasePlugins = async () => { - if ( - typeof window === 'undefined' || - typeof window.electronAPI === 'undefined' - ) { - return - } - const basePlugins = await window.electronAPI.basePlugins() - - if ( - !pluginManager.get(PluginType.Conversational) || - !pluginManager.get(PluginType.Inference) || - !pluginManager.get(PluginType.Model) - ) { - const installed = await pluginManager.install(basePlugins) - if (installed) { - window.location.reload() - } - } -} diff --git a/web/types/downloadState.d.ts b/web/types/downloadState.d.ts index cb154522d..3c3389b4f 100644 --- a/web/types/downloadState.d.ts +++ b/web/types/downloadState.d.ts @@ -4,7 +4,6 @@ type DownloadState = { speed: number percent: number size: DownloadSize - fileName: string error?: string } diff --git a/web/types/index.d.ts b/web/types/index.d.ts index 98fceb094..50b5bbda2 100644 --- a/web/types/index.d.ts +++ b/web/types/index.d.ts @@ -5,9 +5,7 @@ declare global { declare const PLUGIN_CATALOG: string declare const VERSION: string interface Window { + core?: any | undefined electronAPI?: any | undefined - corePlugin?: any | undefined - coreAPI?: any | undefined - pluggableElectronIpc?: any | undefined } } diff --git a/web/utils/converter.ts b/web/utils/converter.ts index 5c69bdf09..630366ed0 100644 --- a/web/utils/converter.ts +++ b/web/utils/converter.ts @@ -29,6 +29,6 @@ export const formatTwoDigits = (input: number) => { return input.toFixed(2) } -export const formatPluginsName = (input: string) => { +export const formatExtensionsName = (input: string) => { return input.replace('@janhq/', '').replaceAll('-', ' ') } diff --git a/web/utils/dummy.ts b/web/utils/dummy.ts deleted file mode 100644 index bde61e38f..000000000 --- a/web/utils/dummy.ts +++ /dev/null @@ -1,119 +0,0 @@ -/* eslint-disable @typescript-eslint/naming-convention */ -import { ModelCatalog, ModelState } from '@janhq/core' - -export const dummyModel: ModelCatalog = { - id: 'aladar/TinyLLama-v0-GGUF', - name: 'TinyLLama-v0-GGUF', - shortDescription: 'TinyLlama-1.1B-Chat-v0.3-GGUF', - longDescription: 'https://huggingface.co/aladar/TinyLLama-v0-GGUF/tree/main', - avatarUrl: '', - releaseDate: Date.now(), - author: 'aladar', - version: '1.0.0', - modelUrl: 'aladar/TinyLLama-v0-GGUF', - tags: ['freeform', 'tags'], - availableVersions: [ - { - object: 'model', - version: '1.0.0', - source_url: - 'https://huggingface.co/aladar/TinyLLama-v0-GGUF/resolve/main/TinyLLama-v0.Q8_0.gguf', - id: 'TinyLLama-v0.Q8_0.gguf', - name: 'TinyLLama-v0.Q8_0.gguf', - owned_by: 'you', - created: 0, - description: '', - state: ModelState.ToDownload, - settings: { - ctx_len: 2048, - ngl: 100, - embedding: true, - n_parallel: 4, - }, - parameters: { - temperature: 0.7, - token_limit: 2048, - top_k: 0, - top_p: 1, - stream: true, - }, - metadata: { - engine: '', - quantization: '', - size: 5816320, - binaries: [], - maxRamRequired: 256000000, - author: 'aladar', - avatarUrl: '', - }, - }, - { - object: 'model', - version: '1.0.0', - source_url: - 'https://huggingface.co/aladar/TinyLLama-v0-GGUF/resolve/main/TinyLLama-v0.f16.gguf', - id: 'TinyLLama-v0.f16.gguf', - name: 'TinyLLama-v0.f16.gguf', - owned_by: 'you', - created: 0, - description: '', - state: ModelState.ToDownload, - settings: { - ctx_len: 2048, - ngl: 100, - embedding: true, - n_parallel: 4, - }, - parameters: { - temperature: 0.7, - token_limit: 2048, - top_k: 0, - top_p: 1, - stream: true, - }, - metadata: { - engine: '', - quantization: '', - size: 5816320, - binaries: [], - maxRamRequired: 256000000, - author: 'aladar', - avatarUrl: '', - }, - }, - { - object: 'model', - version: '1.0.0', - source_url: - 'https://huggingface.co/aladar/TinyLLama-v0-GGUF/resolve/main/TinyLLama-v0.f32.gguf', - id: 'TinyLLama-v0.f32.gguf', - name: 'TinyLLama-v0.f32.gguf', - owned_by: 'you', - created: 0, - description: '', - state: ModelState.ToDownload, - settings: { - ctx_len: 2048, - ngl: 100, - embedding: true, - n_parallel: 4, - }, - parameters: { - temperature: 0.7, - token_limit: 2048, - top_k: 0, - top_p: 1, - stream: true, - }, - metadata: { - engine: '', - quantization: '', - size: 5816320, - binaries: [], - maxRamRequired: 256000000, - author: 'aladar', - avatarUrl: '', - }, - }, - ], -}