Merge branch 'main' into docs/add-guides
This commit is contained in:
commit
91c612721d
1
Makefile
1
Makefile
@ -18,6 +18,7 @@ ifeq ($(OS),Windows_NT)
|
|||||||
yarn config set network-timeout 300000
|
yarn config set network-timeout 300000
|
||||||
endif
|
endif
|
||||||
yarn build:core
|
yarn build:core
|
||||||
|
yarn build:server
|
||||||
yarn install
|
yarn install
|
||||||
yarn build:extensions
|
yarn build:extensions
|
||||||
|
|
||||||
|
|||||||
50
README.md
50
README.md
@ -70,7 +70,7 @@ Jan is an open-source ChatGPT alternative that runs 100% offline on your compute
|
|||||||
<tr style="text-align: center">
|
<tr style="text-align: center">
|
||||||
<td style="text-align:center"><b>Experimental (Nighlty Build)</b></td>
|
<td style="text-align:center"><b>Experimental (Nighlty Build)</b></td>
|
||||||
<td style="text-align:center" colspan="4">
|
<td style="text-align:center" colspan="4">
|
||||||
<a href='https://github.com/janhq/jan/actions/runs/7341513351'>
|
<a href='https://github.com/janhq/jan/actions/runs/7359469616'>
|
||||||
<b>Github action artifactory</b>
|
<b>Github action artifactory</b>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
@ -81,16 +81,14 @@ Download the latest version of Jan at https://jan.ai/ or visit the **[GitHub Rel
|
|||||||
|
|
||||||
## Demo
|
## Demo
|
||||||
|
|
||||||
<p align="center">
|

|
||||||
<video src="https://github.com/janhq/jan/assets/89722390/43adfddc-7980-4ae6-b544-719f04660dd7">
|
|
||||||
</video>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
|
_Realtime Video: Jan v0.4.3-nightly on a Mac M1, 16GB Sonoma 14_
|
||||||
_Video: Jan v0.4.0 on Mac Air M2, 16GB Ventura_
|
|
||||||
|
|
||||||
## Quicklinks
|
## Quicklinks
|
||||||
|
|
||||||
#### Jan
|
#### Jan
|
||||||
|
|
||||||
- [Jan website](https://jan.ai/)
|
- [Jan website](https://jan.ai/)
|
||||||
- [Jan Github](https://github.com/janhq/jan)
|
- [Jan Github](https://github.com/janhq/jan)
|
||||||
- [User Guides](https://jan.ai/docs)
|
- [User Guides](https://jan.ai/docs)
|
||||||
@ -98,8 +96,10 @@ _Video: Jan v0.4.0 on Mac Air M2, 16GB Ventura_
|
|||||||
- [API reference](https://jan.ai/api-reference/)
|
- [API reference](https://jan.ai/api-reference/)
|
||||||
- [Specs](https://jan.ai/specs/)
|
- [Specs](https://jan.ai/specs/)
|
||||||
|
|
||||||
#### Nitro:
|
#### Nitro
|
||||||
Nitro is a high-efficiency C++ inference engine for edge computing, powering Jan. It is lightweight and embeddable, ideal for product integration.
|
|
||||||
|
Nitro is a high-efficiency C++ inference engine for edge computing. It is lightweight and embeddable, and can be used on its own within your own projects.
|
||||||
|
|
||||||
- [Nitro Website](https://nitro.jan.ai)
|
- [Nitro Website](https://nitro.jan.ai)
|
||||||
- [Nitro Github](https://github.com/janhq/nitro)
|
- [Nitro Github](https://github.com/janhq/nitro)
|
||||||
- [Documentation](https://nitro.jan.ai/docs)
|
- [Documentation](https://nitro.jan.ai/docs)
|
||||||
@ -118,21 +118,22 @@ To reset your installation:
|
|||||||
```
|
```
|
||||||
|
|
||||||
This will remove all build artifacts and cached files:
|
This will remove all build artifacts and cached files:
|
||||||
|
|
||||||
- Delete Jan from your `/Applications` folder
|
- Delete Jan from your `/Applications` folder
|
||||||
- Clear Application cache in `/Users/$(whoami)/Library/Caches/jan`
|
- Clear Application cache in `/Users/$(whoami)/Library/Caches/jan`
|
||||||
|
|
||||||
2. Use the following commands to remove any dangling backend processes:
|
2. Use the following commands to remove any dangling backend processes:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
ps aux | grep nitro
|
ps aux | grep nitro
|
||||||
```
|
```
|
||||||
|
|
||||||
Look for processes like "nitro" and "nitro_arm_64," and kill them one by one with:
|
Look for processes like "nitro" and "nitro_arm_64," and kill them one by one with:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
kill -9 <PID>
|
||||||
|
```
|
||||||
|
|
||||||
```sh
|
|
||||||
kill -9 <PID>
|
|
||||||
```
|
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
Contributions are welcome! Please read the [CONTRIBUTING.md](CONTRIBUTING.md) file
|
Contributions are welcome! Please read the [CONTRIBUTING.md](CONTRIBUTING.md) file
|
||||||
@ -176,19 +177,22 @@ This will build the app MacOS m1/m2 for production (with code signing already do
|
|||||||
|
|
||||||
## Nightly Build
|
## Nightly Build
|
||||||
|
|
||||||
Nightly build is a process where the software is built automatically every night. This helps in detecting and fixing bugs early in the development cycle. The process for this project is defined in [`.github/workflows/jan-electron-build-nightly.yml`](.github/workflows/jan-electron-build-nightly.yml)
|
Our nightly build process for this project is defined in [`.github/workflows/jan-electron-build-nightly.yml`](.github/workflows/jan-electron-build-nightly.yml)
|
||||||
|
|
||||||
You can join our Discord server [here](https://discord.gg/FTk2MvZwJH) and go to channel [github-jan](https://discordapp.com/channels/1107178041848909847/1148534730359308298) to monitor the build process.
|
|
||||||
|
|
||||||
The nightly build is triggered at 2:00 AM UTC every day.
|
The nightly build is triggered at 2:00 AM UTC every day.
|
||||||
|
|
||||||
The nightly build can be downloaded from the url notified in the Discord channel. Please access the url from the browser and download the build artifacts from there.
|
Getting on Nightly:
|
||||||
|
|
||||||
|
1. Join our Discord server [here](https://discord.gg/FTk2MvZwJH) and go to channel [github-jan](https://discordapp.com/channels/1107178041848909847/1148534730359308298).
|
||||||
|
2. Download the build artifacts from the channel.
|
||||||
|
3. Subsequently, to get the latest nightly, just quit and restart the app.
|
||||||
|
4. Upon app restart, you will be automatically prompted to update to the latest nightly build.
|
||||||
|
|
||||||
## Manual Build
|
## Manual Build
|
||||||
|
|
||||||
Manual build is a process where the software is built manually by the developers. This is usually done when a new feature is implemented or a bug is fixed. The process for this project is defined in [`.github/workflows/jan-electron-build-nightly.yml`](.github/workflows/jan-electron-build-nightly.yml)
|
Stable releases are triggered by manual builds. This is usually done for new features or a bug fixes.
|
||||||
|
|
||||||
It is similar to the nightly build process, except that it is triggered manually by the developers.
|
The process for this project is defined in [`.github/workflows/jan-electron-build-nightly.yml`](.github/workflows/jan-electron-build-nightly.yml)
|
||||||
|
|
||||||
## Acknowledgements
|
## Acknowledgements
|
||||||
|
|
||||||
|
|||||||
@ -22,6 +22,27 @@
|
|||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.0.0"
|
"node": ">=6.0.0"
|
||||||
},
|
},
|
||||||
|
"exports": {
|
||||||
|
".": "./dist/core.umd.js",
|
||||||
|
"./sdk": "./dist/core.umd.js",
|
||||||
|
"./node": "./dist/node/index.cjs.js"
|
||||||
|
},
|
||||||
|
"typesVersions": {
|
||||||
|
"*": {
|
||||||
|
".": [
|
||||||
|
"./dist/core.es5.js.map",
|
||||||
|
"./dist/types/index.d.ts"
|
||||||
|
],
|
||||||
|
"sdk": [
|
||||||
|
"./dist/core.es5.js.map",
|
||||||
|
"./dist/types/index.d.ts"
|
||||||
|
],
|
||||||
|
"node": [
|
||||||
|
"./dist/node/index.cjs.js.map",
|
||||||
|
"./dist/types/node/index.d.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"lint": "tslint --project tsconfig.json -t codeFrame 'src/**/*.ts' 'test/**/*.ts'",
|
"lint": "tslint --project tsconfig.json -t codeFrame 'src/**/*.ts' 'test/**/*.ts'",
|
||||||
"prebuild": "rimraf dist",
|
"prebuild": "rimraf dist",
|
||||||
|
|||||||
@ -8,30 +8,69 @@ const pkg = require('./package.json')
|
|||||||
|
|
||||||
const libraryName = 'core'
|
const libraryName = 'core'
|
||||||
|
|
||||||
export default {
|
export default [
|
||||||
input: `src/index.ts`,
|
{
|
||||||
output: [
|
input: `src/index.ts`,
|
||||||
{ file: pkg.main, name: libraryName, format: 'umd', sourcemap: true },
|
output: [
|
||||||
{ file: pkg.module, format: 'es', sourcemap: true },
|
{ file: pkg.main, name: libraryName, format: 'umd', sourcemap: true },
|
||||||
],
|
{ file: pkg.module, format: 'es', sourcemap: true },
|
||||||
// Indicate here external modules you don't wanna include in your bundle (i.e.: 'lodash')
|
],
|
||||||
external: [],
|
// Indicate here external modules you don't wanna include in your bundle (i.e.: 'lodash')
|
||||||
watch: {
|
external: ['path'],
|
||||||
include: 'src/**',
|
watch: {
|
||||||
},
|
include: 'src/**',
|
||||||
plugins: [
|
},
|
||||||
// Allow json resolution
|
plugins: [
|
||||||
json(),
|
// Allow json resolution
|
||||||
// Compile TypeScript files
|
json(),
|
||||||
typescript({ useTsconfigDeclarationDir: true }),
|
// Compile TypeScript files
|
||||||
// Allow bundling cjs modules (unlike webpack, rollup doesn't understand cjs)
|
typescript({ useTsconfigDeclarationDir: true }),
|
||||||
commonjs(),
|
// Allow bundling cjs modules (unlike webpack, rollup doesn't understand cjs)
|
||||||
// Allow node_modules resolution, so you can use 'external' to control
|
commonjs(),
|
||||||
// which external modules to include in the bundle
|
// Allow node_modules resolution, so you can use 'external' to control
|
||||||
// https://github.com/rollup/rollup-plugin-node-resolve#usage
|
// which external modules to include in the bundle
|
||||||
resolve(),
|
// https://github.com/rollup/rollup-plugin-node-resolve#usage
|
||||||
|
resolve(),
|
||||||
|
|
||||||
// Resolve source maps to the original source
|
// Resolve source maps to the original source
|
||||||
sourceMaps(),
|
sourceMaps(),
|
||||||
],
|
],
|
||||||
}
|
},
|
||||||
|
{
|
||||||
|
input: `src/node/index.ts`,
|
||||||
|
output: [{ file: 'dist/node/index.cjs.js', format: 'cjs', sourcemap: true }],
|
||||||
|
// Indicate here external modules you don't wanna include in your bundle (i.e.: 'lodash')
|
||||||
|
external: [
|
||||||
|
'fs/promises',
|
||||||
|
'path',
|
||||||
|
'pacote',
|
||||||
|
'@types/pacote',
|
||||||
|
'@npmcli/arborist',
|
||||||
|
'ulid',
|
||||||
|
'node-fetch',
|
||||||
|
'fs',
|
||||||
|
'request',
|
||||||
|
'crypto',
|
||||||
|
'url',
|
||||||
|
'http',
|
||||||
|
],
|
||||||
|
watch: {
|
||||||
|
include: 'src/node/**',
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
// Allow json resolution
|
||||||
|
json(),
|
||||||
|
// Compile TypeScript files
|
||||||
|
typescript({ useTsconfigDeclarationDir: true }),
|
||||||
|
// Allow bundling cjs modules (unlike webpack, rollup doesn't understand cjs)
|
||||||
|
commonjs(),
|
||||||
|
// Allow node_modules resolution, so you can use 'external' to control
|
||||||
|
// which external modules to include in the bundle
|
||||||
|
// https://github.com/rollup/rollup-plugin-node-resolve#usage
|
||||||
|
resolve(),
|
||||||
|
|
||||||
|
// Resolve source maps to the original source
|
||||||
|
sourceMaps(),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|||||||
@ -5,12 +5,12 @@
|
|||||||
export enum AppRoute {
|
export enum AppRoute {
|
||||||
appDataPath = 'appDataPath',
|
appDataPath = 'appDataPath',
|
||||||
appVersion = 'appVersion',
|
appVersion = 'appVersion',
|
||||||
getResourcePath = 'getResourcePath',
|
|
||||||
openExternalUrl = 'openExternalUrl',
|
openExternalUrl = 'openExternalUrl',
|
||||||
openAppDirectory = 'openAppDirectory',
|
openAppDirectory = 'openAppDirectory',
|
||||||
openFileExplore = 'openFileExplorer',
|
openFileExplore = 'openFileExplorer',
|
||||||
relaunch = 'relaunch',
|
relaunch = 'relaunch',
|
||||||
joinPath = 'joinPath'
|
joinPath = 'joinPath',
|
||||||
|
baseName = 'baseName',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum AppEvent {
|
export enum AppEvent {
|
||||||
@ -41,20 +41,20 @@ export enum ExtensionRoute {
|
|||||||
uninstallExtension = 'uninstallExtension',
|
uninstallExtension = 'uninstallExtension',
|
||||||
}
|
}
|
||||||
export enum FileSystemRoute {
|
export enum FileSystemRoute {
|
||||||
appendFile = 'appendFile',
|
appendFileSync = 'appendFileSync',
|
||||||
copyFile = 'copyFile',
|
copyFileSync = 'copyFileSync',
|
||||||
syncFile = 'syncFile',
|
unlinkSync = 'unlinkSync',
|
||||||
deleteFile = 'deleteFile',
|
existsSync = 'existsSync',
|
||||||
exists = 'exists',
|
readdirSync = 'readdirSync',
|
||||||
getResourcePath = 'getResourcePath',
|
mkdirSync = 'mkdirSync',
|
||||||
|
readFileSync = 'readFileSync',
|
||||||
|
rmdirSync = 'rmdirSync',
|
||||||
|
writeFileSync = 'writeFileSync',
|
||||||
|
}
|
||||||
|
export enum FileManagerRoute {
|
||||||
|
synceFile = 'syncFile',
|
||||||
getUserSpace = 'getUserSpace',
|
getUserSpace = 'getUserSpace',
|
||||||
isDirectory = 'isDirectory',
|
getResourcePath = 'getResourcePath',
|
||||||
listFiles = 'listFiles',
|
|
||||||
mkdir = 'mkdir',
|
|
||||||
readFile = 'readFile',
|
|
||||||
readLineByLine = 'readLineByLine',
|
|
||||||
rmdir = 'rmdir',
|
|
||||||
writeFile = 'writeFile',
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ApiFunction = (...args: any[]) => any
|
export type ApiFunction = (...args: any[]) => any
|
||||||
@ -83,17 +83,23 @@ export type FileSystemRouteFunctions = {
|
|||||||
[K in FileSystemRoute]: ApiFunction
|
[K in FileSystemRoute]: ApiFunction
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type FileManagerRouteFunctions = {
|
||||||
|
[K in FileManagerRoute]: ApiFunction
|
||||||
|
}
|
||||||
|
|
||||||
export type APIFunctions = AppRouteFunctions &
|
export type APIFunctions = AppRouteFunctions &
|
||||||
AppEventFunctions &
|
AppEventFunctions &
|
||||||
DownloadRouteFunctions &
|
DownloadRouteFunctions &
|
||||||
DownloadEventFunctions &
|
DownloadEventFunctions &
|
||||||
ExtensionRouteFunctions &
|
ExtensionRouteFunctions &
|
||||||
FileSystemRouteFunctions
|
FileSystemRouteFunctions &
|
||||||
|
FileManagerRoute
|
||||||
|
|
||||||
export const APIRoutes = [
|
export const APIRoutes = [
|
||||||
...Object.values(AppRoute),
|
...Object.values(AppRoute),
|
||||||
...Object.values(DownloadRoute),
|
...Object.values(DownloadRoute),
|
||||||
...Object.values(ExtensionRoute),
|
...Object.values(ExtensionRoute),
|
||||||
...Object.values(FileSystemRoute),
|
...Object.values(FileSystemRoute),
|
||||||
|
...Object.values(FileManagerRoute),
|
||||||
]
|
]
|
||||||
export const APIEvents = [...Object.values(AppEvent), ...Object.values(DownloadEvent)]
|
export const APIEvents = [...Object.values(AppEvent), ...Object.values(DownloadEvent)]
|
||||||
|
|||||||
@ -51,6 +51,27 @@ const openFileExplorer: (path: string) => Promise<any> = (path) =>
|
|||||||
*/
|
*/
|
||||||
const joinPath: (paths: string[]) => Promise<string> = (paths) => global.core.api?.joinPath(paths)
|
const joinPath: (paths: string[]) => Promise<string> = (paths) => global.core.api?.joinPath(paths)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrive the basename from an url.
|
||||||
|
* @param path - The path to retrieve.
|
||||||
|
* @returns {Promise<string>} A promise that resolves with the basename.
|
||||||
|
*/
|
||||||
|
const baseName: (paths: string[]) => Promise<string> = (path) => global.core.api?.baseName(path)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens an external URL in the default web browser.
|
||||||
|
*
|
||||||
|
* @param {string} url - The URL to open.
|
||||||
|
* @returns {Promise<any>} - A promise that resolves when the URL has been successfully opened.
|
||||||
|
*/
|
||||||
|
const openExternalUrl: (url: string) => Promise<any> = (url) =>
|
||||||
|
global.core.api?.openExternalUrl(url)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the resource path of the application.
|
||||||
|
*
|
||||||
|
* @returns {Promise<string>} - A promise that resolves with the resource path.
|
||||||
|
*/
|
||||||
const getResourcePath: () => Promise<string> = () => global.core.api?.getResourcePath()
|
const getResourcePath: () => Promise<string> = () => global.core.api?.getResourcePath()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -74,4 +95,6 @@ export {
|
|||||||
openFileExplorer,
|
openFileExplorer,
|
||||||
getResourcePath,
|
getResourcePath,
|
||||||
joinPath,
|
joinPath,
|
||||||
|
openExternalUrl,
|
||||||
|
baseName,
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,89 +1,74 @@
|
|||||||
/**
|
/**
|
||||||
* Writes data to a file at the specified path.
|
* Writes data to a file at the specified path.
|
||||||
* @param {string} path - The path to the file.
|
|
||||||
* @param {string} data - The data to write to the file.
|
|
||||||
* @returns {Promise<any>} A Promise that resolves when the file is written successfully.
|
* @returns {Promise<any>} A Promise that resolves when the file is written successfully.
|
||||||
*/
|
*/
|
||||||
const writeFile: (path: string, data: string) => Promise<any> = (path, data) =>
|
const writeFileSync = (...args: any[]) => global.core.api?.writeFileSync(...args)
|
||||||
global.core.api?.writeFile(path, data)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks whether the path is a directory.
|
|
||||||
* @param path - The path to check.
|
|
||||||
* @returns {boolean} A boolean indicating whether the path is a directory.
|
|
||||||
*/
|
|
||||||
const isDirectory = (path: string): Promise<boolean> => global.core.api?.isDirectory(path)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reads the contents of a file at the specified path.
|
* Reads the contents of a file at the specified path.
|
||||||
* @param {string} path - The path of the file to read.
|
|
||||||
* @returns {Promise<any>} A Promise that resolves with the contents of the file.
|
* @returns {Promise<any>} A Promise that resolves with the contents of the file.
|
||||||
*/
|
*/
|
||||||
const readFile: (path: string) => Promise<any> = (path) => global.core.api?.readFile(path)
|
const readFileSync = (...args: any[]) => global.core.api?.readFileSync(...args)
|
||||||
/**
|
/**
|
||||||
* Check whether the file exists
|
* Check whether the file exists
|
||||||
* @param {string} path
|
* @param {string} path
|
||||||
* @returns {boolean} A boolean indicating whether the path is a file.
|
* @returns {boolean} A boolean indicating whether the path is a file.
|
||||||
*/
|
*/
|
||||||
const exists = (path: string): Promise<boolean> => global.core.api?.exists(path)
|
const existsSync = (...args: any[]) => global.core.api?.existsSync(...args)
|
||||||
/**
|
/**
|
||||||
* List the directory files
|
* List the directory files
|
||||||
* @param {string} path - The path of the directory to list files.
|
|
||||||
* @returns {Promise<any>} A Promise that resolves with the contents of the directory.
|
* @returns {Promise<any>} A Promise that resolves with the contents of the directory.
|
||||||
*/
|
*/
|
||||||
const listFiles: (path: string) => Promise<any> = (path) => global.core.api?.listFiles(path)
|
const readdirSync = (...args: any[]) => global.core.api?.readdirSync(...args)
|
||||||
/**
|
/**
|
||||||
* Creates a directory at the specified path.
|
* Creates a directory at the specified path.
|
||||||
* @param {string} path - The path of the directory to create.
|
|
||||||
* @returns {Promise<any>} A Promise that resolves when the directory is created successfully.
|
* @returns {Promise<any>} A Promise that resolves when the directory is created successfully.
|
||||||
*/
|
*/
|
||||||
const mkdir: (path: string) => Promise<any> = (path) => global.core.api?.mkdir(path)
|
const mkdirSync = (...args: any[]) => global.core.api?.mkdirSync(...args)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes a directory at the specified path.
|
* Removes a directory at the specified path.
|
||||||
* @param {string} path - The path of the directory to remove.
|
|
||||||
* @returns {Promise<any>} A Promise that resolves when the directory is removed successfully.
|
* @returns {Promise<any>} A Promise that resolves when the directory is removed successfully.
|
||||||
*/
|
*/
|
||||||
const rmdir: (path: string) => Promise<any> = (path) => global.core.api?.rmdir(path)
|
const rmdirSync = (...args: any[]) =>
|
||||||
|
global.core.api?.rmdirSync(...args, { recursive: true, force: true })
|
||||||
/**
|
/**
|
||||||
* Deletes a file from the local file system.
|
* Deletes a file from the local file system.
|
||||||
* @param {string} path - The path of the file to delete.
|
* @param {string} path - The path of the file to delete.
|
||||||
* @returns {Promise<any>} A Promise that resolves when the file is deleted.
|
* @returns {Promise<any>} A Promise that resolves when the file is deleted.
|
||||||
*/
|
*/
|
||||||
const deleteFile: (path: string) => Promise<any> = (path) => global.core.api?.deleteFile(path)
|
const unlinkSync = (...args: any[]) => global.core.api?.unlinkSync(...args)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Appends data to a file at the specified path.
|
* Appends data to a file at the specified path.
|
||||||
* @param path path to the file
|
|
||||||
* @param data data to append
|
|
||||||
*/
|
*/
|
||||||
const appendFile: (path: string, data: string) => Promise<any> = (path, data) =>
|
const appendFileSync = (...args: any[]) => global.core.api?.appendFileSync(...args)
|
||||||
global.core.api?.appendFile(path, data)
|
|
||||||
|
|
||||||
const copyFile: (src: string, dest: string) => Promise<any> = (src, dest) =>
|
|
||||||
global.core.api?.copyFile(src, dest)
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Synchronizes a file from a source path to a destination path.
|
||||||
|
* @param {string} src - The source path of the file to be synchronized.
|
||||||
|
* @param {string} dest - The destination path where the file will be synchronized to.
|
||||||
|
* @returns {Promise<any>} - A promise that resolves when the file has been successfully synchronized.
|
||||||
|
*/
|
||||||
const syncFile: (src: string, dest: string) => Promise<any> = (src, dest) =>
|
const syncFile: (src: string, dest: string) => Promise<any> = (src, dest) =>
|
||||||
global.core.api?.syncFile(src, dest)
|
global.core.api?.syncFile(src, dest)
|
||||||
/**
|
|
||||||
* Reads a file line by line.
|
|
||||||
* @param {string} path - The path of the file to read.
|
|
||||||
* @returns {Promise<any>} A promise that resolves to the lines of the file.
|
|
||||||
*/
|
|
||||||
const readLineByLine: (path: string) => Promise<any> = (path) =>
|
|
||||||
global.core.api?.readLineByLine(path)
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copy file sync.
|
||||||
|
*/
|
||||||
|
const copyFileSync = (...args: any[]) => global.core.api?.copyFileSync(...args)
|
||||||
|
|
||||||
|
// TODO: Export `dummy` fs functions automatically
|
||||||
|
// Currently adding these manually
|
||||||
export const fs = {
|
export const fs = {
|
||||||
isDirectory,
|
writeFileSync,
|
||||||
writeFile,
|
readFileSync,
|
||||||
readFile,
|
existsSync,
|
||||||
exists,
|
readdirSync,
|
||||||
listFiles,
|
mkdirSync,
|
||||||
mkdir,
|
rmdirSync,
|
||||||
rmdir,
|
unlinkSync,
|
||||||
deleteFile,
|
appendFileSync,
|
||||||
appendFile,
|
copyFileSync,
|
||||||
readLineByLine,
|
|
||||||
copyFile,
|
|
||||||
syncFile,
|
syncFile,
|
||||||
}
|
}
|
||||||
|
|||||||
8
core/src/node/api/HttpServer.ts
Normal file
8
core/src/node/api/HttpServer.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
export interface HttpServer {
|
||||||
|
post: (route: string, handler: (req: any, res: any) => Promise<any>) => void
|
||||||
|
get: (route: string, handler: (req: any, res: any) => Promise<any>) => void
|
||||||
|
patch: (route: string, handler: (req: any, res: any) => Promise<any>) => void
|
||||||
|
put: (route: string, handler: (req: any, res: any) => Promise<any>) => void
|
||||||
|
delete: (route: string, handler: (req: any, res: any) => Promise<any>) => void
|
||||||
|
register: (router: any, opts?: any) => void
|
||||||
|
}
|
||||||
335
core/src/node/api/common/builder.ts
Normal file
335
core/src/node/api/common/builder.ts
Normal file
@ -0,0 +1,335 @@
|
|||||||
|
import fs from 'fs'
|
||||||
|
import { JanApiRouteConfiguration, RouteConfiguration } from './configuration'
|
||||||
|
import { join } from 'path'
|
||||||
|
import { Model, ThreadMessage } from './../../../index'
|
||||||
|
|
||||||
|
import fetch from 'node-fetch'
|
||||||
|
import { ulid } from 'ulid'
|
||||||
|
import request from 'request'
|
||||||
|
|
||||||
|
const progress = require('request-progress')
|
||||||
|
const os = require('os')
|
||||||
|
|
||||||
|
const path = join(os.homedir(), 'jan')
|
||||||
|
|
||||||
|
export const getBuilder = async (configuration: RouteConfiguration) => {
|
||||||
|
const directoryPath = join(path, configuration.dirName)
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!(await fs.existsSync(directoryPath))) {
|
||||||
|
console.debug('model folder not found')
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
const files: string[] = await fs.readdirSync(directoryPath)
|
||||||
|
|
||||||
|
const allDirectories: string[] = []
|
||||||
|
for (const file of files) {
|
||||||
|
if (file === '.DS_Store') continue
|
||||||
|
allDirectories.push(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
const readJsonPromises = allDirectories.map(async (dirName) => {
|
||||||
|
const jsonPath = join(directoryPath, dirName, configuration.metadataFileName)
|
||||||
|
return await readModelMetadata(jsonPath)
|
||||||
|
})
|
||||||
|
|
||||||
|
const results = await Promise.all(readJsonPromises)
|
||||||
|
const modelData = results
|
||||||
|
.map((result: any) => {
|
||||||
|
try {
|
||||||
|
return JSON.parse(result)
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter((e: any) => !!e)
|
||||||
|
|
||||||
|
return modelData
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const readModelMetadata = async (path: string) => {
|
||||||
|
return fs.readFileSync(path, 'utf-8')
|
||||||
|
}
|
||||||
|
|
||||||
|
export const retrieveBuilder = async (configuration: RouteConfiguration, id: string) => {
|
||||||
|
const data = await getBuilder(configuration)
|
||||||
|
const filteredData = data.filter((d: any) => d.id === id)[0]
|
||||||
|
|
||||||
|
if (!filteredData) {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
return filteredData
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deleteBuilder = async (configuration: RouteConfiguration, id: string) => {
|
||||||
|
if (configuration.dirName === 'assistants' && id === 'jan') {
|
||||||
|
return {
|
||||||
|
message: 'Cannot delete Jan assistant',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const directoryPath = join(path, configuration.dirName)
|
||||||
|
try {
|
||||||
|
const data = await retrieveBuilder(configuration, id)
|
||||||
|
if (!data || !data.keys) {
|
||||||
|
return {
|
||||||
|
message: 'Not found',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const myPath = join(directoryPath, id)
|
||||||
|
fs.rmdirSync(myPath, { recursive: true })
|
||||||
|
return {
|
||||||
|
id: id,
|
||||||
|
object: configuration.delete.object,
|
||||||
|
deleted: true,
|
||||||
|
}
|
||||||
|
} catch (ex) {
|
||||||
|
console.error(ex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getMessages = async (threadId: string) => {
|
||||||
|
const threadDirPath = join(path, 'threads', threadId)
|
||||||
|
const messageFile = 'messages.jsonl'
|
||||||
|
try {
|
||||||
|
const files: string[] = await fs.readdirSync(threadDirPath)
|
||||||
|
if (!files.includes(messageFile)) {
|
||||||
|
throw Error(`${threadDirPath} not contains message file`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const messageFilePath = join(threadDirPath, messageFile)
|
||||||
|
|
||||||
|
const lines = fs
|
||||||
|
.readFileSync(messageFilePath, 'utf-8')
|
||||||
|
.toString()
|
||||||
|
.split('\n')
|
||||||
|
.filter((line: any) => line !== '')
|
||||||
|
|
||||||
|
const messages: ThreadMessage[] = []
|
||||||
|
lines.forEach((line: string) => {
|
||||||
|
messages.push(JSON.parse(line) as ThreadMessage)
|
||||||
|
})
|
||||||
|
return messages
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const retrieveMesasge = async (threadId: string, messageId: string) => {
|
||||||
|
const messages = await getMessages(threadId)
|
||||||
|
const filteredMessages = messages.filter((m) => m.id === messageId)
|
||||||
|
if (!filteredMessages || filteredMessages.length === 0) {
|
||||||
|
return {
|
||||||
|
message: 'Not found',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return filteredMessages[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createThread = async (thread: any) => {
|
||||||
|
const threadMetadataFileName = 'thread.json'
|
||||||
|
// TODO: add validation
|
||||||
|
if (!thread.assistants || thread.assistants.length === 0) {
|
||||||
|
return {
|
||||||
|
message: 'Thread must have at least one assistant',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const threadId = generateThreadId(thread.assistants[0].assistant_id)
|
||||||
|
try {
|
||||||
|
const updatedThread = {
|
||||||
|
...thread,
|
||||||
|
id: threadId,
|
||||||
|
created: Date.now(),
|
||||||
|
updated: Date.now(),
|
||||||
|
}
|
||||||
|
const threadDirPath = join(path, 'threads', updatedThread.id)
|
||||||
|
const threadJsonPath = join(threadDirPath, threadMetadataFileName)
|
||||||
|
|
||||||
|
if (!fs.existsSync(threadDirPath)) {
|
||||||
|
fs.mkdirSync(threadDirPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
await fs.writeFileSync(threadJsonPath, JSON.stringify(updatedThread, null, 2))
|
||||||
|
return updatedThread
|
||||||
|
} catch (err) {
|
||||||
|
return {
|
||||||
|
error: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const updateThread = async (threadId: string, thread: any) => {
|
||||||
|
const threadMetadataFileName = 'thread.json'
|
||||||
|
const currentThreadData = await retrieveBuilder(JanApiRouteConfiguration.threads, threadId)
|
||||||
|
if (!currentThreadData) {
|
||||||
|
return {
|
||||||
|
message: 'Thread not found',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// we don't want to update the id and object
|
||||||
|
delete thread.id
|
||||||
|
delete thread.object
|
||||||
|
|
||||||
|
const updatedThread = {
|
||||||
|
...currentThreadData,
|
||||||
|
...thread,
|
||||||
|
updated: Date.now(),
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const threadDirPath = join(path, 'threads', updatedThread.id)
|
||||||
|
const threadJsonPath = join(threadDirPath, threadMetadataFileName)
|
||||||
|
|
||||||
|
await fs.writeFileSync(threadJsonPath, JSON.stringify(updatedThread, null, 2))
|
||||||
|
return updatedThread
|
||||||
|
} catch (err) {
|
||||||
|
return {
|
||||||
|
message: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const generateThreadId = (assistantId: string) => {
|
||||||
|
return `${assistantId}_${(Date.now() / 1000).toFixed(0)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createMessage = async (threadId: string, message: any) => {
|
||||||
|
const threadMessagesFileName = 'messages.jsonl'
|
||||||
|
// TODO: add validation
|
||||||
|
try {
|
||||||
|
const msgId = ulid()
|
||||||
|
const createdAt = Date.now()
|
||||||
|
const threadMessage: ThreadMessage = {
|
||||||
|
...message,
|
||||||
|
id: msgId,
|
||||||
|
thread_id: threadId,
|
||||||
|
created: createdAt,
|
||||||
|
updated: createdAt,
|
||||||
|
object: 'thread.message',
|
||||||
|
}
|
||||||
|
|
||||||
|
const threadDirPath = join(path, 'threads', threadId)
|
||||||
|
const threadMessagePath = join(threadDirPath, threadMessagesFileName)
|
||||||
|
|
||||||
|
if (!fs.existsSync(threadDirPath)) {
|
||||||
|
fs.mkdirSync(threadDirPath)
|
||||||
|
}
|
||||||
|
fs.appendFileSync(threadMessagePath, JSON.stringify(threadMessage) + '\n')
|
||||||
|
return threadMessage
|
||||||
|
} catch (err) {
|
||||||
|
return {
|
||||||
|
message: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const downloadModel = async (modelId: string) => {
|
||||||
|
const model = await retrieveBuilder(JanApiRouteConfiguration.models, modelId)
|
||||||
|
if (!model || model.object !== 'model') {
|
||||||
|
return {
|
||||||
|
message: 'Model not found',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const directoryPath = join(path, 'models', modelId)
|
||||||
|
if (!fs.existsSync(directoryPath)) {
|
||||||
|
fs.mkdirSync(directoryPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// path to model binary
|
||||||
|
const modelBinaryPath = join(directoryPath, modelId)
|
||||||
|
const rq = request(model.source_url)
|
||||||
|
|
||||||
|
progress(rq, {})
|
||||||
|
.on('progress', function (state: any) {
|
||||||
|
console.log('progress', JSON.stringify(state, null, 2))
|
||||||
|
})
|
||||||
|
.on('error', function (err: Error) {
|
||||||
|
console.error('error', err)
|
||||||
|
})
|
||||||
|
.on('end', function () {
|
||||||
|
console.log('end')
|
||||||
|
})
|
||||||
|
.pipe(fs.createWriteStream(modelBinaryPath))
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: `Starting download ${modelId}`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const chatCompletions = async (request: any, reply: any) => {
|
||||||
|
const modelList = await getBuilder(JanApiRouteConfiguration.models)
|
||||||
|
const modelId = request.body.model
|
||||||
|
|
||||||
|
const matchedModels = modelList.filter((model: Model) => model.id === modelId)
|
||||||
|
if (matchedModels.length === 0) {
|
||||||
|
const error = {
|
||||||
|
error: {
|
||||||
|
message: `The model ${request.body.model} does not exist`,
|
||||||
|
type: 'invalid_request_error',
|
||||||
|
param: null,
|
||||||
|
code: 'model_not_found',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
reply.code(404).send(error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestedModel = matchedModels[0]
|
||||||
|
const engineConfiguration = await getEngineConfiguration(requestedModel.engine)
|
||||||
|
|
||||||
|
let apiKey: string | undefined = undefined
|
||||||
|
let apiUrl: string = 'http://127.0.0.1:3928/inferences/llamacpp/chat_completion' // default nitro url
|
||||||
|
|
||||||
|
if (engineConfiguration) {
|
||||||
|
apiKey = engineConfiguration.api_key
|
||||||
|
apiUrl = engineConfiguration.full_url
|
||||||
|
}
|
||||||
|
|
||||||
|
reply.raw.writeHead(200, {
|
||||||
|
'Content-Type': 'text/event-stream',
|
||||||
|
'Cache-Control': 'no-cache',
|
||||||
|
Connection: 'keep-alive',
|
||||||
|
})
|
||||||
|
|
||||||
|
const headers: Record<string, any> = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
}
|
||||||
|
|
||||||
|
if (apiKey) {
|
||||||
|
headers['Authorization'] = `Bearer ${apiKey}`
|
||||||
|
headers['api-key'] = apiKey
|
||||||
|
}
|
||||||
|
console.log(apiUrl)
|
||||||
|
console.log(JSON.stringify(headers))
|
||||||
|
const response = await fetch(apiUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: headers,
|
||||||
|
body: JSON.stringify(request.body),
|
||||||
|
})
|
||||||
|
if (response.status !== 200) {
|
||||||
|
console.error(response)
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
response.body.pipe(reply.raw)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getEngineConfiguration = async (engineId: string) => {
|
||||||
|
if (engineId !== 'openai') {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
const directoryPath = join(path, 'engines')
|
||||||
|
const filePath = join(directoryPath, `${engineId}.json`)
|
||||||
|
const data = await fs.readFileSync(filePath, 'utf-8')
|
||||||
|
return JSON.parse(data)
|
||||||
|
}
|
||||||
31
core/src/node/api/common/configuration.ts
Normal file
31
core/src/node/api/common/configuration.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
export const JanApiRouteConfiguration: Record<string, RouteConfiguration> = {
|
||||||
|
models: {
|
||||||
|
dirName: 'models',
|
||||||
|
metadataFileName: 'model.json',
|
||||||
|
delete: {
|
||||||
|
object: 'model',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
assistants: {
|
||||||
|
dirName: 'assistants',
|
||||||
|
metadataFileName: 'assistant.json',
|
||||||
|
delete: {
|
||||||
|
object: 'assistant',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
threads: {
|
||||||
|
dirName: 'threads',
|
||||||
|
metadataFileName: 'thread.json',
|
||||||
|
delete: {
|
||||||
|
object: 'thread',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RouteConfiguration = {
|
||||||
|
dirName: string
|
||||||
|
metadataFileName: string
|
||||||
|
delete: {
|
||||||
|
object: string
|
||||||
|
}
|
||||||
|
}
|
||||||
2
core/src/node/api/index.ts
Normal file
2
core/src/node/api/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './HttpServer'
|
||||||
|
export * from './routes'
|
||||||
46
core/src/node/api/routes/common.ts
Normal file
46
core/src/node/api/routes/common.ts
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import { AppRoute } from '../../../api'
|
||||||
|
import { HttpServer } from '../HttpServer'
|
||||||
|
import { basename, join } from 'path'
|
||||||
|
import {
|
||||||
|
chatCompletions,
|
||||||
|
deleteBuilder,
|
||||||
|
downloadModel,
|
||||||
|
getBuilder,
|
||||||
|
retrieveBuilder,
|
||||||
|
} from '../common/builder'
|
||||||
|
|
||||||
|
import { JanApiRouteConfiguration } from '../common/configuration'
|
||||||
|
|
||||||
|
export const commonRouter = async (app: HttpServer) => {
|
||||||
|
// Common Routes
|
||||||
|
Object.keys(JanApiRouteConfiguration).forEach((key) => {
|
||||||
|
app.get(`/${key}`, async (_request) => getBuilder(JanApiRouteConfiguration[key]))
|
||||||
|
|
||||||
|
app.get(`/${key}/:id`, async (request: any) =>
|
||||||
|
retrieveBuilder(JanApiRouteConfiguration[key], request.params.id),
|
||||||
|
)
|
||||||
|
|
||||||
|
app.delete(`/${key}/:id`, async (request: any) =>
|
||||||
|
deleteBuilder(JanApiRouteConfiguration[key], request.params.id),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Download Model Routes
|
||||||
|
app.get(`/models/download/:modelId`, async (request: any) =>
|
||||||
|
downloadModel(request.params.modelId),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Chat Completion Routes
|
||||||
|
app.post(`/chat/completions`, async (request: any, reply: any) => chatCompletions(request, reply))
|
||||||
|
|
||||||
|
// App Routes
|
||||||
|
app.post(`/app/${AppRoute.joinPath}`, async (request: any, reply: any) => {
|
||||||
|
const args = JSON.parse(request.body) as any[]
|
||||||
|
reply.send(JSON.stringify(join(...args[0])))
|
||||||
|
})
|
||||||
|
|
||||||
|
app.post(`/app/${AppRoute.baseName}`, async (request: any, reply: any) => {
|
||||||
|
const args = JSON.parse(request.body) as any[]
|
||||||
|
reply.send(JSON.stringify(basename(args[0])))
|
||||||
|
})
|
||||||
|
}
|
||||||
54
core/src/node/api/routes/download.ts
Normal file
54
core/src/node/api/routes/download.ts
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import { DownloadRoute } from '../../../api'
|
||||||
|
import { join } from 'path'
|
||||||
|
import { userSpacePath, DownloadManager, HttpServer } from '../../index'
|
||||||
|
import { createWriteStream } from 'fs'
|
||||||
|
|
||||||
|
const request = require('request')
|
||||||
|
const progress = require('request-progress')
|
||||||
|
|
||||||
|
export const downloadRouter = async (app: HttpServer) => {
|
||||||
|
app.post(`/${DownloadRoute.downloadFile}`, async (req, res) => {
|
||||||
|
const body = JSON.parse(req.body as any)
|
||||||
|
const normalizedArgs = body.map((arg: any) => {
|
||||||
|
if (typeof arg === 'string' && arg.includes('file:/')) {
|
||||||
|
return join(userSpacePath, arg.replace('file:/', ''))
|
||||||
|
}
|
||||||
|
return arg
|
||||||
|
})
|
||||||
|
|
||||||
|
const localPath = normalizedArgs[1]
|
||||||
|
const fileName = localPath.split('/').pop() ?? ''
|
||||||
|
|
||||||
|
const rq = request(normalizedArgs[0])
|
||||||
|
progress(rq, {})
|
||||||
|
.on('progress', function (state: any) {
|
||||||
|
console.log('download onProgress', state)
|
||||||
|
})
|
||||||
|
.on('error', function (err: Error) {
|
||||||
|
console.log('download onError', err)
|
||||||
|
})
|
||||||
|
.on('end', function () {
|
||||||
|
console.log('download onEnd')
|
||||||
|
})
|
||||||
|
.pipe(createWriteStream(normalizedArgs[1]))
|
||||||
|
|
||||||
|
DownloadManager.instance.setRequest(fileName, rq)
|
||||||
|
})
|
||||||
|
|
||||||
|
app.post(`/${DownloadRoute.abortDownload}`, async (req, res) => {
|
||||||
|
const body = JSON.parse(req.body as any)
|
||||||
|
const normalizedArgs = body.map((arg: any) => {
|
||||||
|
if (typeof arg === 'string' && arg.includes('file:/')) {
|
||||||
|
return join(userSpacePath, arg.replace('file:/', ''))
|
||||||
|
}
|
||||||
|
return arg
|
||||||
|
})
|
||||||
|
|
||||||
|
const localPath = normalizedArgs[0]
|
||||||
|
const fileName = localPath.split('/').pop() ?? ''
|
||||||
|
console.debug('fileName', fileName)
|
||||||
|
const rq = DownloadManager.instance.networkRequests[fileName]
|
||||||
|
DownloadManager.instance.networkRequests[fileName] = undefined
|
||||||
|
rq?.abort()
|
||||||
|
})
|
||||||
|
}
|
||||||
51
core/src/node/api/routes/extension.ts
Normal file
51
core/src/node/api/routes/extension.ts
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import { join, extname } from 'path'
|
||||||
|
import { ExtensionRoute } from '../../../api'
|
||||||
|
import {
|
||||||
|
userSpacePath,
|
||||||
|
ModuleManager,
|
||||||
|
getActiveExtensions,
|
||||||
|
installExtensions,
|
||||||
|
HttpServer,
|
||||||
|
} from '../../index'
|
||||||
|
import { readdirSync } from 'fs'
|
||||||
|
|
||||||
|
export const extensionRouter = async (app: HttpServer) => {
|
||||||
|
// TODO: Share code between node projects
|
||||||
|
app.post(`/${ExtensionRoute.getActiveExtensions}`, async (req, res) => {
|
||||||
|
const activeExtensions = await getActiveExtensions()
|
||||||
|
res.status(200).send(activeExtensions)
|
||||||
|
})
|
||||||
|
|
||||||
|
app.post(`/${ExtensionRoute.baseExtensions}`, async (req, res) => {
|
||||||
|
const baseExtensionPath = join(__dirname, '..', '..', '..', 'pre-install')
|
||||||
|
const extensions = readdirSync(baseExtensionPath)
|
||||||
|
.filter((file) => extname(file) === '.tgz')
|
||||||
|
.map((file) => join(baseExtensionPath, file))
|
||||||
|
|
||||||
|
res.status(200).send(extensions)
|
||||||
|
})
|
||||||
|
|
||||||
|
app.post(`/${ExtensionRoute.installExtension}`, async (req, res) => {
|
||||||
|
const extensions = req.body as any
|
||||||
|
const installed = await installExtensions(JSON.parse(extensions)[0])
|
||||||
|
return JSON.parse(JSON.stringify(installed))
|
||||||
|
})
|
||||||
|
|
||||||
|
app.post(`/${ExtensionRoute.invokeExtensionFunc}`, async (req, res) => {
|
||||||
|
const args = JSON.parse(req.body as any)
|
||||||
|
console.debug(args)
|
||||||
|
const module = await import(join(userSpacePath, 'extensions', args[0]))
|
||||||
|
|
||||||
|
ModuleManager.instance.setModule(args[0], module)
|
||||||
|
const method = args[1]
|
||||||
|
if (typeof module[method] === 'function') {
|
||||||
|
// remove first item from args
|
||||||
|
const newArgs = args.slice(2)
|
||||||
|
console.log(newArgs)
|
||||||
|
return module[method](...args.slice(2))
|
||||||
|
} else {
|
||||||
|
console.debug(module[method])
|
||||||
|
console.error(`Function "${method}" does not exist in the module.`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
27
core/src/node/api/routes/fs.ts
Normal file
27
core/src/node/api/routes/fs.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { FileSystemRoute } from '../../../api'
|
||||||
|
import { join } from 'path'
|
||||||
|
import { HttpServer, userSpacePath } from '../../index'
|
||||||
|
|
||||||
|
export const fsRouter = async (app: HttpServer) => {
|
||||||
|
const moduleName = 'fs'
|
||||||
|
// Generate handlers for each fs route
|
||||||
|
Object.values(FileSystemRoute).forEach((route) => {
|
||||||
|
app.post(`/${route}`, async (req, res) => {
|
||||||
|
const body = JSON.parse(req.body as any)
|
||||||
|
try {
|
||||||
|
const result = await import(moduleName).then((mdl) => {
|
||||||
|
return mdl[route](
|
||||||
|
...body.map((arg: any) =>
|
||||||
|
typeof arg === 'string' && arg.includes('file:/')
|
||||||
|
? join(userSpacePath, arg.replace('file:/', ''))
|
||||||
|
: arg,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
res.status(200).send(result)
|
||||||
|
} catch (ex) {
|
||||||
|
console.log(ex)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
6
core/src/node/api/routes/index.ts
Normal file
6
core/src/node/api/routes/index.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export * from './download'
|
||||||
|
export * from './extension'
|
||||||
|
export * from './fs'
|
||||||
|
export * from './thread'
|
||||||
|
export * from './common'
|
||||||
|
export * from './v1'
|
||||||
30
core/src/node/api/routes/thread.ts
Normal file
30
core/src/node/api/routes/thread.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { HttpServer } from '../HttpServer'
|
||||||
|
import {
|
||||||
|
createMessage,
|
||||||
|
createThread,
|
||||||
|
getMessages,
|
||||||
|
retrieveMesasge,
|
||||||
|
updateThread,
|
||||||
|
} from '../common/builder'
|
||||||
|
|
||||||
|
export const threadRouter = async (app: HttpServer) => {
|
||||||
|
// create thread
|
||||||
|
app.post(`/`, async (req, res) => createThread(req.body))
|
||||||
|
|
||||||
|
app.get(`/:threadId/messages`, async (req, res) => getMessages(req.params.threadId))
|
||||||
|
|
||||||
|
// retrieve message
|
||||||
|
app.get(`/:threadId/messages/:messageId`, async (req, res) =>
|
||||||
|
retrieveMesasge(req.params.threadId, req.params.messageId),
|
||||||
|
)
|
||||||
|
|
||||||
|
// create message
|
||||||
|
app.post(`/:threadId/messages`, async (req, res) =>
|
||||||
|
createMessage(req.params.threadId as any, req.body as any),
|
||||||
|
)
|
||||||
|
|
||||||
|
// modify thread
|
||||||
|
app.patch(`/:threadId`, async (request: any) =>
|
||||||
|
updateThread(request.params.threadId, request.body),
|
||||||
|
)
|
||||||
|
}
|
||||||
21
core/src/node/api/routes/v1.ts
Normal file
21
core/src/node/api/routes/v1.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { HttpServer } from '../HttpServer'
|
||||||
|
import { commonRouter, threadRouter, fsRouter, extensionRouter, downloadRouter } from './index'
|
||||||
|
|
||||||
|
export const v1Router = async (app: HttpServer) => {
|
||||||
|
// MARK: External Routes
|
||||||
|
app.register(commonRouter)
|
||||||
|
app.register(threadRouter, {
|
||||||
|
prefix: '/thread',
|
||||||
|
})
|
||||||
|
|
||||||
|
// MARK: Internal Application Routes
|
||||||
|
app.register(fsRouter, {
|
||||||
|
prefix: '/fs',
|
||||||
|
})
|
||||||
|
app.register(extensionRouter, {
|
||||||
|
prefix: '/extension',
|
||||||
|
})
|
||||||
|
app.register(downloadRouter, {
|
||||||
|
prefix: '/download',
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -1,14 +1,14 @@
|
|||||||
import { rmdir } from 'fs/promises'
|
import { rmdirSync } from 'fs'
|
||||||
import { resolve, join } from 'path'
|
import { resolve, join } from 'path'
|
||||||
import { manifest, extract } from 'pacote'
|
import { manifest, extract } from 'pacote'
|
||||||
import * as Arborist from '@npmcli/arborist'
|
import * as Arborist from '@npmcli/arborist'
|
||||||
import { ExtensionManager } from './../managers/extension'
|
import { ExtensionManager } from './manager'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An NPM package that can be used as an 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.
|
* Used to hold all the information and functions necessary to handle the extension lifecycle.
|
||||||
*/
|
*/
|
||||||
class Extension {
|
export default class Extension {
|
||||||
/**
|
/**
|
||||||
* @property {string} origin Original specification provided to fetch the package.
|
* @property {string} origin Original specification provided to fetch the package.
|
||||||
* @property {Object} installOptions Options provided to pacote when fetching the manifest.
|
* @property {Object} installOptions Options provided to pacote when fetching the manifest.
|
||||||
@ -56,10 +56,7 @@ class Extension {
|
|||||||
* @type {string}
|
* @type {string}
|
||||||
*/
|
*/
|
||||||
get specifier() {
|
get specifier() {
|
||||||
return (
|
return this.origin + (this.installOptions.version ? '@' + this.installOptions.version : '')
|
||||||
this.origin +
|
|
||||||
(this.installOptions.version ? '@' + this.installOptions.version : '')
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -85,9 +82,7 @@ class Extension {
|
|||||||
this.main = mnf.main
|
this.main = mnf.main
|
||||||
this.description = mnf.description
|
this.description = mnf.description
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(
|
throw new Error(`Package ${this.origin} does not contain a valid manifest: ${error}`)
|
||||||
`Package ${this.origin} does not contain a valid manifest: ${error}`
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
@ -107,7 +102,7 @@ class Extension {
|
|||||||
await extract(
|
await extract(
|
||||||
this.specifier,
|
this.specifier,
|
||||||
join(ExtensionManager.instance.extensionsPath ?? '', this.name ?? ''),
|
join(ExtensionManager.instance.extensionsPath ?? '', this.name ?? ''),
|
||||||
this.installOptions
|
this.installOptions,
|
||||||
)
|
)
|
||||||
|
|
||||||
// Set the url using the custom extensions protocol
|
// Set the url using the custom extensions protocol
|
||||||
@ -180,11 +175,8 @@ class Extension {
|
|||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
async uninstall() {
|
async uninstall() {
|
||||||
const extPath = resolve(
|
const extPath = resolve(ExtensionManager.instance.extensionsPath ?? '', this.name ?? '')
|
||||||
ExtensionManager.instance.extensionsPath ?? '',
|
await rmdirSync(extPath, { recursive: true })
|
||||||
this.name ?? ''
|
|
||||||
)
|
|
||||||
await rmdir(extPath, { recursive: true })
|
|
||||||
|
|
||||||
this.emitUpdate()
|
this.emitUpdate()
|
||||||
}
|
}
|
||||||
@ -200,5 +192,3 @@ class Extension {
|
|||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Extension
|
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import { readFileSync } from 'fs'
|
import { readFileSync } from 'fs'
|
||||||
import { protocol } from 'electron'
|
|
||||||
import { normalize } from 'path'
|
import { normalize } from 'path'
|
||||||
|
|
||||||
import Extension from './extension'
|
import Extension from './extension'
|
||||||
@ -12,18 +12,8 @@ import {
|
|||||||
getActiveExtensions,
|
getActiveExtensions,
|
||||||
addExtension,
|
addExtension,
|
||||||
} from './store'
|
} from './store'
|
||||||
import { ExtensionManager } from './../managers/extension'
|
import { ExtensionManager } from './manager'
|
||||||
|
|
||||||
/**
|
|
||||||
* 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) {
|
export function init(options: any) {
|
||||||
// Create extensions protocol to serve extensions to renderer
|
// Create extensions protocol to serve extensions to renderer
|
||||||
registerExtensionProtocol()
|
registerExtensionProtocol()
|
||||||
@ -41,13 +31,24 @@ export function init(options: any) {
|
|||||||
* @private
|
* @private
|
||||||
* @returns {boolean} Whether the protocol registration was successful
|
* @returns {boolean} Whether the protocol registration was successful
|
||||||
*/
|
*/
|
||||||
function registerExtensionProtocol() {
|
async function registerExtensionProtocol() {
|
||||||
return protocol.registerFileProtocol('extension', (request, callback) => {
|
let electron: any = undefined
|
||||||
const entry = request.url.substr('extension://'.length - 1)
|
|
||||||
|
|
||||||
const url = normalize(ExtensionManager.instance.extensionsPath + entry)
|
try {
|
||||||
callback({ path: url })
|
const moduleName = "electron"
|
||||||
})
|
electron = await import(moduleName)
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Electron is not available')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (electron) {
|
||||||
|
return electron.protocol.registerFileProtocol('extension', (request: any, callback: any) => {
|
||||||
|
const entry = request.url.substr('extension://'.length - 1)
|
||||||
|
|
||||||
|
const url = normalize(ExtensionManager.instance.extensionsPath + entry)
|
||||||
|
callback({ path: url })
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -57,8 +58,7 @@ function registerExtensionProtocol() {
|
|||||||
* @returns {extensionManager} A set of functions used to manage the extension lifecycle.
|
* @returns {extensionManager} A set of functions used to manage the extension lifecycle.
|
||||||
*/
|
*/
|
||||||
export function useExtensions(extensionsPath: string) {
|
export function useExtensions(extensionsPath: string) {
|
||||||
if (!extensionsPath)
|
if (!extensionsPath) throw Error('A path to the extensions folder is required to use extensions')
|
||||||
throw Error('A path to the extensions folder is required to use extensions')
|
|
||||||
// Store the path to the extensions folder
|
// Store the path to the extensions folder
|
||||||
ExtensionManager.instance.setExtensionsPath(extensionsPath)
|
ExtensionManager.instance.setExtensionsPath(extensionsPath)
|
||||||
|
|
||||||
@ -69,7 +69,7 @@ export function useExtensions(extensionsPath: string) {
|
|||||||
|
|
||||||
// Read extension list from extensions folder
|
// Read extension list from extensions folder
|
||||||
const extensions = JSON.parse(
|
const extensions = JSON.parse(
|
||||||
readFileSync(ExtensionManager.instance.getExtensionsFile(), 'utf-8')
|
readFileSync(ExtensionManager.instance.getExtensionsFile(), 'utf-8'),
|
||||||
)
|
)
|
||||||
try {
|
try {
|
||||||
// Create and store a Extension instance for each extension in list
|
// Create and store a Extension instance for each extension in list
|
||||||
@ -82,7 +82,7 @@ export function useExtensions(extensionsPath: string) {
|
|||||||
throw new Error(
|
throw new Error(
|
||||||
'Could not successfully rebuild list of installed extensions.\n' +
|
'Could not successfully rebuild list of installed extensions.\n' +
|
||||||
error +
|
error +
|
||||||
'\nPlease check the extensions.json file in the extensions folder.'
|
'\nPlease check the extensions.json file in the extensions folder.',
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -111,7 +111,6 @@ function loadExtension(ext: any) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
addExtension(extension, false)
|
addExtension(extension, false)
|
||||||
extension.subscribe('pe-persist', persistExtensions)
|
extension.subscribe('pe-persist', persistExtensions)
|
||||||
}
|
}
|
||||||
@ -123,7 +122,7 @@ function loadExtension(ext: any) {
|
|||||||
export function getStore() {
|
export function getStore() {
|
||||||
if (!ExtensionManager.instance.extensionsPath) {
|
if (!ExtensionManager.instance.extensionsPath) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'The extension path has not yet been set up. Please run useExtensions before accessing the store'
|
'The extension path has not yet been set up. Please run useExtensions before accessing the store',
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -134,4 +133,4 @@ export function getStore() {
|
|||||||
getActiveExtensions,
|
getActiveExtensions,
|
||||||
removeExtension,
|
removeExtension,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
61
core/src/node/extension/manager.ts
Normal file
61
core/src/node/extension/manager.ts
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import { join, resolve } from "path";
|
||||||
|
|
||||||
|
import { existsSync, mkdirSync, writeFileSync } from "fs";
|
||||||
|
import { init } from "./index";
|
||||||
|
import { homedir } from "os"
|
||||||
|
/**
|
||||||
|
* Manages extension installation and migration.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const userSpacePath = join(homedir(), "jan");
|
||||||
|
|
||||||
|
export class ExtensionManager {
|
||||||
|
public static instance: ExtensionManager = new ExtensionManager();
|
||||||
|
|
||||||
|
extensionsPath: string | undefined = join(userSpacePath, "extensions");
|
||||||
|
|
||||||
|
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"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,16 +1,6 @@
|
|||||||
/**
|
import { writeFileSync } from "fs";
|
||||||
* Provides access to the extensions stored by Extension Store
|
import Extension from "./extension";
|
||||||
* @typedef {Object} extensionManager
|
import { ExtensionManager } from "./manager";
|
||||||
* @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
|
* @module store
|
||||||
@ -21,7 +11,7 @@ import { ExtensionManager } from './../managers/extension'
|
|||||||
* Register of installed extensions
|
* Register of installed extensions
|
||||||
* @type {Object.<string, Extension>} extension - List of installed extensions
|
* @type {Object.<string, Extension>} extension - List of installed extensions
|
||||||
*/
|
*/
|
||||||
const extensions: Record<string, Extension> = {}
|
const extensions: Record<string, Extension> = {};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a extension from the stored extensions.
|
* Get a extension from the stored extensions.
|
||||||
@ -31,10 +21,10 @@ const extensions: Record<string, Extension> = {}
|
|||||||
*/
|
*/
|
||||||
export function getExtension(name: string) {
|
export function getExtension(name: string) {
|
||||||
if (!Object.prototype.hasOwnProperty.call(extensions, name)) {
|
if (!Object.prototype.hasOwnProperty.call(extensions, name)) {
|
||||||
throw new Error(`Extension ${name} does not exist`)
|
throw new Error(`Extension ${name} does not exist`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return extensions[name]
|
return extensions[name];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -43,7 +33,7 @@ export function getExtension(name: string) {
|
|||||||
* @alias extensionManager.getAllExtensions
|
* @alias extensionManager.getAllExtensions
|
||||||
*/
|
*/
|
||||||
export function getAllExtensions() {
|
export function getAllExtensions() {
|
||||||
return Object.values(extensions)
|
return Object.values(extensions);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -52,7 +42,7 @@ export function getAllExtensions() {
|
|||||||
* @alias extensionManager.getActiveExtensions
|
* @alias extensionManager.getActiveExtensions
|
||||||
*/
|
*/
|
||||||
export function getActiveExtensions() {
|
export function getActiveExtensions() {
|
||||||
return Object.values(extensions).filter((extension) => extension.active)
|
return Object.values(extensions).filter((extension) => extension.active);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -63,9 +53,9 @@ export function getActiveExtensions() {
|
|||||||
* @alias extensionManager.removeExtension
|
* @alias extensionManager.removeExtension
|
||||||
*/
|
*/
|
||||||
export function removeExtension(name: string, persist = true) {
|
export function removeExtension(name: string, persist = true) {
|
||||||
const del = delete extensions[name]
|
const del = delete extensions[name];
|
||||||
if (persist) persistExtensions()
|
if (persist) persistExtensions();
|
||||||
return del
|
return del;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -75,10 +65,10 @@ export function removeExtension(name: string, persist = true) {
|
|||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
export function addExtension(extension: Extension, persist = true) {
|
export function addExtension(extension: Extension, persist = true) {
|
||||||
if (extension.name) extensions[extension.name] = extension
|
if (extension.name) extensions[extension.name] = extension;
|
||||||
if (persist) {
|
if (persist) {
|
||||||
persistExtensions()
|
persistExtensions();
|
||||||
extension.subscribe('pe-persist', persistExtensions)
|
extension.subscribe("pe-persist", persistExtensions);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -87,15 +77,15 @@ export function addExtension(extension: Extension, persist = true) {
|
|||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
export function persistExtensions() {
|
export function persistExtensions() {
|
||||||
const persistData: Record<string, Extension> = {}
|
const persistData: Record<string, Extension> = {};
|
||||||
for (const name in extensions) {
|
for (const name in extensions) {
|
||||||
persistData[name] = extensions[name]
|
persistData[name] = extensions[name];
|
||||||
}
|
}
|
||||||
writeFileSync(
|
writeFileSync(
|
||||||
ExtensionManager.instance.getExtensionsFile(),
|
ExtensionManager.instance.getExtensionsFile(),
|
||||||
JSON.stringify(persistData),
|
JSON.stringify(persistData),
|
||||||
'utf8'
|
"utf8"
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -106,25 +96,25 @@ export function persistExtensions() {
|
|||||||
* @alias extensionManager.installExtensions
|
* @alias extensionManager.installExtensions
|
||||||
*/
|
*/
|
||||||
export async function installExtensions(extensions: any, store = true) {
|
export async function installExtensions(extensions: any, store = true) {
|
||||||
const installed: Extension[] = []
|
const installed: Extension[] = [];
|
||||||
for (const ext of extensions) {
|
for (const ext of extensions) {
|
||||||
// Set install options and activation based on input type
|
// Set install options and activation based on input type
|
||||||
const isObject = typeof ext === 'object'
|
const isObject = typeof ext === "object";
|
||||||
const spec = isObject ? [ext.specifier, ext] : [ext]
|
const spec = isObject ? [ext.specifier, ext] : [ext];
|
||||||
const activate = isObject ? ext.activate !== false : true
|
const activate = isObject ? ext.activate !== false : true;
|
||||||
|
|
||||||
// Install and possibly activate extension
|
// Install and possibly activate extension
|
||||||
const extension = new Extension(...spec)
|
const extension = new Extension(...spec);
|
||||||
await extension._install()
|
await extension._install();
|
||||||
if (activate) extension.setActive(true)
|
if (activate) extension.setActive(true);
|
||||||
|
|
||||||
// Add extension to store if needed
|
// Add extension to store if needed
|
||||||
if (store) addExtension(extension)
|
if (store) addExtension(extension);
|
||||||
installed.push(extension)
|
installed.push(extension);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return list of all installed extensions
|
// Return list of all installed extensions
|
||||||
return installed
|
return installed;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
7
core/src/node/index.ts
Normal file
7
core/src/node/index.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export * from './extension/index'
|
||||||
|
export * from './extension/extension'
|
||||||
|
export * from './extension/manager'
|
||||||
|
export * from './extension/store'
|
||||||
|
export * from './download'
|
||||||
|
export * from './module'
|
||||||
|
export * from './api'
|
||||||
@ -1,16 +1,14 @@
|
|||||||
import { dispose } from "./../utils/disposable";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manages imported modules.
|
* Manages imported modules.
|
||||||
*/
|
*/
|
||||||
export class ModuleManager {
|
export class ModuleManager {
|
||||||
public requiredModules: Record<string, any> = {};
|
public requiredModules: Record<string, any> = {}
|
||||||
|
|
||||||
public static instance: ModuleManager = new ModuleManager();
|
public static instance: ModuleManager = new ModuleManager()
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
if (ModuleManager.instance) {
|
if (ModuleManager.instance) {
|
||||||
return ModuleManager.instance;
|
return ModuleManager.instance
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -20,14 +18,13 @@ export class ModuleManager {
|
|||||||
* @param {any | undefined} nodule - The module to set, or undefined to clear the module.
|
* @param {any | undefined} nodule - The module to set, or undefined to clear the module.
|
||||||
*/
|
*/
|
||||||
setModule(moduleName: string, nodule: any | undefined) {
|
setModule(moduleName: string, nodule: any | undefined) {
|
||||||
this.requiredModules[moduleName] = nodule;
|
this.requiredModules[moduleName] = nodule
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clears all imported modules.
|
* Clears all imported modules.
|
||||||
*/
|
*/
|
||||||
clearImportedModules() {
|
clearImportedModules() {
|
||||||
dispose(this.requiredModules);
|
this.requiredModules = {}
|
||||||
this.requiredModules = {};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2,7 +2,7 @@
|
|||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"target": "es5",
|
"target": "es5",
|
||||||
"module": "es2015",
|
"module": "ES2020",
|
||||||
"lib": ["es2015", "es2016", "es2017", "dom"],
|
"lib": ["es2015", "es2016", "es2017", "dom"],
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
|
|||||||
@ -1,11 +1,9 @@
|
|||||||
import { app, ipcMain, shell, nativeTheme } from 'electron'
|
import { app, ipcMain, shell, nativeTheme } from 'electron'
|
||||||
import { ModuleManager } from './../managers/module'
|
import { join, basename } from 'path'
|
||||||
import { join } from 'path'
|
|
||||||
import { ExtensionManager } from './../managers/extension'
|
|
||||||
import { WindowManager } from './../managers/window'
|
import { WindowManager } from './../managers/window'
|
||||||
import { userSpacePath } from './../utils/path'
|
import { userSpacePath } from './../utils/path'
|
||||||
import { AppRoute } from '@janhq/core'
|
import { AppRoute } from '@janhq/core'
|
||||||
import { getResourcePath } from './../utils/path'
|
import { ExtensionManager, ModuleManager } from '@janhq/core/node'
|
||||||
|
|
||||||
export function handleAppIPCs() {
|
export function handleAppIPCs() {
|
||||||
/**
|
/**
|
||||||
@ -26,10 +24,6 @@ export function handleAppIPCs() {
|
|||||||
shell.openPath(userSpacePath)
|
shell.openPath(userSpacePath)
|
||||||
})
|
})
|
||||||
|
|
||||||
ipcMain.handle(AppRoute.getResourcePath, async (_event) => {
|
|
||||||
return getResourcePath()
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Opens a URL in the user's default browser.
|
* Opens a URL in the user's default browser.
|
||||||
* @param _event - The IPC event object.
|
* @param _event - The IPC event object.
|
||||||
@ -55,6 +49,13 @@ export function handleAppIPCs() {
|
|||||||
join(...paths)
|
join(...paths)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve basename from given path, respect to the current OS.
|
||||||
|
*/
|
||||||
|
ipcMain.handle(AppRoute.baseName, async (_event, path: string) =>
|
||||||
|
basename(path)
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Relaunches the app in production - reload window in development.
|
* Relaunches the app in production - reload window in development.
|
||||||
* @param _event - The IPC event object.
|
* @param _event - The IPC event object.
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
import { app, ipcMain } from 'electron'
|
import { app, ipcMain } from 'electron'
|
||||||
import { DownloadManager } from './../managers/download'
|
|
||||||
import { resolve, join } from 'path'
|
import { resolve, join } from 'path'
|
||||||
import { WindowManager } from './../managers/window'
|
import { WindowManager } from './../managers/window'
|
||||||
import request from 'request'
|
import request from 'request'
|
||||||
import { createWriteStream, renameSync } from 'fs'
|
import { createWriteStream, renameSync } from 'fs'
|
||||||
import { DownloadEvent, DownloadRoute } from '@janhq/core'
|
import { DownloadEvent, DownloadRoute } from '@janhq/core'
|
||||||
const progress = require('request-progress')
|
const progress = require('request-progress')
|
||||||
|
import { DownloadManager } from '@janhq/core/node'
|
||||||
|
|
||||||
export function handleDownloaderIPCs() {
|
export function handleDownloaderIPCs() {
|
||||||
/**
|
/**
|
||||||
@ -46,6 +46,12 @@ export function handleDownloaderIPCs() {
|
|||||||
*/
|
*/
|
||||||
ipcMain.handle(DownloadRoute.downloadFile, async (_event, url, fileName) => {
|
ipcMain.handle(DownloadRoute.downloadFile, async (_event, url, fileName) => {
|
||||||
const userDataPath = join(app.getPath('home'), 'jan')
|
const userDataPath = join(app.getPath('home'), 'jan')
|
||||||
|
if (
|
||||||
|
typeof fileName === 'string' &&
|
||||||
|
(fileName.includes('file:/') || fileName.includes('file:\\'))
|
||||||
|
) {
|
||||||
|
fileName = fileName.replace('file:/', '').replace('file:\\', '')
|
||||||
|
}
|
||||||
const destination = resolve(userDataPath, fileName)
|
const destination = resolve(userDataPath, fileName)
|
||||||
const rq = request(url)
|
const rq = request(url)
|
||||||
// downloading file to a temp file first
|
// downloading file to a temp file first
|
||||||
|
|||||||
@ -1,15 +1,15 @@
|
|||||||
import { ipcMain, webContents } from 'electron'
|
import { ipcMain, webContents } from 'electron'
|
||||||
import { readdirSync } from 'fs'
|
import { readdirSync } from 'fs'
|
||||||
import { ModuleManager } from './../managers/module'
|
|
||||||
import { join, extname } from 'path'
|
import { join, extname } from 'path'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getActiveExtensions,
|
|
||||||
getAllExtensions,
|
|
||||||
installExtensions,
|
installExtensions,
|
||||||
} from './../extension/store'
|
getExtension,
|
||||||
import { getExtension } from './../extension/store'
|
removeExtension,
|
||||||
import { removeExtension } from './../extension/store'
|
getActiveExtensions,
|
||||||
import Extension from './../extension/extension'
|
ModuleManager
|
||||||
|
} from '@janhq/core/node'
|
||||||
|
|
||||||
import { getResourcePath, userSpacePath } from './../utils/path'
|
import { getResourcePath, userSpacePath } from './../utils/path'
|
||||||
import { ExtensionRoute } from '@janhq/core'
|
import { ExtensionRoute } from '@janhq/core'
|
||||||
|
|
||||||
@ -81,7 +81,7 @@ export function handleExtensionIPCs() {
|
|||||||
ExtensionRoute.updateExtension,
|
ExtensionRoute.updateExtension,
|
||||||
async (e, extensions, reload) => {
|
async (e, extensions, reload) => {
|
||||||
// Update all provided extensions
|
// Update all provided extensions
|
||||||
const updated: Extension[] = []
|
const updated: any[] = []
|
||||||
for (const ext of extensions) {
|
for (const ext of extensions) {
|
||||||
const extension = getExtension(ext)
|
const extension = getExtension(ext)
|
||||||
const res = await extension.update()
|
const res = await extension.update()
|
||||||
|
|||||||
37
electron/handlers/fileManager.ts
Normal file
37
electron/handlers/fileManager.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import { ipcMain } from 'electron'
|
||||||
|
// @ts-ignore
|
||||||
|
import reflect from '@alumna/reflect'
|
||||||
|
|
||||||
|
import { FileManagerRoute } from '@janhq/core'
|
||||||
|
import { userSpacePath, getResourcePath } from './../utils/path'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles file system extensions operations.
|
||||||
|
*/
|
||||||
|
export function handleFileMangerIPCs() {
|
||||||
|
// Handles the 'synceFile' IPC event. This event is triggered to synchronize a file from a source path to a destination path.
|
||||||
|
ipcMain.handle(
|
||||||
|
FileManagerRoute.synceFile,
|
||||||
|
async (_event, src: string, dest: string) => {
|
||||||
|
return reflect({
|
||||||
|
src,
|
||||||
|
dest,
|
||||||
|
recursive: true,
|
||||||
|
delete: false,
|
||||||
|
overwrite: true,
|
||||||
|
errorOnExist: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Handles the 'getUserSpace' IPC event. This event is triggered to get the user space path.
|
||||||
|
ipcMain.handle(
|
||||||
|
FileManagerRoute.getUserSpace,
|
||||||
|
(): Promise<string> => Promise.resolve(userSpacePath)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Handles the 'getResourcePath' IPC event. This event is triggered to get the resource path.
|
||||||
|
ipcMain.handle(FileManagerRoute.getResourcePath, async (_event) => {
|
||||||
|
return getResourcePath()
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -1,238 +1,32 @@
|
|||||||
import { 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'
|
|
||||||
import { FileSystemRoute } from '@janhq/core'
|
|
||||||
const reflect = require('@alumna/reflect')
|
|
||||||
|
|
||||||
|
import { FileSystemRoute } from '@janhq/core'
|
||||||
|
import { userSpacePath } from '../utils/path'
|
||||||
|
import { join } from 'path'
|
||||||
/**
|
/**
|
||||||
* Handles file system operations.
|
* Handles file system operations.
|
||||||
*/
|
*/
|
||||||
export function handleFsIPCs() {
|
export function handleFsIPCs() {
|
||||||
/**
|
const moduleName = 'fs'
|
||||||
* Gets the path to the user data directory.
|
Object.values(FileSystemRoute).forEach((route) => {
|
||||||
* @param event - The event object.
|
ipcMain.handle(route, async (event, ...args) => {
|
||||||
* @returns A promise that resolves with the path to the user data directory.
|
return import(moduleName).then((mdl) =>
|
||||||
*/
|
mdl[route](
|
||||||
ipcMain.handle(
|
...args.map((arg) =>
|
||||||
FileSystemRoute.getUserSpace,
|
typeof arg === 'string' &&
|
||||||
(): Promise<string> => Promise.resolve(userSpacePath)
|
(arg.includes(`file:/`) || arg.includes(`file:\\`))
|
||||||
)
|
? join(
|
||||||
|
userSpacePath,
|
||||||
/**
|
arg
|
||||||
* Checks whether the path is a directory.
|
.replace(`file://`, '')
|
||||||
* @param event - The event object.
|
.replace(`file:/`, '')
|
||||||
* @param path - The path to check.
|
.replace(`file:\\\\`, '')
|
||||||
* @returns A promise that resolves with a boolean indicating whether the path is a directory.
|
.replace(`file:\\`, '')
|
||||||
*/
|
)
|
||||||
ipcMain.handle(
|
: arg
|
||||||
FileSystemRoute.isDirectory,
|
)
|
||||||
(_event, path: string): Promise<boolean> => {
|
)
|
||||||
const fullPath = join(userSpacePath, path)
|
|
||||||
return Promise.resolve(
|
|
||||||
fs.existsSync(fullPath) && fs.lstatSync(fullPath).isDirectory()
|
|
||||||
)
|
)
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reads a file from the user data directory.
|
|
||||||
* @param event - The event object.
|
|
||||||
* @param path - The path of the file to read.
|
|
||||||
* @returns A promise that resolves with the contents of the file.
|
|
||||||
*/
|
|
||||||
ipcMain.handle(
|
|
||||||
FileSystemRoute.readFile,
|
|
||||||
async (event, path: string): Promise<string> => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
fs.readFile(join(userSpacePath, path), 'utf8', (err, data) => {
|
|
||||||
if (err) {
|
|
||||||
reject(err)
|
|
||||||
} else {
|
|
||||||
resolve(data)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks whether a file exists in the user data directory.
|
|
||||||
* @param event - The event object.
|
|
||||||
* @param path - The path of the file to check.
|
|
||||||
* @returns A promise that resolves with a boolean indicating whether the file exists.
|
|
||||||
*/
|
|
||||||
ipcMain.handle(FileSystemRoute.exists, async (_event, path: string) => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const fullPath = join(userSpacePath, path)
|
|
||||||
fs.existsSync(fullPath) ? resolve(true) : resolve(false)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
|
||||||
* Writes data to a file in the user data directory.
|
|
||||||
* @param event - The event object.
|
|
||||||
* @param path - The path of the file to write to.
|
|
||||||
* @param data - The data to write to the file.
|
|
||||||
* @returns A promise that resolves when the file has been written.
|
|
||||||
*/
|
|
||||||
ipcMain.handle(
|
|
||||||
FileSystemRoute.writeFile,
|
|
||||||
async (event, path: string, data: string): Promise<void> => {
|
|
||||||
try {
|
|
||||||
await fs.writeFileSync(join(userSpacePath, path), data, 'utf8')
|
|
||||||
} catch (err) {
|
|
||||||
console.error(`writeFile ${path} result: ${err}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a directory in the user data directory.
|
|
||||||
* @param event - The event object.
|
|
||||||
* @param path - The path of the directory to create.
|
|
||||||
* @returns A promise that resolves when the directory has been created.
|
|
||||||
*/
|
|
||||||
ipcMain.handle(
|
|
||||||
FileSystemRoute.mkdir,
|
|
||||||
async (event, path: string): Promise<void> => {
|
|
||||||
try {
|
|
||||||
fs.mkdirSync(join(userSpacePath, path), { recursive: true })
|
|
||||||
} catch (err) {
|
|
||||||
console.error(`mkdir ${path} result: ${err}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes a directory in the user data directory.
|
|
||||||
* @param event - The event object.
|
|
||||||
* @param path - The path of the directory to remove.
|
|
||||||
* @returns A promise that resolves when the directory is removed successfully.
|
|
||||||
*/
|
|
||||||
ipcMain.handle(
|
|
||||||
FileSystemRoute.rmdir,
|
|
||||||
async (event, path: string): Promise<void> => {
|
|
||||||
try {
|
|
||||||
await fs.rmSync(join(userSpacePath, path), { recursive: true })
|
|
||||||
} catch (err) {
|
|
||||||
console.error(`rmdir ${path} result: ${err}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Lists the files in a directory in the user data directory.
|
|
||||||
* @param event - The event object.
|
|
||||||
* @param path - The path of the directory to list files from.
|
|
||||||
* @returns A promise that resolves with an array of file names.
|
|
||||||
*/
|
|
||||||
ipcMain.handle(
|
|
||||||
FileSystemRoute.listFiles,
|
|
||||||
async (event, path: string): Promise<string[]> => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
fs.readdir(join(userSpacePath, path), (err, files) => {
|
|
||||||
if (err) {
|
|
||||||
reject(err)
|
|
||||||
} else {
|
|
||||||
resolve(files)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Deletes a file from the user data folder.
|
|
||||||
* @param _event - The IPC event object.
|
|
||||||
* @param filePath - The path to the file to delete.
|
|
||||||
* @returns A string indicating the result of the operation.
|
|
||||||
*/
|
|
||||||
ipcMain.handle(FileSystemRoute.deleteFile, async (_event, filePath) => {
|
|
||||||
try {
|
|
||||||
await fs.unlinkSync(join(userSpacePath, filePath))
|
|
||||||
} catch (err) {
|
|
||||||
console.error(`unlink ${filePath} result: ${err}`)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Appends data to a file in the user data directory.
|
|
||||||
* @param event - The event object.
|
|
||||||
* @param path - The path of the file to append to.
|
|
||||||
* @param data - The data to append to the file.
|
|
||||||
* @returns A promise that resolves when the file has been written.
|
|
||||||
*/
|
|
||||||
ipcMain.handle(
|
|
||||||
FileSystemRoute.appendFile,
|
|
||||||
async (_event, path: string, data: string) => {
|
|
||||||
try {
|
|
||||||
await fs.appendFileSync(join(userSpacePath, path), data, 'utf8')
|
|
||||||
} catch (err) {
|
|
||||||
console.error(`appendFile ${path} result: ${err}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
ipcMain.handle(
|
|
||||||
FileSystemRoute.syncFile,
|
|
||||||
async (_event, src: string, dest: string) => {
|
|
||||||
console.debug(`Copying file from ${src} to ${dest}`)
|
|
||||||
|
|
||||||
return reflect({
|
|
||||||
src,
|
|
||||||
dest,
|
|
||||||
recursive: true,
|
|
||||||
delete: false,
|
|
||||||
overwrite: true,
|
|
||||||
errorOnExist: false,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
ipcMain.handle(
|
|
||||||
FileSystemRoute.copyFile,
|
|
||||||
async (_event, src: string, dest: string) => {
|
|
||||||
console.debug(`Copying file from ${src} to ${dest}`)
|
|
||||||
|
|
||||||
return fse.copySync(src, dest, {
|
|
||||||
overwrite: false,
|
|
||||||
recursive: true,
|
|
||||||
errorOnExist: false,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reads a file line by line.
|
|
||||||
* @param event - The event object.
|
|
||||||
* @param path - The path of the file to read.
|
|
||||||
* @returns A promise that resolves with the contents of the file.
|
|
||||||
*/
|
|
||||||
ipcMain.handle(
|
|
||||||
FileSystemRoute.readLineByLine,
|
|
||||||
async (_event, path: string) => {
|
|
||||||
const fullPath = join(userSpacePath, path)
|
|
||||||
|
|
||||||
return new Promise((res, rej) => {
|
|
||||||
try {
|
|
||||||
const readInterface = readline.createInterface({
|
|
||||||
input: fs.createReadStream(fullPath),
|
|
||||||
})
|
|
||||||
const lines: any = []
|
|
||||||
readInterface
|
|
||||||
.on('line', function (line) {
|
|
||||||
lines.push(line)
|
|
||||||
})
|
|
||||||
.on('close', function () {
|
|
||||||
res(lines)
|
|
||||||
})
|
|
||||||
} catch (err) {
|
|
||||||
rej(err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,27 +7,34 @@ import { createUserSpace } from './utils/path'
|
|||||||
* Managers
|
* Managers
|
||||||
**/
|
**/
|
||||||
import { WindowManager } from './managers/window'
|
import { WindowManager } from './managers/window'
|
||||||
import { ModuleManager } from './managers/module'
|
import { ExtensionManager, ModuleManager } from '@janhq/core/node'
|
||||||
import { ExtensionManager } from './managers/extension'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* IPC Handlers
|
* IPC Handlers
|
||||||
**/
|
**/
|
||||||
import { handleDownloaderIPCs } from './handlers/download'
|
import { handleDownloaderIPCs } from './handlers/download'
|
||||||
import { handleExtensionIPCs } from './handlers/extension'
|
import { handleExtensionIPCs } from './handlers/extension'
|
||||||
|
import { handleFileMangerIPCs } from './handlers/fileManager'
|
||||||
import { handleAppIPCs } from './handlers/app'
|
import { handleAppIPCs } from './handlers/app'
|
||||||
import { handleAppUpdates } from './handlers/update'
|
import { handleAppUpdates } from './handlers/update'
|
||||||
import { handleFsIPCs } from './handlers/fs'
|
import { handleFsIPCs } from './handlers/fs'
|
||||||
|
import { migrateExtensions } from './utils/migration'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Server
|
||||||
|
*/
|
||||||
|
import { startServer } from '@janhq/server'
|
||||||
|
|
||||||
app
|
app
|
||||||
.whenReady()
|
.whenReady()
|
||||||
.then(createUserSpace)
|
.then(createUserSpace)
|
||||||
.then(ExtensionManager.instance.migrateExtensions)
|
.then(migrateExtensions)
|
||||||
.then(ExtensionManager.instance.setupExtensions)
|
.then(ExtensionManager.instance.setupExtensions)
|
||||||
.then(setupMenu)
|
.then(setupMenu)
|
||||||
.then(handleIPCs)
|
.then(handleIPCs)
|
||||||
.then(handleAppUpdates)
|
.then(handleAppUpdates)
|
||||||
.then(createMainWindow)
|
.then(createMainWindow)
|
||||||
|
.then(startServer)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
app.on('activate', () => {
|
app.on('activate', () => {
|
||||||
if (!BrowserWindow.getAllWindows().length) {
|
if (!BrowserWindow.getAllWindows().length) {
|
||||||
@ -80,4 +87,5 @@ function handleIPCs() {
|
|||||||
handleDownloaderIPCs()
|
handleDownloaderIPCs()
|
||||||
handleExtensionIPCs()
|
handleExtensionIPCs()
|
||||||
handleAppIPCs()
|
handleAppIPCs()
|
||||||
|
handleFileMangerIPCs()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,85 +0,0 @@
|
|||||||
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')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -72,15 +72,20 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@alumna/reflect": "^1.1.3",
|
"@alumna/reflect": "^1.1.3",
|
||||||
"@janhq/core": "link:./core",
|
"@janhq/core": "link:./core",
|
||||||
|
"@janhq/server": "link:./server",
|
||||||
"@npmcli/arborist": "^7.1.0",
|
"@npmcli/arborist": "^7.1.0",
|
||||||
"@types/request": "^2.48.12",
|
"@types/request": "^2.48.12",
|
||||||
"@uiball/loaders": "^1.3.0",
|
"@uiball/loaders": "^1.3.0",
|
||||||
"electron-store": "^8.1.0",
|
"electron-store": "^8.1.0",
|
||||||
"electron-updater": "^6.1.7",
|
"electron-updater": "^6.1.7",
|
||||||
"fs-extra": "^11.2.0",
|
"fs-extra": "^11.2.0",
|
||||||
|
"node-fetch": "2",
|
||||||
"pacote": "^17.0.4",
|
"pacote": "^17.0.4",
|
||||||
"request": "^2.88.2",
|
"request": "^2.88.2",
|
||||||
"request-progress": "^3.0.0",
|
"request-progress": "^3.0.0",
|
||||||
|
"rimraf": "^5.0.5",
|
||||||
|
"typescript": "^5.3.3",
|
||||||
|
"ulid": "^2.3.0",
|
||||||
"use-debounce": "^9.0.4"
|
"use-debounce": "^9.0.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@ -15,6 +15,9 @@
|
|||||||
"paths": { "*": ["node_modules/*"] },
|
"paths": { "*": ["node_modules/*"] },
|
||||||
"typeRoots": ["node_modules/@types"]
|
"typeRoots": ["node_modules/@types"]
|
||||||
},
|
},
|
||||||
|
"ts-node": {
|
||||||
|
"esm": true
|
||||||
|
},
|
||||||
"include": ["./**/*.ts"],
|
"include": ["./**/*.ts"],
|
||||||
"exclude": ["core", "build", "dist", "tests", "node_modules"]
|
"exclude": ["core", "build", "dist", "tests", "node_modules"]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
const { app, Menu, dialog } = require("electron");
|
import { app, Menu, dialog, shell } from "electron";
|
||||||
const isMac = process.platform === "darwin";
|
const isMac = process.platform === "darwin";
|
||||||
const { autoUpdater } = require("electron-updater");
|
const { autoUpdater } = require("electron-updater");
|
||||||
import { compareSemanticVersions } from "./versionDiff";
|
import { compareSemanticVersions } from "./versionDiff";
|
||||||
@ -97,7 +97,6 @@ const template: (Electron.MenuItemConstructorOptions | Electron.MenuItem)[] = [
|
|||||||
{
|
{
|
||||||
label: "Learn More",
|
label: "Learn More",
|
||||||
click: async () => {
|
click: async () => {
|
||||||
const { shell } = require("electron");
|
|
||||||
await shell.openExternal("https://jan.ai/");
|
await shell.openExternal("https://jan.ai/");
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
30
electron/utils/migration.ts
Normal file
30
electron/utils/migration.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { app } from 'electron'
|
||||||
|
import { join } from 'path'
|
||||||
|
|
||||||
|
import { rmdir } from 'fs'
|
||||||
|
import Store from 'electron-store'
|
||||||
|
import { userSpacePath } from './path'
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
export function 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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -3,17 +3,18 @@ import { AssistantExtension } from "@janhq/core";
|
|||||||
import { join } from "path";
|
import { join } from "path";
|
||||||
|
|
||||||
export default class JanAssistantExtension implements AssistantExtension {
|
export default class JanAssistantExtension implements AssistantExtension {
|
||||||
private static readonly _homeDir = "assistants";
|
private static readonly _homeDir = "file://assistants";
|
||||||
|
|
||||||
type(): ExtensionType {
|
type(): ExtensionType {
|
||||||
return ExtensionType.Assistant;
|
return ExtensionType.Assistant;
|
||||||
}
|
}
|
||||||
|
|
||||||
onLoad(): void {
|
async onLoad() {
|
||||||
// making the assistant directory
|
// making the assistant directory
|
||||||
fs.mkdir(JanAssistantExtension._homeDir).then(() => {
|
if (!(await fs.existsSync(JanAssistantExtension._homeDir)))
|
||||||
this.createJanAssistant();
|
fs.mkdirSync(JanAssistantExtension._homeDir).then(() => {
|
||||||
});
|
this.createJanAssistant();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -23,12 +24,12 @@ export default class JanAssistantExtension implements AssistantExtension {
|
|||||||
|
|
||||||
async createAssistant(assistant: Assistant): Promise<void> {
|
async createAssistant(assistant: Assistant): Promise<void> {
|
||||||
const assistantDir = join(JanAssistantExtension._homeDir, assistant.id);
|
const assistantDir = join(JanAssistantExtension._homeDir, assistant.id);
|
||||||
await fs.mkdir(assistantDir);
|
if (!(await fs.existsSync(assistantDir))) await fs.mkdirSync(assistantDir);
|
||||||
|
|
||||||
// store the assistant metadata json
|
// store the assistant metadata json
|
||||||
const assistantMetadataPath = join(assistantDir, "assistant.json");
|
const assistantMetadataPath = join(assistantDir, "assistant.json");
|
||||||
try {
|
try {
|
||||||
await fs.writeFile(
|
await fs.writeFileSync(
|
||||||
assistantMetadataPath,
|
assistantMetadataPath,
|
||||||
JSON.stringify(assistant, null, 2)
|
JSON.stringify(assistant, null, 2)
|
||||||
);
|
);
|
||||||
@ -41,18 +42,14 @@ export default class JanAssistantExtension implements AssistantExtension {
|
|||||||
// get all the assistant directories
|
// get all the assistant directories
|
||||||
// get all the assistant metadata json
|
// get all the assistant metadata json
|
||||||
const results: Assistant[] = [];
|
const results: Assistant[] = [];
|
||||||
const allFileName: string[] = await fs.listFiles(
|
const allFileName: string[] = await fs.readdirSync(
|
||||||
JanAssistantExtension._homeDir
|
JanAssistantExtension._homeDir
|
||||||
);
|
);
|
||||||
for (const fileName of allFileName) {
|
for (const fileName of allFileName) {
|
||||||
const filePath = join(JanAssistantExtension._homeDir, fileName);
|
const filePath = join(JanAssistantExtension._homeDir, fileName);
|
||||||
const isDirectory = await fs.isDirectory(filePath);
|
|
||||||
if (!isDirectory) {
|
|
||||||
// if not a directory, ignore
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const jsonFiles: string[] = (await fs.listFiles(filePath)).filter(
|
if (filePath.includes(".DS_Store")) continue;
|
||||||
|
const jsonFiles: string[] = (await fs.readdirSync(filePath)).filter(
|
||||||
(file: string) => file === "assistant.json"
|
(file: string) => file === "assistant.json"
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -61,9 +58,12 @@ export default class JanAssistantExtension implements AssistantExtension {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const assistant: Assistant = JSON.parse(
|
const content = await fs.readFileSync(
|
||||||
await fs.readFile(join(filePath, jsonFiles[0]))
|
join(filePath, jsonFiles[0]),
|
||||||
|
"utf-8"
|
||||||
);
|
);
|
||||||
|
const assistant: Assistant =
|
||||||
|
typeof content === "object" ? content : JSON.parse(content);
|
||||||
|
|
||||||
results.push(assistant);
|
results.push(assistant);
|
||||||
}
|
}
|
||||||
@ -78,7 +78,7 @@ export default class JanAssistantExtension implements AssistantExtension {
|
|||||||
|
|
||||||
// remove the directory
|
// remove the directory
|
||||||
const assistantDir = join(JanAssistantExtension._homeDir, assistant.id);
|
const assistantDir = join(JanAssistantExtension._homeDir, assistant.id);
|
||||||
await fs.rmdir(assistantDir);
|
await fs.rmdirSync(assistantDir);
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import { Thread, ThreadMessage } from '@janhq/core'
|
|||||||
export default class JSONConversationalExtension
|
export default class JSONConversationalExtension
|
||||||
implements ConversationalExtension
|
implements ConversationalExtension
|
||||||
{
|
{
|
||||||
private static readonly _homeDir = 'threads'
|
private static readonly _homeDir = 'file://threads'
|
||||||
private static readonly _threadInfoFileName = 'thread.json'
|
private static readonly _threadInfoFileName = 'thread.json'
|
||||||
private static readonly _threadMessagesFileName = 'messages.jsonl'
|
private static readonly _threadMessagesFileName = 'messages.jsonl'
|
||||||
|
|
||||||
@ -23,8 +23,9 @@ export default class JSONConversationalExtension
|
|||||||
/**
|
/**
|
||||||
* Called when the extension is loaded.
|
* Called when the extension is loaded.
|
||||||
*/
|
*/
|
||||||
onLoad() {
|
async onLoad() {
|
||||||
fs.mkdir(JSONConversationalExtension._homeDir)
|
if (!(await fs.existsSync(JSONConversationalExtension._homeDir)))
|
||||||
|
await fs.mkdirSync(JSONConversationalExtension._homeDir)
|
||||||
console.debug('JSONConversationalExtension loaded')
|
console.debug('JSONConversationalExtension loaded')
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -47,7 +48,9 @@ export default class JSONConversationalExtension
|
|||||||
const convos = promiseResults
|
const convos = promiseResults
|
||||||
.map((result) => {
|
.map((result) => {
|
||||||
if (result.status === 'fulfilled') {
|
if (result.status === 'fulfilled') {
|
||||||
return JSON.parse(result.value) as Thread
|
return typeof result.value === 'object'
|
||||||
|
? result.value
|
||||||
|
: JSON.parse(result.value)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.filter((convo) => convo != null)
|
.filter((convo) => convo != null)
|
||||||
@ -76,8 +79,11 @@ export default class JSONConversationalExtension
|
|||||||
threadDirPath,
|
threadDirPath,
|
||||||
JSONConversationalExtension._threadInfoFileName,
|
JSONConversationalExtension._threadInfoFileName,
|
||||||
])
|
])
|
||||||
await fs.mkdir(threadDirPath)
|
if (!(await fs.existsSync(threadDirPath))) {
|
||||||
await fs.writeFile(threadJsonPath, JSON.stringify(thread, null, 2))
|
await fs.mkdirSync(threadDirPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
await fs.writeFileSync(threadJsonPath, JSON.stringify(thread))
|
||||||
Promise.resolve()
|
Promise.resolve()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Promise.reject(err)
|
Promise.reject(err)
|
||||||
@ -89,8 +95,9 @@ export default class JSONConversationalExtension
|
|||||||
* @param threadId The ID of the thread to delete.
|
* @param threadId The ID of the thread to delete.
|
||||||
*/
|
*/
|
||||||
async deleteThread(threadId: string): Promise<void> {
|
async deleteThread(threadId: string): Promise<void> {
|
||||||
return fs.rmdir(
|
return fs.rmdirSync(
|
||||||
await joinPath([JSONConversationalExtension._homeDir, `${threadId}`])
|
await joinPath([JSONConversationalExtension._homeDir, `${threadId}`]),
|
||||||
|
{ recursive: true }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -104,8 +111,9 @@ export default class JSONConversationalExtension
|
|||||||
threadDirPath,
|
threadDirPath,
|
||||||
JSONConversationalExtension._threadMessagesFileName,
|
JSONConversationalExtension._threadMessagesFileName,
|
||||||
])
|
])
|
||||||
await fs.mkdir(threadDirPath)
|
if (!(await fs.existsSync(threadDirPath)))
|
||||||
await fs.appendFile(threadMessagePath, JSON.stringify(message) + '\n')
|
await fs.mkdirSync(threadDirPath)
|
||||||
|
await fs.appendFileSync(threadMessagePath, JSON.stringify(message) + '\n')
|
||||||
Promise.resolve()
|
Promise.resolve()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Promise.reject(err)
|
Promise.reject(err)
|
||||||
@ -125,8 +133,9 @@ export default class JSONConversationalExtension
|
|||||||
threadDirPath,
|
threadDirPath,
|
||||||
JSONConversationalExtension._threadMessagesFileName,
|
JSONConversationalExtension._threadMessagesFileName,
|
||||||
])
|
])
|
||||||
await fs.mkdir(threadDirPath)
|
if (!(await fs.existsSync(threadDirPath)))
|
||||||
await fs.writeFile(
|
await fs.mkdirSync(threadDirPath)
|
||||||
|
await fs.writeFileSync(
|
||||||
threadMessagePath,
|
threadMessagePath,
|
||||||
messages.map((msg) => JSON.stringify(msg)).join('\n') +
|
messages.map((msg) => JSON.stringify(msg)).join('\n') +
|
||||||
(messages.length ? '\n' : '')
|
(messages.length ? '\n' : '')
|
||||||
@ -143,12 +152,13 @@ export default class JSONConversationalExtension
|
|||||||
* @returns data of the thread
|
* @returns data of the thread
|
||||||
*/
|
*/
|
||||||
private async readThread(threadDirName: string): Promise<any> {
|
private async readThread(threadDirName: string): Promise<any> {
|
||||||
return fs.readFile(
|
return fs.readFileSync(
|
||||||
await joinPath([
|
await joinPath([
|
||||||
JSONConversationalExtension._homeDir,
|
JSONConversationalExtension._homeDir,
|
||||||
threadDirName,
|
threadDirName,
|
||||||
JSONConversationalExtension._threadInfoFileName,
|
JSONConversationalExtension._threadInfoFileName,
|
||||||
])
|
]),
|
||||||
|
'utf-8'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -157,23 +167,19 @@ export default class JSONConversationalExtension
|
|||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
private async getValidThreadDirs(): Promise<string[]> {
|
private async getValidThreadDirs(): Promise<string[]> {
|
||||||
const fileInsideThread: string[] = await fs.listFiles(
|
const fileInsideThread: string[] = await fs.readdirSync(
|
||||||
JSONConversationalExtension._homeDir
|
JSONConversationalExtension._homeDir
|
||||||
)
|
)
|
||||||
|
|
||||||
const threadDirs: string[] = []
|
const threadDirs: string[] = []
|
||||||
for (let i = 0; i < fileInsideThread.length; i++) {
|
for (let i = 0; i < fileInsideThread.length; i++) {
|
||||||
|
if (fileInsideThread[i].includes('.DS_Store')) continue
|
||||||
const path = await joinPath([
|
const path = await joinPath([
|
||||||
JSONConversationalExtension._homeDir,
|
JSONConversationalExtension._homeDir,
|
||||||
fileInsideThread[i],
|
fileInsideThread[i],
|
||||||
])
|
])
|
||||||
const isDirectory = await fs.isDirectory(path)
|
|
||||||
if (!isDirectory) {
|
|
||||||
console.debug(`Ignore ${path} because it is not a directory`)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
const isHavingThreadInfo = (await fs.listFiles(path)).includes(
|
const isHavingThreadInfo = (await fs.readdirSync(path)).includes(
|
||||||
JSONConversationalExtension._threadInfoFileName
|
JSONConversationalExtension._threadInfoFileName
|
||||||
)
|
)
|
||||||
if (!isHavingThreadInfo) {
|
if (!isHavingThreadInfo) {
|
||||||
@ -192,12 +198,8 @@ export default class JSONConversationalExtension
|
|||||||
JSONConversationalExtension._homeDir,
|
JSONConversationalExtension._homeDir,
|
||||||
threadId,
|
threadId,
|
||||||
])
|
])
|
||||||
const isDir = await fs.isDirectory(threadDirPath)
|
|
||||||
if (!isDir) {
|
|
||||||
throw Error(`${threadDirPath} is not directory`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const files: string[] = await fs.listFiles(threadDirPath)
|
const files: string[] = await fs.readdirSync(threadDirPath)
|
||||||
if (
|
if (
|
||||||
!files.includes(JSONConversationalExtension._threadMessagesFileName)
|
!files.includes(JSONConversationalExtension._threadMessagesFileName)
|
||||||
) {
|
) {
|
||||||
@ -209,7 +211,14 @@ export default class JSONConversationalExtension
|
|||||||
JSONConversationalExtension._threadMessagesFileName,
|
JSONConversationalExtension._threadMessagesFileName,
|
||||||
])
|
])
|
||||||
|
|
||||||
const result = await fs.readLineByLine(messageFilePath)
|
const result = await fs
|
||||||
|
.readFileSync(messageFilePath, 'utf-8')
|
||||||
|
.then((content) =>
|
||||||
|
content
|
||||||
|
.toString()
|
||||||
|
.split('\n')
|
||||||
|
.filter((line) => line !== '')
|
||||||
|
)
|
||||||
|
|
||||||
const messages: ThreadMessage[] = []
|
const messages: ThreadMessage[] = []
|
||||||
result.forEach((line: string) => {
|
result.forEach((line: string) => {
|
||||||
|
|||||||
@ -17,9 +17,9 @@ import {
|
|||||||
ThreadMessage,
|
ThreadMessage,
|
||||||
events,
|
events,
|
||||||
executeOnMain,
|
executeOnMain,
|
||||||
getUserSpace,
|
|
||||||
fs,
|
fs,
|
||||||
Model,
|
Model,
|
||||||
|
joinPath,
|
||||||
} from "@janhq/core";
|
} from "@janhq/core";
|
||||||
import { InferenceExtension } from "@janhq/core";
|
import { InferenceExtension } from "@janhq/core";
|
||||||
import { requestInference } from "./helpers/sse";
|
import { requestInference } from "./helpers/sse";
|
||||||
@ -32,7 +32,8 @@ import { join } from "path";
|
|||||||
* It also subscribes to events emitted by the @janhq/core package and handles new message requests.
|
* It also subscribes to events emitted by the @janhq/core package and handles new message requests.
|
||||||
*/
|
*/
|
||||||
export default class JanInferenceNitroExtension implements InferenceExtension {
|
export default class JanInferenceNitroExtension implements InferenceExtension {
|
||||||
private static readonly _homeDir = "engines";
|
private static readonly _homeDir = "file://engines";
|
||||||
|
private static readonly _settingsDir = "file://settings";
|
||||||
private static readonly _engineMetadataFileName = "nitro.json";
|
private static readonly _engineMetadataFileName = "nitro.json";
|
||||||
|
|
||||||
private static _currentModel: Model;
|
private static _currentModel: Model;
|
||||||
@ -58,8 +59,15 @@ export default class JanInferenceNitroExtension implements InferenceExtension {
|
|||||||
/**
|
/**
|
||||||
* Subscribes to events emitted by the @janhq/core package.
|
* Subscribes to events emitted by the @janhq/core package.
|
||||||
*/
|
*/
|
||||||
onLoad(): void {
|
async onLoad() {
|
||||||
fs.mkdir(JanInferenceNitroExtension._homeDir);
|
if (!(await fs.existsSync(JanInferenceNitroExtension._homeDir))) {
|
||||||
|
await fs
|
||||||
|
.mkdirSync(JanInferenceNitroExtension._homeDir)
|
||||||
|
.catch((err) => console.debug(err));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(await fs.existsSync(JanInferenceNitroExtension._settingsDir)))
|
||||||
|
await fs.mkdirSync(JanInferenceNitroExtension._settingsDir);
|
||||||
this.writeDefaultEngineSettings();
|
this.writeDefaultEngineSettings();
|
||||||
|
|
||||||
// Events subscription
|
// Events subscription
|
||||||
@ -78,6 +86,9 @@ export default class JanInferenceNitroExtension implements InferenceExtension {
|
|||||||
events.on(EventName.OnInferenceStopped, () => {
|
events.on(EventName.OnInferenceStopped, () => {
|
||||||
JanInferenceNitroExtension.handleInferenceStopped(this);
|
JanInferenceNitroExtension.handleInferenceStopped(this);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Attempt to fetch nvidia info
|
||||||
|
await executeOnMain(MODULE, "updateNvidiaInfo", {});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -91,12 +102,12 @@ export default class JanInferenceNitroExtension implements InferenceExtension {
|
|||||||
JanInferenceNitroExtension._homeDir,
|
JanInferenceNitroExtension._homeDir,
|
||||||
JanInferenceNitroExtension._engineMetadataFileName
|
JanInferenceNitroExtension._engineMetadataFileName
|
||||||
);
|
);
|
||||||
if (await fs.exists(engineFile)) {
|
if (await fs.existsSync(engineFile)) {
|
||||||
JanInferenceNitroExtension._engineSettings = JSON.parse(
|
const engine = await fs.readFileSync(engineFile, "utf-8");
|
||||||
await fs.readFile(engineFile)
|
JanInferenceNitroExtension._engineSettings =
|
||||||
);
|
typeof engine === "object" ? engine : JSON.parse(engine);
|
||||||
} else {
|
} else {
|
||||||
await fs.writeFile(
|
await fs.writeFileSync(
|
||||||
engineFile,
|
engineFile,
|
||||||
JSON.stringify(JanInferenceNitroExtension._engineSettings, null, 2)
|
JSON.stringify(JanInferenceNitroExtension._engineSettings, null, 2)
|
||||||
);
|
);
|
||||||
@ -110,8 +121,7 @@ export default class JanInferenceNitroExtension implements InferenceExtension {
|
|||||||
if (model.engine !== "nitro") {
|
if (model.engine !== "nitro") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const userSpacePath = await getUserSpace();
|
const modelFullPath = await joinPath(["models", model.id]);
|
||||||
const modelFullPath = join(userSpacePath, "models", model.id);
|
|
||||||
|
|
||||||
const nitroInitResult = await executeOnMain(MODULE, "initModel", {
|
const nitroInitResult = await executeOnMain(MODULE, "initModel", {
|
||||||
modelFullPath: modelFullPath,
|
modelFullPath: modelFullPath,
|
||||||
|
|||||||
@ -1,9 +1,11 @@
|
|||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
|
const fsPromises = fs.promises;
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
const { spawn } = require("child_process");
|
const { exec, spawn } = require("child_process");
|
||||||
const tcpPortUsed = require("tcp-port-used");
|
const tcpPortUsed = require("tcp-port-used");
|
||||||
const fetchRetry = require("fetch-retry")(global.fetch);
|
const fetchRetry = require("fetch-retry")(global.fetch);
|
||||||
const si = require("systeminformation");
|
const si = require("systeminformation");
|
||||||
|
const { readFileSync, writeFileSync, existsSync } = require("fs");
|
||||||
|
|
||||||
// The PORT to use for the Nitro subprocess
|
// The PORT to use for the Nitro subprocess
|
||||||
const PORT = 3928;
|
const PORT = 3928;
|
||||||
@ -14,6 +16,27 @@ const NITRO_HTTP_UNLOAD_MODEL_URL = `${NITRO_HTTP_SERVER_URL}/inferences/llamacp
|
|||||||
const NITRO_HTTP_VALIDATE_MODEL_URL = `${NITRO_HTTP_SERVER_URL}/inferences/llamacpp/modelstatus`;
|
const NITRO_HTTP_VALIDATE_MODEL_URL = `${NITRO_HTTP_SERVER_URL}/inferences/llamacpp/modelstatus`;
|
||||||
const NITRO_HTTP_KILL_URL = `${NITRO_HTTP_SERVER_URL}/processmanager/destroy`;
|
const NITRO_HTTP_KILL_URL = `${NITRO_HTTP_SERVER_URL}/processmanager/destroy`;
|
||||||
const SUPPORTED_MODEL_FORMAT = ".gguf";
|
const SUPPORTED_MODEL_FORMAT = ".gguf";
|
||||||
|
const NVIDIA_INFO_FILE = path.join(
|
||||||
|
require("os").homedir(),
|
||||||
|
"jan",
|
||||||
|
"settings",
|
||||||
|
"settings.json"
|
||||||
|
);
|
||||||
|
|
||||||
|
const DEFALT_SETTINGS = {
|
||||||
|
"notify": true,
|
||||||
|
"run_mode": "cpu",
|
||||||
|
"nvidia_driver": {
|
||||||
|
"exist": false,
|
||||||
|
"version": ""
|
||||||
|
},
|
||||||
|
"cuda": {
|
||||||
|
"exist": false,
|
||||||
|
"version": ""
|
||||||
|
},
|
||||||
|
"gpus": [],
|
||||||
|
"gpu_highest_vram": ""
|
||||||
|
}
|
||||||
|
|
||||||
// The subprocess instance for Nitro
|
// The subprocess instance for Nitro
|
||||||
let subprocess = undefined;
|
let subprocess = undefined;
|
||||||
@ -29,6 +52,125 @@ function stopModel(): Promise<void> {
|
|||||||
return killSubprocess();
|
return killSubprocess();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate nvidia and cuda for linux and windows
|
||||||
|
*/
|
||||||
|
async function updateNvidiaDriverInfo(): Promise<void> {
|
||||||
|
exec(
|
||||||
|
"nvidia-smi --query-gpu=driver_version --format=csv,noheader",
|
||||||
|
(error, stdout) => {
|
||||||
|
let data;
|
||||||
|
try {
|
||||||
|
data = JSON.parse(readFileSync(NVIDIA_INFO_FILE, "utf8"));
|
||||||
|
} catch (error) {
|
||||||
|
data = DEFALT_SETTINGS;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!error) {
|
||||||
|
const firstLine = stdout.split("\n")[0].trim();
|
||||||
|
data["nvidia_driver"].exist = true;
|
||||||
|
data["nvidia_driver"].version = firstLine;
|
||||||
|
} else {
|
||||||
|
data["nvidia_driver"].exist = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
writeFileSync(NVIDIA_INFO_FILE, JSON.stringify(data, null, 2));
|
||||||
|
Promise.resolve();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkFileExistenceInPaths(file: string, paths: string[]): boolean {
|
||||||
|
return paths.some((p) => existsSync(path.join(p, file)));
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateCudaExistence() {
|
||||||
|
let files: string[];
|
||||||
|
let paths: string[];
|
||||||
|
|
||||||
|
if (process.platform === "win32") {
|
||||||
|
files = ["cublas64_12.dll", "cudart64_12.dll", "cublasLt64_12.dll"];
|
||||||
|
paths = process.env.PATH ? process.env.PATH.split(path.delimiter) : [];
|
||||||
|
const nitro_cuda_path = path.join(__dirname, "bin", "win-cuda");
|
||||||
|
paths.push(nitro_cuda_path);
|
||||||
|
} else {
|
||||||
|
files = ["libcudart.so.12", "libcublas.so.12", "libcublasLt.so.12"];
|
||||||
|
paths = process.env.LD_LIBRARY_PATH
|
||||||
|
? process.env.LD_LIBRARY_PATH.split(path.delimiter)
|
||||||
|
: [];
|
||||||
|
const nitro_cuda_path = path.join(__dirname, "bin", "linux-cuda");
|
||||||
|
paths.push(nitro_cuda_path);
|
||||||
|
paths.push("/usr/lib/x86_64-linux-gnu/");
|
||||||
|
}
|
||||||
|
|
||||||
|
let cudaExists = files.every(
|
||||||
|
(file) => existsSync(file) || checkFileExistenceInPaths(file, paths)
|
||||||
|
);
|
||||||
|
|
||||||
|
let data;
|
||||||
|
try {
|
||||||
|
data = JSON.parse(readFileSync(NVIDIA_INFO_FILE, "utf8"));
|
||||||
|
} catch (error) {
|
||||||
|
data = DEFALT_SETTINGS;
|
||||||
|
}
|
||||||
|
|
||||||
|
data["cuda"].exist = cudaExists;
|
||||||
|
if (cudaExists) {
|
||||||
|
data.run_mode = "gpu";
|
||||||
|
}
|
||||||
|
writeFileSync(NVIDIA_INFO_FILE, JSON.stringify(data, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateGpuInfo(): Promise<void> {
|
||||||
|
exec(
|
||||||
|
"nvidia-smi --query-gpu=index,memory.total --format=csv,noheader,nounits",
|
||||||
|
(error, stdout) => {
|
||||||
|
let data;
|
||||||
|
try {
|
||||||
|
data = JSON.parse(readFileSync(NVIDIA_INFO_FILE, "utf8"));
|
||||||
|
} catch (error) {
|
||||||
|
data = DEFALT_SETTINGS;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!error) {
|
||||||
|
// Get GPU info and gpu has higher memory first
|
||||||
|
let highestVram = 0;
|
||||||
|
let highestVramId = "0";
|
||||||
|
let gpus = stdout
|
||||||
|
.trim()
|
||||||
|
.split("\n")
|
||||||
|
.map((line) => {
|
||||||
|
let [id, vram] = line.split(", ");
|
||||||
|
vram = vram.replace(/\r/g, "");
|
||||||
|
if (parseFloat(vram) > highestVram) {
|
||||||
|
highestVram = parseFloat(vram);
|
||||||
|
highestVramId = id;
|
||||||
|
}
|
||||||
|
return { id, vram };
|
||||||
|
});
|
||||||
|
|
||||||
|
data["gpus"] = gpus;
|
||||||
|
data["gpu_highest_vram"] = highestVramId;
|
||||||
|
} else {
|
||||||
|
data["gpus"] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
writeFileSync(NVIDIA_INFO_FILE, JSON.stringify(data, null, 2));
|
||||||
|
Promise.resolve();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateNvidiaInfo() {
|
||||||
|
if (process.platform !== "darwin") {
|
||||||
|
await Promise.all([
|
||||||
|
updateNvidiaDriverInfo(),
|
||||||
|
updateCudaExistence(),
|
||||||
|
updateGpuInfo(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes a Nitro subprocess to load a machine learning model.
|
* Initializes a Nitro subprocess to load a machine learning model.
|
||||||
* @param wrapper - The model wrapper.
|
* @param wrapper - The model wrapper.
|
||||||
@ -38,6 +180,10 @@ function stopModel(): Promise<void> {
|
|||||||
*/
|
*/
|
||||||
async function initModel(wrapper: any): Promise<ModelOperationResponse> {
|
async function initModel(wrapper: any): Promise<ModelOperationResponse> {
|
||||||
currentModelFile = wrapper.modelFullPath;
|
currentModelFile = wrapper.modelFullPath;
|
||||||
|
const janRoot = path.join(require("os").homedir(), "jan");
|
||||||
|
if (!currentModelFile.includes(janRoot)) {
|
||||||
|
currentModelFile = path.join(janRoot, currentModelFile);
|
||||||
|
}
|
||||||
const files: string[] = fs.readdirSync(currentModelFile);
|
const files: string[] = fs.readdirSync(currentModelFile);
|
||||||
|
|
||||||
// Look for GGUF model file
|
// Look for GGUF model file
|
||||||
@ -218,14 +364,26 @@ async function killSubprocess(): Promise<void> {
|
|||||||
* Using child-process to spawn the process
|
* Using child-process to spawn the process
|
||||||
* Should run exactly platform specified Nitro binary version
|
* Should run exactly platform specified Nitro binary version
|
||||||
*/
|
*/
|
||||||
|
/**
|
||||||
|
* Spawns a Nitro subprocess.
|
||||||
|
* @param nitroResourceProbe - The Nitro resource probe.
|
||||||
|
* @returns A promise that resolves when the Nitro subprocess is started.
|
||||||
|
*/
|
||||||
function spawnNitroProcess(nitroResourceProbe: any): Promise<any> {
|
function spawnNitroProcess(nitroResourceProbe: any): Promise<any> {
|
||||||
console.debug("Starting Nitro subprocess...");
|
console.debug("Starting Nitro subprocess...");
|
||||||
return new Promise(async (resolve, reject) => {
|
return new Promise(async (resolve, reject) => {
|
||||||
let binaryFolder = path.join(__dirname, "bin"); // Current directory by default
|
let binaryFolder = path.join(__dirname, "bin"); // Current directory by default
|
||||||
|
let cudaVisibleDevices = "";
|
||||||
let binaryName;
|
let binaryName;
|
||||||
|
|
||||||
if (process.platform === "win32") {
|
if (process.platform === "win32") {
|
||||||
binaryName = "win-start.bat";
|
let nvida_info = JSON.parse(readFileSync(NVIDIA_INFO_FILE, "utf8"));
|
||||||
|
if (nvida_info["run_mode"] === "cpu") {
|
||||||
|
binaryFolder = path.join(binaryFolder, "win-cpu");
|
||||||
|
} else {
|
||||||
|
binaryFolder = path.join(binaryFolder, "win-cuda");
|
||||||
|
cudaVisibleDevices = nvida_info["gpu_highest_vram"];
|
||||||
|
}
|
||||||
|
binaryName = "nitro.exe";
|
||||||
} else if (process.platform === "darwin") {
|
} else if (process.platform === "darwin") {
|
||||||
if (process.arch === "arm64") {
|
if (process.arch === "arm64") {
|
||||||
binaryFolder = path.join(binaryFolder, "mac-arm64");
|
binaryFolder = path.join(binaryFolder, "mac-arm64");
|
||||||
@ -234,13 +392,24 @@ function spawnNitroProcess(nitroResourceProbe: any): Promise<any> {
|
|||||||
}
|
}
|
||||||
binaryName = "nitro";
|
binaryName = "nitro";
|
||||||
} else {
|
} else {
|
||||||
binaryName = "linux-start.sh";
|
let nvida_info = JSON.parse(readFileSync(NVIDIA_INFO_FILE, "utf8"));
|
||||||
|
if (nvida_info["run_mode"] === "cpu") {
|
||||||
|
binaryFolder = path.join(binaryFolder, "win-cpu");
|
||||||
|
} else {
|
||||||
|
binaryFolder = path.join(binaryFolder, "win-cuda");
|
||||||
|
cudaVisibleDevices = nvida_info["gpu_highest_vram"];
|
||||||
|
}
|
||||||
|
binaryName = "nitro";
|
||||||
}
|
}
|
||||||
|
|
||||||
const binaryPath = path.join(binaryFolder, binaryName);
|
const binaryPath = path.join(binaryFolder, binaryName);
|
||||||
// Execute the binary
|
// Execute the binary
|
||||||
subprocess = spawn(binaryPath, [1, LOCAL_HOST, PORT], {
|
subprocess = spawn(binaryPath, [1, LOCAL_HOST, PORT], {
|
||||||
cwd: binaryFolder,
|
cwd: binaryFolder,
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
CUDA_VISIBLE_DEVICES: cudaVisibleDevices,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle subprocess output
|
// Handle subprocess output
|
||||||
@ -272,11 +441,11 @@ function spawnNitroProcess(nitroResourceProbe: any): Promise<any> {
|
|||||||
function getResourcesInfo(): Promise<ResourcesInfo> {
|
function getResourcesInfo(): Promise<ResourcesInfo> {
|
||||||
return new Promise(async (resolve) => {
|
return new Promise(async (resolve) => {
|
||||||
const cpu = await si.cpu();
|
const cpu = await si.cpu();
|
||||||
const mem = await si.mem();
|
// const mem = await si.mem();
|
||||||
|
|
||||||
const response = {
|
const response: ResourcesInfo = {
|
||||||
numCpuPhysicalCore: cpu.physicalCores,
|
numCpuPhysicalCore: cpu.physicalCores,
|
||||||
memAvailable: mem.available,
|
memAvailable: 0,
|
||||||
};
|
};
|
||||||
resolve(response);
|
resolve(response);
|
||||||
});
|
});
|
||||||
@ -292,4 +461,5 @@ module.exports = {
|
|||||||
stopModel,
|
stopModel,
|
||||||
killSubprocess,
|
killSubprocess,
|
||||||
dispose,
|
dispose,
|
||||||
|
updateNvidiaInfo,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -29,7 +29,7 @@ import { join } from "path";
|
|||||||
* It also subscribes to events emitted by the @janhq/core package and handles new message requests.
|
* It also subscribes to events emitted by the @janhq/core package and handles new message requests.
|
||||||
*/
|
*/
|
||||||
export default class JanInferenceOpenAIExtension implements InferenceExtension {
|
export default class JanInferenceOpenAIExtension implements InferenceExtension {
|
||||||
private static readonly _homeDir = "engines";
|
private static readonly _homeDir = "file://engines";
|
||||||
private static readonly _engineMetadataFileName = "openai.json";
|
private static readonly _engineMetadataFileName = "openai.json";
|
||||||
|
|
||||||
private static _currentModel: OpenAIModel;
|
private static _currentModel: OpenAIModel;
|
||||||
@ -53,8 +53,13 @@ export default class JanInferenceOpenAIExtension implements InferenceExtension {
|
|||||||
/**
|
/**
|
||||||
* Subscribes to events emitted by the @janhq/core package.
|
* Subscribes to events emitted by the @janhq/core package.
|
||||||
*/
|
*/
|
||||||
onLoad(): void {
|
async onLoad() {
|
||||||
fs.mkdir(JanInferenceOpenAIExtension._homeDir);
|
if (!(await fs.existsSync(JanInferenceOpenAIExtension._homeDir))) {
|
||||||
|
await fs
|
||||||
|
.mkdirSync(JanInferenceOpenAIExtension._homeDir)
|
||||||
|
.catch((err) => console.debug(err));
|
||||||
|
}
|
||||||
|
|
||||||
JanInferenceOpenAIExtension.writeDefaultEngineSettings();
|
JanInferenceOpenAIExtension.writeDefaultEngineSettings();
|
||||||
|
|
||||||
// Events subscription
|
// Events subscription
|
||||||
@ -85,12 +90,12 @@ export default class JanInferenceOpenAIExtension implements InferenceExtension {
|
|||||||
JanInferenceOpenAIExtension._homeDir,
|
JanInferenceOpenAIExtension._homeDir,
|
||||||
JanInferenceOpenAIExtension._engineMetadataFileName
|
JanInferenceOpenAIExtension._engineMetadataFileName
|
||||||
);
|
);
|
||||||
if (await fs.exists(engineFile)) {
|
if (await fs.existsSync(engineFile)) {
|
||||||
JanInferenceOpenAIExtension._engineSettings = JSON.parse(
|
const engine = await fs.readFileSync(engineFile, "utf-8");
|
||||||
await fs.readFile(engineFile)
|
JanInferenceOpenAIExtension._engineSettings =
|
||||||
);
|
typeof engine === "object" ? engine : JSON.parse(engine);
|
||||||
} else {
|
} else {
|
||||||
await fs.writeFile(
|
await fs.writeFileSync(
|
||||||
engineFile,
|
engineFile,
|
||||||
JSON.stringify(JanInferenceOpenAIExtension._engineSettings, null, 2)
|
JSON.stringify(JanInferenceOpenAIExtension._engineSettings, null, 2)
|
||||||
);
|
);
|
||||||
|
|||||||
@ -34,7 +34,7 @@ import { EngineSettings } from "./@types/global";
|
|||||||
export default class JanInferenceTritonTrtLLMExtension
|
export default class JanInferenceTritonTrtLLMExtension
|
||||||
implements InferenceExtension
|
implements InferenceExtension
|
||||||
{
|
{
|
||||||
private static readonly _homeDir = "engines";
|
private static readonly _homeDir = "file://engines";
|
||||||
private static readonly _engineMetadataFileName = "triton_trtllm.json";
|
private static readonly _engineMetadataFileName = "triton_trtllm.json";
|
||||||
|
|
||||||
static _currentModel: Model;
|
static _currentModel: Model;
|
||||||
@ -57,9 +57,9 @@ export default class JanInferenceTritonTrtLLMExtension
|
|||||||
/**
|
/**
|
||||||
* Subscribes to events emitted by the @janhq/core package.
|
* Subscribes to events emitted by the @janhq/core package.
|
||||||
*/
|
*/
|
||||||
onLoad(): void {
|
async onLoad() {
|
||||||
fs.mkdir(JanInferenceTritonTrtLLMExtension._homeDir);
|
if (!(await fs.existsSync(JanInferenceTritonTrtLLMExtension._homeDir)))
|
||||||
JanInferenceTritonTrtLLMExtension.writeDefaultEngineSettings();
|
JanInferenceTritonTrtLLMExtension.writeDefaultEngineSettings();
|
||||||
|
|
||||||
// Events subscription
|
// Events subscription
|
||||||
events.on(EventName.OnMessageSent, (data) =>
|
events.on(EventName.OnMessageSent, (data) =>
|
||||||
@ -98,12 +98,12 @@ export default class JanInferenceTritonTrtLLMExtension
|
|||||||
JanInferenceTritonTrtLLMExtension._homeDir,
|
JanInferenceTritonTrtLLMExtension._homeDir,
|
||||||
JanInferenceTritonTrtLLMExtension._engineMetadataFileName
|
JanInferenceTritonTrtLLMExtension._engineMetadataFileName
|
||||||
);
|
);
|
||||||
if (await fs.exists(engine_json)) {
|
if (await fs.existsSync(engine_json)) {
|
||||||
JanInferenceTritonTrtLLMExtension._engineSettings = JSON.parse(
|
const engine = await fs.readFileSync(engine_json, "utf-8");
|
||||||
await fs.readFile(engine_json)
|
JanInferenceTritonTrtLLMExtension._engineSettings =
|
||||||
);
|
typeof engine === "object" ? engine : JSON.parse(engine);
|
||||||
} else {
|
} else {
|
||||||
await fs.writeFile(
|
await fs.writeFileSync(
|
||||||
engine_json,
|
engine_json,
|
||||||
JSON.stringify(
|
JSON.stringify(
|
||||||
JanInferenceTritonTrtLLMExtension._engineSettings,
|
JanInferenceTritonTrtLLMExtension._engineSettings,
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@janhq/model-extension",
|
"name": "@janhq/model-extension",
|
||||||
"version": "1.0.16",
|
"version": "1.0.17",
|
||||||
"description": "Model Management Extension provides model exploration and seamless downloads",
|
"description": "Model Management Extension provides model exploration and seamless downloads",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"module": "dist/module.js",
|
"module": "dist/module.js",
|
||||||
|
|||||||
@ -8,14 +8,14 @@ import {
|
|||||||
InferenceEngine,
|
InferenceEngine,
|
||||||
joinPath,
|
joinPath,
|
||||||
} from '@janhq/core'
|
} from '@janhq/core'
|
||||||
import { basename } from 'path'
|
|
||||||
import { ModelExtension, Model } from '@janhq/core'
|
import { ModelExtension, Model } from '@janhq/core'
|
||||||
|
import { baseName } from '@janhq/core/.'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A extension for models
|
* A extension for models
|
||||||
*/
|
*/
|
||||||
export default class JanModelExtension implements ModelExtension {
|
export default class JanModelExtension implements ModelExtension {
|
||||||
private static readonly _homeDir = 'models'
|
private static readonly _homeDir = 'file://models'
|
||||||
private static readonly _modelMetadataFileName = 'model.json'
|
private static readonly _modelMetadataFileName = 'model.json'
|
||||||
private static readonly _supportedModelFormat = '.gguf'
|
private static readonly _supportedModelFormat = '.gguf'
|
||||||
private static readonly _incompletedModelFileName = '.download'
|
private static readonly _incompletedModelFileName = '.download'
|
||||||
@ -34,7 +34,7 @@ export default class JanModelExtension implements ModelExtension {
|
|||||||
* Called when the extension is loaded.
|
* Called when the extension is loaded.
|
||||||
* @override
|
* @override
|
||||||
*/
|
*/
|
||||||
onLoad(): void {
|
async onLoad() {
|
||||||
this.copyModelsToHomeDir()
|
this.copyModelsToHomeDir()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -46,11 +46,11 @@ export default class JanModelExtension implements ModelExtension {
|
|||||||
|
|
||||||
private async copyModelsToHomeDir() {
|
private async copyModelsToHomeDir() {
|
||||||
try {
|
try {
|
||||||
if (
|
// list all of the files under the home directory
|
||||||
localStorage.getItem(`${EXTENSION_NAME}-version`) === VERSION &&
|
|
||||||
(await fs.exists(JanModelExtension._homeDir))
|
if (await fs.existsSync(JanModelExtension._homeDir)) {
|
||||||
) {
|
// ignore if the model is already downloaded
|
||||||
console.debug('Model already migrated')
|
console.debug('Models already persisted.')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -62,7 +62,7 @@ export default class JanModelExtension implements ModelExtension {
|
|||||||
const srcPath = await joinPath([resourePath, 'models'])
|
const srcPath = await joinPath([resourePath, 'models'])
|
||||||
|
|
||||||
const userSpace = await getUserSpace()
|
const userSpace = await getUserSpace()
|
||||||
const destPath = await joinPath([userSpace, JanModelExtension._homeDir])
|
const destPath = await joinPath([userSpace, 'models'])
|
||||||
|
|
||||||
await fs.syncFile(srcPath, destPath)
|
await fs.syncFile(srcPath, destPath)
|
||||||
|
|
||||||
@ -94,11 +94,11 @@ export default class JanModelExtension implements ModelExtension {
|
|||||||
async downloadModel(model: Model): Promise<void> {
|
async downloadModel(model: Model): Promise<void> {
|
||||||
// create corresponding directory
|
// create corresponding directory
|
||||||
const modelDirPath = await joinPath([JanModelExtension._homeDir, model.id])
|
const modelDirPath = await joinPath([JanModelExtension._homeDir, model.id])
|
||||||
await fs.mkdir(modelDirPath)
|
if (!(await fs.existsSync(modelDirPath))) await fs.mkdirSync(modelDirPath)
|
||||||
|
|
||||||
// try to retrieve the download file name from the source url
|
// try to retrieve the download file name from the source url
|
||||||
// if it fails, use the model ID as the file name
|
// if it fails, use the model ID as the file name
|
||||||
const extractedFileName = basename(model.source_url)
|
const extractedFileName = await model.source_url.split('/').pop()
|
||||||
const fileName = extractedFileName
|
const fileName = extractedFileName
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.endsWith(JanModelExtension._supportedModelFormat)
|
.endsWith(JanModelExtension._supportedModelFormat)
|
||||||
@ -116,11 +116,11 @@ export default class JanModelExtension implements ModelExtension {
|
|||||||
async cancelModelDownload(modelId: string): Promise<void> {
|
async cancelModelDownload(modelId: string): Promise<void> {
|
||||||
return abortDownload(
|
return abortDownload(
|
||||||
await joinPath([JanModelExtension._homeDir, modelId, modelId])
|
await joinPath([JanModelExtension._homeDir, modelId, modelId])
|
||||||
).then(async () =>
|
).then(async () => {
|
||||||
fs.deleteFile(
|
fs.unlinkSync(
|
||||||
await joinPath([JanModelExtension._homeDir, modelId, modelId])
|
await joinPath([JanModelExtension._homeDir, modelId, modelId])
|
||||||
)
|
)
|
||||||
)
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -133,10 +133,10 @@ export default class JanModelExtension implements ModelExtension {
|
|||||||
const dirPath = await joinPath([JanModelExtension._homeDir, modelId])
|
const dirPath = await joinPath([JanModelExtension._homeDir, modelId])
|
||||||
|
|
||||||
// remove all files under dirPath except model.json
|
// remove all files under dirPath except model.json
|
||||||
const files = await fs.listFiles(dirPath)
|
const files = await fs.readdirSync(dirPath)
|
||||||
const deletePromises = files.map(async (fileName: string) => {
|
const deletePromises = files.map(async (fileName: string) => {
|
||||||
if (fileName !== JanModelExtension._modelMetadataFileName) {
|
if (fileName !== JanModelExtension._modelMetadataFileName) {
|
||||||
return fs.deleteFile(await joinPath([dirPath, fileName]))
|
return fs.unlinkSync(await joinPath([dirPath, fileName]))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
await Promise.allSettled(deletePromises)
|
await Promise.allSettled(deletePromises)
|
||||||
@ -158,7 +158,7 @@ export default class JanModelExtension implements ModelExtension {
|
|||||||
])
|
])
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await fs.writeFile(jsonFilePath, JSON.stringify(model, null, 2))
|
await fs.writeFileSync(jsonFilePath, JSON.stringify(model, null, 2))
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
}
|
}
|
||||||
@ -175,7 +175,7 @@ export default class JanModelExtension implements ModelExtension {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return await fs
|
return await fs
|
||||||
.listFiles(await joinPath([JanModelExtension._homeDir, modelDir]))
|
.readdirSync(await joinPath([JanModelExtension._homeDir, modelDir]))
|
||||||
.then((files: string[]) => {
|
.then((files: string[]) => {
|
||||||
// or model binary exists in the directory
|
// or model binary exists in the directory
|
||||||
// model binary name can match model ID or be a .gguf file and not be an incompleted model file
|
// model binary name can match model ID or be a .gguf file and not be an incompleted model file
|
||||||
@ -198,22 +198,17 @@ export default class JanModelExtension implements ModelExtension {
|
|||||||
selector?: (path: string, model: Model) => Promise<boolean>
|
selector?: (path: string, model: Model) => Promise<boolean>
|
||||||
): Promise<Model[]> {
|
): Promise<Model[]> {
|
||||||
try {
|
try {
|
||||||
const filesUnderJanRoot = await fs.listFiles('')
|
if (!(await fs.existsSync(JanModelExtension._homeDir))) {
|
||||||
if (!filesUnderJanRoot.includes(JanModelExtension._homeDir)) {
|
|
||||||
console.debug('model folder not found')
|
console.debug('model folder not found')
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
const files: string[] = await fs.listFiles(JanModelExtension._homeDir)
|
const files: string[] = await fs.readdirSync(JanModelExtension._homeDir)
|
||||||
|
|
||||||
const allDirectories: string[] = []
|
const allDirectories: string[] = []
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
const isDirectory = await fs.isDirectory(
|
if (file === '.DS_Store') continue
|
||||||
await joinPath([JanModelExtension._homeDir, file])
|
allDirectories.push(file)
|
||||||
)
|
|
||||||
if (isDirectory) {
|
|
||||||
allDirectories.push(file)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const readJsonPromises = allDirectories.map(async (dirName) => {
|
const readJsonPromises = allDirectories.map(async (dirName) => {
|
||||||
@ -247,6 +242,7 @@ export default class JanModelExtension implements ModelExtension {
|
|||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return modelData.filter((e) => !!e)
|
return modelData.filter((e) => !!e)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
@ -255,7 +251,7 @@ export default class JanModelExtension implements ModelExtension {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private readModelMetadata(path: string) {
|
private readModelMetadata(path: string) {
|
||||||
return fs.readFile(path)
|
return fs.readFileSync(path, 'utf-8')
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -18,7 +18,7 @@ export default class JanMonitoringExtension implements MonitoringExtension {
|
|||||||
/**
|
/**
|
||||||
* Called when the extension is loaded.
|
* Called when the extension is loaded.
|
||||||
*/
|
*/
|
||||||
onLoad(): void {}
|
async onLoad() {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when the extension is unloaded.
|
* Called when the extension is unloaded.
|
||||||
|
|||||||
@ -4,11 +4,11 @@ const getResourcesInfo = async () =>
|
|||||||
new Promise(async (resolve) => {
|
new Promise(async (resolve) => {
|
||||||
const cpu = await si.cpu();
|
const cpu = await si.cpu();
|
||||||
const mem = await si.mem();
|
const mem = await si.mem();
|
||||||
const gpu = await si.graphics();
|
// const gpu = await si.graphics();
|
||||||
const response = {
|
const response = {
|
||||||
cpu,
|
cpu,
|
||||||
mem,
|
mem,
|
||||||
gpu,
|
// gpu,
|
||||||
};
|
};
|
||||||
resolve(response);
|
resolve(response);
|
||||||
});
|
});
|
||||||
|
|||||||
23
models/codeninja-1.0-7b/model.json
Normal file
23
models/codeninja-1.0-7b/model.json
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"source_url": "https://huggingface.co/beowolx/CodeNinja-1.0-OpenChat-7B-GGUF/resolve/main/codeninja-1.0-openchat-7b.Q4_K_M.gguf",
|
||||||
|
"id": "codeninja-1.0-7b",
|
||||||
|
"object": "model",
|
||||||
|
"name": "CodeNinja 7B Q4",
|
||||||
|
"version": "1.0",
|
||||||
|
"description": "CodeNinja is finetuned on openchat/openchat-3.5-1210. It is good for codding tasks",
|
||||||
|
"format": "gguf",
|
||||||
|
"settings": {
|
||||||
|
"ctx_len": 4096,
|
||||||
|
"prompt_template": "GPT4 Correct User: {prompt}<|end_of_turn|>GPT4 Correct Assistant:"
|
||||||
|
},
|
||||||
|
"parameters": {
|
||||||
|
"max_tokens": 4096
|
||||||
|
},
|
||||||
|
"metadata": {
|
||||||
|
"author": "Beowolx",
|
||||||
|
"tags": ["7B", "Finetuned"],
|
||||||
|
"size": 4370000000
|
||||||
|
},
|
||||||
|
"engine": "nitro"
|
||||||
|
}
|
||||||
|
|
||||||
23
models/magicoder-s-ds-7b/model.json
Normal file
23
models/magicoder-s-ds-7b/model.json
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"source_url": "https://huggingface.co/TheBloke/Magicoder-S-DS-6.7B-GGUF/resolve/main/magicoder-s-ds-6.7b.Q4_K_M.gguf",
|
||||||
|
"id": "magicoder-s-ds7b",
|
||||||
|
"object": "model",
|
||||||
|
"name": "Magicoder 7B Q4",
|
||||||
|
"version": "1.0",
|
||||||
|
"description": "Magicoder is a model family, a novel approach to enlightening LLMs with open-source code snippets for generating low-bias and high-quality instruction data for code.",
|
||||||
|
"format": "gguf",
|
||||||
|
"settings": {
|
||||||
|
"ctx_len": 4096,
|
||||||
|
"prompt_template": "@@ Instruction\n{prompt}\n@@ Response"
|
||||||
|
},
|
||||||
|
"parameters": {
|
||||||
|
"max_tokens": 4096
|
||||||
|
},
|
||||||
|
"metadata": {
|
||||||
|
"author": "Ise-uiuc",
|
||||||
|
"tags": ["7B", "Code"],
|
||||||
|
"size": 4080000000
|
||||||
|
},
|
||||||
|
"engine": "nitro"
|
||||||
|
}
|
||||||
|
|
||||||
25
models/stealth-v1.2-7b/model.json
Normal file
25
models/stealth-v1.2-7b/model.json
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"source_url": "https://huggingface.co/janhq/stealth-v1.2-GGUF/resolve/main/stealth-v1.2.Q4_K_M.gguf",
|
||||||
|
"id": "stealth-v1.2-7b",
|
||||||
|
"object": "model",
|
||||||
|
"name": "Stealth-v1.2 7B Q4",
|
||||||
|
"version": "1.0",
|
||||||
|
"description": "This is a new experimental family designed to enhance Mathematical and Logical abilities.",
|
||||||
|
"format": "gguf",
|
||||||
|
"settings": {
|
||||||
|
"ctx_len": 4096,
|
||||||
|
"prompt_template": "<|im_start|>system\n{system_message}<|im_end|>\n<|im_start|>user\n{prompt}<|im_end|>\n<|im_start|>assistant"
|
||||||
|
},
|
||||||
|
"parameters": {
|
||||||
|
"max_tokens": 4096
|
||||||
|
},
|
||||||
|
"metadata": {
|
||||||
|
"author": "Jan",
|
||||||
|
"tags": [
|
||||||
|
"7B",
|
||||||
|
"Merged"
|
||||||
|
],
|
||||||
|
"size": 4370000000
|
||||||
|
},
|
||||||
|
"engine": "nitro"
|
||||||
|
}
|
||||||
@ -4,7 +4,7 @@
|
|||||||
"object": "model",
|
"object": "model",
|
||||||
"name": "Trinity-v1 7B Q4",
|
"name": "Trinity-v1 7B Q4",
|
||||||
"version": "1.0",
|
"version": "1.0",
|
||||||
"description": "Trinity is an experimental model merge of GreenNodeLM & LeoScorpius using the Slerp method. Recommended for daily assistance purposes.",
|
"description": "Please use the latest version Trinity v1.2 for the best experience. Trinity is an experimental model merge of GreenNodeLM & LeoScorpius using the Slerp method. Recommended for daily assistance purposes.",
|
||||||
"format": "gguf",
|
"format": "gguf",
|
||||||
"settings": {
|
"settings": {
|
||||||
"ctx_len": 4096,
|
"ctx_len": 4096,
|
||||||
|
|||||||
@ -6,7 +6,8 @@
|
|||||||
"uikit",
|
"uikit",
|
||||||
"core",
|
"core",
|
||||||
"electron",
|
"electron",
|
||||||
"web"
|
"web",
|
||||||
|
"server"
|
||||||
],
|
],
|
||||||
"nohoist": [
|
"nohoist": [
|
||||||
"uikit",
|
"uikit",
|
||||||
@ -16,7 +17,9 @@
|
|||||||
"electron",
|
"electron",
|
||||||
"electron/**",
|
"electron/**",
|
||||||
"web",
|
"web",
|
||||||
"web/**"
|
"web/**",
|
||||||
|
"server",
|
||||||
|
"server/**"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@ -28,6 +31,7 @@
|
|||||||
"test-local": "yarn lint && yarn build:test && yarn test",
|
"test-local": "yarn lint && yarn build:test && yarn test",
|
||||||
"dev:uikit": "yarn workspace @janhq/uikit install && yarn workspace @janhq/uikit dev",
|
"dev:uikit": "yarn workspace @janhq/uikit install && yarn workspace @janhq/uikit dev",
|
||||||
"build:uikit": "yarn workspace @janhq/uikit install && yarn workspace @janhq/uikit build",
|
"build:uikit": "yarn workspace @janhq/uikit install && yarn workspace @janhq/uikit build",
|
||||||
|
"build:server": "cd server && yarn install && yarn run build",
|
||||||
"build:core": "cd core && yarn install && yarn run build",
|
"build:core": "cd core && yarn install && yarn run build",
|
||||||
"build:web": "yarn workspace jan-web build && cpx \"web/out/**\" \"electron/renderer/\"",
|
"build:web": "yarn workspace jan-web build && cpx \"web/out/**\" \"electron/renderer/\"",
|
||||||
"build:electron": "cpx \"models/**\" \"electron/models/\" && yarn workspace jan build",
|
"build:electron": "cpx \"models/**\" \"electron/models/\" && yarn workspace jan build",
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 38 KiB |
43
server/index.ts
Normal file
43
server/index.ts
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import fastify from "fastify";
|
||||||
|
import dotenv from "dotenv";
|
||||||
|
import { v1Router } from "@janhq/core/node";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
|
const JAN_API_HOST = process.env.JAN_API_HOST || "0.0.0.0";
|
||||||
|
const JAN_API_PORT = Number.parseInt(process.env.JAN_API_PORT || "1337");
|
||||||
|
|
||||||
|
const server = fastify();
|
||||||
|
server.register(require("@fastify/cors"), {});
|
||||||
|
server.register(
|
||||||
|
(childContext, _, done) => {
|
||||||
|
childContext.register(require("@fastify/static"), {
|
||||||
|
root:
|
||||||
|
process.env.EXTENSION_ROOT ||
|
||||||
|
path.join(require("os").homedir(), "jan", "extensions"),
|
||||||
|
wildcard: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
done();
|
||||||
|
},
|
||||||
|
{ prefix: "extensions" }
|
||||||
|
);
|
||||||
|
server.register(v1Router, { prefix: "/v1" });
|
||||||
|
|
||||||
|
export const startServer = () => {
|
||||||
|
server
|
||||||
|
.listen({
|
||||||
|
port: JAN_API_PORT,
|
||||||
|
host: JAN_API_HOST,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
console.log(
|
||||||
|
`JAN API listening at: http://${JAN_API_HOST}:${JAN_API_PORT}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const stopServer = () => {
|
||||||
|
server.close();
|
||||||
|
};
|
||||||
@ -1,19 +1,3 @@
|
|||||||
import fastify from 'fastify'
|
import { startServer } from "./index";
|
||||||
import dotenv from 'dotenv'
|
|
||||||
import v1API from './v1'
|
|
||||||
const server = fastify()
|
|
||||||
|
|
||||||
dotenv.config()
|
|
||||||
server.register(v1API, {prefix: "/api/v1"})
|
|
||||||
|
|
||||||
|
|
||||||
const JAN_API_PORT = Number.parseInt(process.env.JAN_API_PORT || '1337')
|
|
||||||
const JAN_API_HOST = process.env.JAN_API_HOST || "0.0.0.0"
|
|
||||||
|
|
||||||
server.listen({
|
|
||||||
port: JAN_API_PORT,
|
|
||||||
host: JAN_API_HOST
|
|
||||||
}).then(() => {
|
|
||||||
console.log(`JAN API listening at: http://${JAN_API_HOST}:${JAN_API_PORT}`);
|
|
||||||
})
|
|
||||||
|
|
||||||
|
startServer();
|
||||||
|
|||||||
@ -1,32 +1,37 @@
|
|||||||
{
|
{
|
||||||
"name": "jan-server",
|
"name": "@janhq/server",
|
||||||
"version": "0.1.3",
|
"version": "0.1.3",
|
||||||
"main": "./build/main.js",
|
"main": "build/index.js",
|
||||||
|
"types": "build/index.d.ts",
|
||||||
"author": "Jan <service@jan.ai>",
|
"author": "Jan <service@jan.ai>",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"homepage": "https://jan.ai",
|
"homepage": "https://jan.ai",
|
||||||
"description": "Use offline LLMs with your own data. Run open source models like Llama2 or Falcon on your internal computers/servers.",
|
"description": "Use offline LLMs with your own data. Run open source models like Llama2 or Falcon on your internal computers/servers.",
|
||||||
"build": "",
|
"files": [
|
||||||
|
"build/**"
|
||||||
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"lint": "eslint . --ext \".js,.jsx,.ts,.tsx\"",
|
"lint": "eslint . --ext \".js,.jsx,.ts,.tsx\"",
|
||||||
"test:e2e": "playwright test --workers=1",
|
"test:e2e": "playwright test --workers=1",
|
||||||
"dev": "nodemon .",
|
"dev": "tsc --watch & node --watch build/main.js",
|
||||||
"build": "tsc"
|
"build": "tsc"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@fastify/cors": "^8.4.2",
|
||||||
|
"@fastify/static": "^6.12.0",
|
||||||
|
"@janhq/core": "link:./core",
|
||||||
|
"dotenv": "^16.3.1",
|
||||||
|
"fastify": "^4.24.3",
|
||||||
|
"request": "^2.88.2",
|
||||||
|
"request-progress": "^3.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/body-parser": "^1.19.5",
|
"@types/body-parser": "^1.19.5",
|
||||||
"@types/npmcli__arborist": "^5.6.4",
|
"@types/npmcli__arborist": "^5.6.4",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.7.3",
|
"@typescript-eslint/eslint-plugin": "^6.7.3",
|
||||||
"@typescript-eslint/parser": "^6.7.3",
|
"@typescript-eslint/parser": "^6.7.3",
|
||||||
"dotenv": "^16.3.1",
|
|
||||||
"eslint-plugin-react": "^7.33.2",
|
"eslint-plugin-react": "^7.33.2",
|
||||||
"fastify": "^4.24.3",
|
"run-script-os": "^1.1.6",
|
||||||
"nodemon": "^3.0.1",
|
"typescript": "^5.2.2"
|
||||||
"run-script-os": "^1.1.6"
|
|
||||||
},
|
|
||||||
"installConfig": {
|
|
||||||
"hoistingLimits": "workspaces"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,10 +13,12 @@
|
|||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"paths": { "*": ["node_modules/*"] },
|
"paths": { "*": ["node_modules/*"] },
|
||||||
"typeRoots": ["node_modules/@types"]
|
"typeRoots": ["node_modules/@types"],
|
||||||
|
"ignoreDeprecations": "5.0",
|
||||||
|
"declaration": true
|
||||||
},
|
},
|
||||||
// "sourceMap": true,
|
// "sourceMap": true,
|
||||||
|
|
||||||
"include": ["./**/*.ts"],
|
"include": ["./**/*.ts"],
|
||||||
"exclude": ["core", "build", "dist", "tests", "node_modules"]
|
"exclude": ["core", "build", "dist", "tests", "node_modules"]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,8 +0,0 @@
|
|||||||
import { FastifyInstance, FastifyPluginAsync, FastifyPluginOptions } from 'fastify'
|
|
||||||
|
|
||||||
const router: FastifyPluginAsync = async (app: FastifyInstance, opts: FastifyPluginOptions) => {
|
|
||||||
//TODO: Add controllers for assistants here
|
|
||||||
// app.get("/", controller)
|
|
||||||
// app.post("/", controller)
|
|
||||||
}
|
|
||||||
export default router;
|
|
||||||
@ -1,11 +0,0 @@
|
|||||||
import { FastifyInstance, FastifyPluginAsync, FastifyPluginOptions } from 'fastify'
|
|
||||||
|
|
||||||
const router: FastifyPluginAsync = async (app: FastifyInstance, opts: FastifyPluginOptions) => {
|
|
||||||
//TODO: Add controllers for here
|
|
||||||
// app.get("/", controller)
|
|
||||||
|
|
||||||
app.post("/", (req, res) => {
|
|
||||||
req.body
|
|
||||||
})
|
|
||||||
}
|
|
||||||
export default router;
|
|
||||||
@ -1,37 +0,0 @@
|
|||||||
import assistantsAPI from './assistants'
|
|
||||||
import chatCompletionAPI from './chat'
|
|
||||||
import modelsAPI from './models'
|
|
||||||
import threadsAPI from './threads'
|
|
||||||
|
|
||||||
import { FastifyInstance, FastifyPluginAsync } from 'fastify'
|
|
||||||
|
|
||||||
const router: FastifyPluginAsync = async (app: FastifyInstance, opts) => {
|
|
||||||
app.register(
|
|
||||||
assistantsAPI,
|
|
||||||
{
|
|
||||||
prefix: "/assistants"
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
app.register(
|
|
||||||
chatCompletionAPI,
|
|
||||||
{
|
|
||||||
prefix: "/chat/completion"
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
app.register(
|
|
||||||
modelsAPI,
|
|
||||||
{
|
|
||||||
prefix: "/models"
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
app.register(
|
|
||||||
threadsAPI,
|
|
||||||
{
|
|
||||||
prefix: "/threads"
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
export default router;
|
|
||||||
@ -1,23 +0,0 @@
|
|||||||
import { RouteHandlerMethod, FastifyRequest, FastifyReply } from 'fastify'
|
|
||||||
import { MODEL_FOLDER_PATH } from "./index"
|
|
||||||
import fs from 'fs/promises'
|
|
||||||
|
|
||||||
const controller: RouteHandlerMethod = async (req: FastifyRequest, res: FastifyReply) => {
|
|
||||||
//TODO: download models impl
|
|
||||||
//Mirror logic from JanModelExtension.downloadModel?
|
|
||||||
let model = req.body.model;
|
|
||||||
|
|
||||||
// Fetching logic
|
|
||||||
// const directoryPath = join(MODEL_FOLDER_PATH, model.id)
|
|
||||||
// await fs.mkdir(directoryPath)
|
|
||||||
|
|
||||||
// const path = join(directoryPath, model.id)
|
|
||||||
// downloadFile(model.source_url, path)
|
|
||||||
// TODO: Different model downloader from different model vendor
|
|
||||||
|
|
||||||
res.status(200).send({
|
|
||||||
status: "Ok"
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export default controller;
|
|
||||||
@ -1,61 +0,0 @@
|
|||||||
|
|
||||||
export const MODEL_FOLDER_PATH = "./data/models"
|
|
||||||
export const _modelMetadataFileName = 'model.json'
|
|
||||||
|
|
||||||
import fs from 'fs/promises'
|
|
||||||
import { Model } from '@janhq/core'
|
|
||||||
import { join } from 'path'
|
|
||||||
|
|
||||||
// map string => model object
|
|
||||||
let modelIndex = new Map<String, Model>();
|
|
||||||
async function buildModelIndex(){
|
|
||||||
let modelIds = await fs.readdir(MODEL_FOLDER_PATH);
|
|
||||||
// TODO: read modelFolders to get model info, mirror JanModelExtension?
|
|
||||||
try{
|
|
||||||
for(let modelId in modelIds){
|
|
||||||
let path = join(MODEL_FOLDER_PATH, modelId)
|
|
||||||
let fileData = await fs.readFile(join(path, _modelMetadataFileName))
|
|
||||||
modelIndex.set(modelId, JSON.parse(fileData.toString("utf-8")) as Model)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch(err){
|
|
||||||
console.error("build model index failed. ", err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
buildModelIndex()
|
|
||||||
|
|
||||||
import { FastifyInstance, FastifyPluginAsync, FastifyPluginOptions } from 'fastify'
|
|
||||||
import downloadModelController from './downloadModel'
|
|
||||||
import { startModel, stopModel } from './modelOp'
|
|
||||||
|
|
||||||
const router: FastifyPluginAsync = async (app: FastifyInstance, opts: FastifyPluginOptions) => {
|
|
||||||
//TODO: Add controllers declaration here
|
|
||||||
|
|
||||||
///////////// CRUD ////////////////
|
|
||||||
// Model listing
|
|
||||||
app.get("/", async (req, res) => {
|
|
||||||
res.status(200).send(
|
|
||||||
modelIndex.values()
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Retrieve model info
|
|
||||||
app.get("/:id", (req, res) => {
|
|
||||||
res.status(200).send(
|
|
||||||
modelIndex.get(req.params.id)
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Delete model
|
|
||||||
app.delete("/:id", (req, res) => {
|
|
||||||
modelIndex.delete(req.params)
|
|
||||||
|
|
||||||
// TODO: delete on disk
|
|
||||||
})
|
|
||||||
|
|
||||||
///////////// Other ops ////////////////
|
|
||||||
app.post("/", downloadModelController)
|
|
||||||
app.put("/start", startModel)
|
|
||||||
app.put("/stop", stopModel)
|
|
||||||
}
|
|
||||||
export default router;
|
|
||||||
@ -1,11 +0,0 @@
|
|||||||
import {FastifyRequest, FastifyReply} from 'fastify'
|
|
||||||
|
|
||||||
export async function startModel(req: FastifyRequest, res: FastifyReply): Promise<void> {
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function stopModel(req: FastifyRequest, res: FastifyReply): Promise<void> {
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
import { FastifyInstance, FastifyPluginAsync, FastifyPluginOptions } from 'fastify'
|
|
||||||
|
|
||||||
const router: FastifyPluginAsync = async (app: FastifyInstance, opts: FastifyPluginOptions) => {
|
|
||||||
//TODO: Add controllers declaration here
|
|
||||||
|
|
||||||
// app.get()
|
|
||||||
}
|
|
||||||
export default router;
|
|
||||||
84
web/containers/GPUDriverPromptModal/index.tsx
Normal file
84
web/containers/GPUDriverPromptModal/index.tsx
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
import { openExternalUrl } from '@janhq/core'
|
||||||
|
|
||||||
|
import {
|
||||||
|
ModalClose,
|
||||||
|
ModalFooter,
|
||||||
|
ModalContent,
|
||||||
|
Modal,
|
||||||
|
ModalTitle,
|
||||||
|
ModalHeader,
|
||||||
|
Button,
|
||||||
|
} from '@janhq/uikit'
|
||||||
|
|
||||||
|
import { useAtom } from 'jotai'
|
||||||
|
|
||||||
|
import { isShowNotificationAtom, useSettings } from '@/hooks/useSettings'
|
||||||
|
|
||||||
|
const GPUDriverPrompt: React.FC = () => {
|
||||||
|
const [showNotification, setShowNotification] = useAtom(
|
||||||
|
isShowNotificationAtom
|
||||||
|
)
|
||||||
|
|
||||||
|
const { saveSettings } = useSettings()
|
||||||
|
const onDoNotShowAgainChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const isChecked = !e.target.checked
|
||||||
|
saveSettings({ notify: isChecked })
|
||||||
|
}
|
||||||
|
|
||||||
|
const openChanged = () => {
|
||||||
|
setShowNotification(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Modal open={showNotification} onOpenChange={openChanged}>
|
||||||
|
<ModalContent>
|
||||||
|
<ModalHeader>
|
||||||
|
<ModalTitle>Missing Nvidia Driver and Cuda Toolkit</ModalTitle>
|
||||||
|
</ModalHeader>
|
||||||
|
<p>
|
||||||
|
It seems like you are missing Nvidia Driver or Cuda Toolkit or both.
|
||||||
|
Please follow the instructions on the{' '}
|
||||||
|
<span
|
||||||
|
className="cursor-pointer text-blue-600"
|
||||||
|
onClick={() =>
|
||||||
|
openExternalUrl('https://developer.nvidia.com/cuda-toolkit')
|
||||||
|
}
|
||||||
|
>
|
||||||
|
NVidia Cuda Toolkit Installation Page
|
||||||
|
</span>{' '}
|
||||||
|
and the{' '}
|
||||||
|
<span
|
||||||
|
className="cursor-pointer text-blue-600"
|
||||||
|
onClick={() =>
|
||||||
|
openExternalUrl('https://www.nvidia.com/Download/index.aspx')
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Nvidia Driver Installation Page
|
||||||
|
</span>
|
||||||
|
.
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<input
|
||||||
|
id="default-checkbox"
|
||||||
|
type="checkbox"
|
||||||
|
onChange={onDoNotShowAgainChange}
|
||||||
|
className="h-4 w-4 rounded border-gray-300 bg-gray-100 text-blue-600 focus:ring-2 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:ring-offset-gray-800 dark:focus:ring-blue-600"
|
||||||
|
/>
|
||||||
|
<span>Don't show again</span>
|
||||||
|
</div>
|
||||||
|
<ModalFooter>
|
||||||
|
<div className="flex gap-x-2">
|
||||||
|
<ModalClose asChild>
|
||||||
|
<Button themes="ghost">OK</Button>
|
||||||
|
</ModalClose>
|
||||||
|
</div>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
export default GPUDriverPrompt
|
||||||
@ -1,9 +1,8 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
|
||||||
import { basename } from 'path'
|
|
||||||
|
|
||||||
import { PropsWithChildren, useEffect, useRef } from 'react'
|
import { PropsWithChildren, useEffect, useRef } from 'react'
|
||||||
|
|
||||||
|
import { baseName } from '@janhq/core'
|
||||||
import { useAtomValue, useSetAtom } from 'jotai'
|
import { useAtomValue, useSetAtom } from 'jotai'
|
||||||
|
|
||||||
import { useDownloadState } from '@/hooks/useDownloadState'
|
import { useDownloadState } from '@/hooks/useDownloadState'
|
||||||
@ -37,10 +36,11 @@ export default function EventListenerWrapper({ children }: PropsWithChildren) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (window && window.electronAPI) {
|
if (window && window.electronAPI) {
|
||||||
window.electronAPI.onFileDownloadUpdate(
|
window.electronAPI.onFileDownloadUpdate(
|
||||||
(_event: string, state: any | undefined) => {
|
async (_event: string, state: any | undefined) => {
|
||||||
if (!state) return
|
if (!state) return
|
||||||
|
const modelName = await baseName(state.fileName)
|
||||||
const model = modelsRef.current.find(
|
const model = modelsRef.current.find(
|
||||||
(model) => modelBinFileName(model) === basename(state.fileName)
|
(model) => modelBinFileName(model) === modelName
|
||||||
)
|
)
|
||||||
if (model)
|
if (model)
|
||||||
setDownloadState({
|
setDownloadState({
|
||||||
@ -50,25 +50,31 @@ export default function EventListenerWrapper({ children }: PropsWithChildren) {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
window.electronAPI.onFileDownloadError((_event: string, state: any) => {
|
window.electronAPI.onFileDownloadError(
|
||||||
console.error('Download error', state)
|
async (_event: string, state: any) => {
|
||||||
const model = modelsRef.current.find(
|
console.error('Download error', state)
|
||||||
(model) => modelBinFileName(model) === basename(state.fileName)
|
const modelName = await baseName(state.fileName)
|
||||||
)
|
|
||||||
if (model) setDownloadStateFailed(model.id)
|
|
||||||
})
|
|
||||||
|
|
||||||
window.electronAPI.onFileDownloadSuccess((_event: string, state: any) => {
|
|
||||||
if (state && state.fileName) {
|
|
||||||
const model = modelsRef.current.find(
|
const model = modelsRef.current.find(
|
||||||
(model) => modelBinFileName(model) === basename(state.fileName)
|
(model) => modelBinFileName(model) === modelName
|
||||||
)
|
)
|
||||||
if (model) {
|
if (model) setDownloadStateFailed(model.id)
|
||||||
setDownloadStateSuccess(model.id)
|
}
|
||||||
setDownloadedModels([...downloadedModelRef.current, model])
|
)
|
||||||
|
|
||||||
|
window.electronAPI.onFileDownloadSuccess(
|
||||||
|
async (_event: string, state: any) => {
|
||||||
|
if (state && state.fileName) {
|
||||||
|
const modelName = await baseName(state.fileName)
|
||||||
|
const model = modelsRef.current.find(
|
||||||
|
async (model) => modelBinFileName(model) === modelName
|
||||||
|
)
|
||||||
|
if (model) {
|
||||||
|
setDownloadStateSuccess(model.id)
|
||||||
|
setDownloadedModels([...downloadedModelRef.current, model])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
)
|
||||||
|
|
||||||
window.electronAPI.onAppUpdateDownloadUpdate(
|
window.electronAPI.onAppUpdateDownloadUpdate(
|
||||||
(_event: string, progress: any) => {
|
(_event: string, progress: any) => {
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import { TooltipProvider } from '@janhq/uikit'
|
|||||||
|
|
||||||
import { PostHogProvider } from 'posthog-js/react'
|
import { PostHogProvider } from 'posthog-js/react'
|
||||||
|
|
||||||
|
import GPUDriverPrompt from '@/containers/GPUDriverPromptModal'
|
||||||
import EventListenerWrapper from '@/containers/Providers/EventListener'
|
import EventListenerWrapper from '@/containers/Providers/EventListener'
|
||||||
import JotaiWrapper from '@/containers/Providers/Jotai'
|
import JotaiWrapper from '@/containers/Providers/Jotai'
|
||||||
import ThemeWrapper from '@/containers/Providers/Theme'
|
import ThemeWrapper from '@/containers/Providers/Theme'
|
||||||
@ -25,11 +26,11 @@ import { instance } from '@/utils/posthog'
|
|||||||
import { extensionManager } from '@/extension'
|
import { extensionManager } from '@/extension'
|
||||||
|
|
||||||
const Providers = (props: PropsWithChildren) => {
|
const Providers = (props: PropsWithChildren) => {
|
||||||
|
const { children } = props
|
||||||
|
|
||||||
const [setupCore, setSetupCore] = useState(false)
|
const [setupCore, setSetupCore] = useState(false)
|
||||||
const [activated, setActivated] = useState(false)
|
const [activated, setActivated] = useState(false)
|
||||||
|
|
||||||
const { children } = props
|
|
||||||
|
|
||||||
async function setupExtensions() {
|
async function setupExtensions() {
|
||||||
// Register all active extensions
|
// Register all active extensions
|
||||||
await extensionManager.registerActive()
|
await extensionManager.registerActive()
|
||||||
@ -74,6 +75,7 @@ const Providers = (props: PropsWithChildren) => {
|
|||||||
<FeatureToggleWrapper>
|
<FeatureToggleWrapper>
|
||||||
<EventListenerWrapper>
|
<EventListenerWrapper>
|
||||||
<TooltipProvider delayDuration={0}>{children}</TooltipProvider>
|
<TooltipProvider delayDuration={0}>{children}</TooltipProvider>
|
||||||
|
{!isMac && <GPUDriverPrompt />}
|
||||||
</EventListenerWrapper>
|
</EventListenerWrapper>
|
||||||
<Toaster position="top-right" />
|
<Toaster position="top-right" />
|
||||||
</FeatureToggleWrapper>
|
</FeatureToggleWrapper>
|
||||||
|
|||||||
@ -81,7 +81,10 @@ export class ExtensionManager {
|
|||||||
*/
|
*/
|
||||||
async activateExtension(extension: Extension) {
|
async activateExtension(extension: Extension) {
|
||||||
// Import class
|
// Import class
|
||||||
await import(/* webpackIgnore: true */ extension.url).then(
|
const extensionUrl = window.electronAPI
|
||||||
|
? extension.url
|
||||||
|
: extension.url.replace('extension://', `${API_BASE_URL}/extensions/`)
|
||||||
|
await import(/* webpackIgnore: true */ extensionUrl).then(
|
||||||
(extensionClass) => {
|
(extensionClass) => {
|
||||||
// Register class if it has a default export
|
// Register class if it has a default export
|
||||||
if (
|
if (
|
||||||
|
|||||||
@ -2,11 +2,16 @@ import { fs, joinPath } from '@janhq/core'
|
|||||||
|
|
||||||
export const useEngineSettings = () => {
|
export const useEngineSettings = () => {
|
||||||
const readOpenAISettings = async () => {
|
const readOpenAISettings = async () => {
|
||||||
const settings = await fs.readFile(
|
if (
|
||||||
await joinPath(['engines', 'openai.json'])
|
!(await fs.existsSync(await joinPath(['file://engines', 'openai.json'])))
|
||||||
|
)
|
||||||
|
return {}
|
||||||
|
const settings = await fs.readFileSync(
|
||||||
|
await joinPath(['file://engines', 'openai.json']),
|
||||||
|
'utf-8'
|
||||||
)
|
)
|
||||||
if (settings) {
|
if (settings) {
|
||||||
return JSON.parse(settings)
|
return typeof settings === 'object' ? settings : JSON.parse(settings)
|
||||||
}
|
}
|
||||||
return {}
|
return {}
|
||||||
}
|
}
|
||||||
@ -17,8 +22,8 @@ export const useEngineSettings = () => {
|
|||||||
}) => {
|
}) => {
|
||||||
const settings = await readOpenAISettings()
|
const settings = await readOpenAISettings()
|
||||||
settings.api_key = apiKey
|
settings.api_key = apiKey
|
||||||
await fs.writeFile(
|
await fs.writeFileSync(
|
||||||
await joinPath(['engines', 'openai.json']),
|
await joinPath(['file://engines', 'openai.json']),
|
||||||
JSON.stringify(settings)
|
JSON.stringify(settings)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,7 +13,6 @@ import {
|
|||||||
events,
|
events,
|
||||||
Model,
|
Model,
|
||||||
ConversationalExtension,
|
ConversationalExtension,
|
||||||
ModelRuntimeParams,
|
|
||||||
} from '@janhq/core'
|
} from '@janhq/core'
|
||||||
import { useAtom, useAtomValue, useSetAtom } from 'jotai'
|
import { useAtom, useAtomValue, useSetAtom } from 'jotai'
|
||||||
|
|
||||||
@ -173,7 +172,7 @@ export default function useSendChatMessage() {
|
|||||||
updateThreadInitSuccess(activeThread.id)
|
updateThreadInitSuccess(activeThread.id)
|
||||||
updateThread(updatedThread)
|
updateThread(updatedThread)
|
||||||
|
|
||||||
extensionManager
|
await extensionManager
|
||||||
.get<ConversationalExtension>(ExtensionType.Conversational)
|
.get<ConversationalExtension>(ExtensionType.Conversational)
|
||||||
?.saveThread(updatedThread)
|
?.saveThread(updatedThread)
|
||||||
}
|
}
|
||||||
|
|||||||
67
web/hooks/useSettings.ts
Normal file
67
web/hooks/useSettings.ts
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
|
||||||
|
import { fs, joinPath } from '@janhq/core'
|
||||||
|
import { atom, useAtom } from 'jotai'
|
||||||
|
|
||||||
|
export const isShowNotificationAtom = atom<boolean>(false)
|
||||||
|
|
||||||
|
export const useSettings = () => {
|
||||||
|
const [isGPUModeEnabled, setIsGPUModeEnabled] = useState(false) // New state for GPU mode
|
||||||
|
const [showNotification, setShowNotification] = useAtom(
|
||||||
|
isShowNotificationAtom
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setTimeout(() => validateSettings, 3000)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const validateSettings = async () => {
|
||||||
|
readSettings().then((settings) => {
|
||||||
|
if (
|
||||||
|
settings &&
|
||||||
|
settings.notify &&
|
||||||
|
((settings.nvidia_driver?.exist && !settings.cuda?.exist) ||
|
||||||
|
!settings.nvidia_driver?.exist)
|
||||||
|
) {
|
||||||
|
setShowNotification(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if run_mode is 'gpu' or 'cpu' and update state accordingly
|
||||||
|
setIsGPUModeEnabled(settings?.run_mode === 'gpu')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const readSettings = async () => {
|
||||||
|
if (!window?.core?.api) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const settingsFile = await joinPath(['file://settings', 'settings.json'])
|
||||||
|
if (await fs.existsSync(settingsFile)) {
|
||||||
|
const settings = await fs.readFileSync(settingsFile, 'utf-8')
|
||||||
|
return typeof settings === 'object' ? settings : JSON.parse(settings)
|
||||||
|
}
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
const saveSettings = async ({
|
||||||
|
runMode,
|
||||||
|
notify,
|
||||||
|
}: {
|
||||||
|
runMode?: string | undefined
|
||||||
|
notify?: boolean | undefined
|
||||||
|
}) => {
|
||||||
|
const settingsFile = await joinPath(['file://settings', 'settings.json'])
|
||||||
|
const settings = await readSettings()
|
||||||
|
if (runMode != null) settings.run_mode = runMode
|
||||||
|
if (notify != null) settings.notify = notify
|
||||||
|
await fs.writeFileSync(settingsFile, JSON.stringify(settings))
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
showNotification,
|
||||||
|
isGPUModeEnabled,
|
||||||
|
readSettings,
|
||||||
|
saveSettings,
|
||||||
|
setShowNotification,
|
||||||
|
validateSettings,
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -32,6 +32,7 @@ const nextConfig = {
|
|||||||
JSON.stringify(process.env.ANALYTICS_ID) ?? JSON.stringify('xxx'),
|
JSON.stringify(process.env.ANALYTICS_ID) ?? JSON.stringify('xxx'),
|
||||||
ANALYTICS_HOST:
|
ANALYTICS_HOST:
|
||||||
JSON.stringify(process.env.ANALYTICS_HOST) ?? JSON.stringify('xxx'),
|
JSON.stringify(process.env.ANALYTICS_HOST) ?? JSON.stringify('xxx'),
|
||||||
|
API_BASE_URL: JSON.stringify('http://localhost:1337'),
|
||||||
isMac: process.platform === 'darwin',
|
isMac: process.platform === 'darwin',
|
||||||
isWindows: process.platform === 'win32',
|
isWindows: process.platform === 'win32',
|
||||||
isLinux: process.platform === 'linux',
|
isLinux: process.platform === 'linux',
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import React from 'react'
|
import React, { useContext } from 'react'
|
||||||
|
|
||||||
import { getUserSpace, openFileExplorer, joinPath } from '@janhq/core'
|
import { getUserSpace, openFileExplorer, joinPath } from '@janhq/core'
|
||||||
|
|
||||||
@ -14,6 +14,8 @@ import DropdownListSidebar, {
|
|||||||
selectedModelAtom,
|
selectedModelAtom,
|
||||||
} from '@/containers/DropdownListSidebar'
|
} from '@/containers/DropdownListSidebar'
|
||||||
|
|
||||||
|
import { FeatureToggleContext } from '@/context/FeatureToggle'
|
||||||
|
|
||||||
import { useCreateNewThread } from '@/hooks/useCreateNewThread'
|
import { useCreateNewThread } from '@/hooks/useCreateNewThread'
|
||||||
|
|
||||||
import { toSettingParams } from '@/utils/model_param'
|
import { toSettingParams } from '@/utils/model_param'
|
||||||
@ -35,6 +37,7 @@ const Sidebar: React.FC = () => {
|
|||||||
const selectedModel = useAtomValue(selectedModelAtom)
|
const selectedModel = useAtomValue(selectedModelAtom)
|
||||||
const { updateThreadMetadata } = useCreateNewThread()
|
const { updateThreadMetadata } = useCreateNewThread()
|
||||||
const threadStates = useAtomValue(threadStatesAtom)
|
const threadStates = useAtomValue(threadStatesAtom)
|
||||||
|
const { experimentalFeatureEnabed } = useContext(FeatureToggleContext)
|
||||||
|
|
||||||
const activeModelParams = useAtomValue(getActiveThreadModelParamsAtom)
|
const activeModelParams = useAtomValue(getActiveThreadModelParamsAtom)
|
||||||
const modelSettingParams = toSettingParams(activeModelParams)
|
const modelSettingParams = toSettingParams(activeModelParams)
|
||||||
@ -51,6 +54,7 @@ const Sidebar: React.FC = () => {
|
|||||||
let filePath = undefined
|
let filePath = undefined
|
||||||
const assistantId = activeThread.assistants[0]?.assistant_id
|
const assistantId = activeThread.assistants[0]?.assistant_id
|
||||||
switch (type) {
|
switch (type) {
|
||||||
|
case 'Engine':
|
||||||
case 'Thread':
|
case 'Thread':
|
||||||
filePath = await joinPath(['threads', activeThread.id])
|
filePath = await joinPath(['threads', activeThread.id])
|
||||||
break
|
break
|
||||||
@ -83,6 +87,7 @@ const Sidebar: React.FC = () => {
|
|||||||
let filePath = undefined
|
let filePath = undefined
|
||||||
const assistantId = activeThread.assistants[0]?.assistant_id
|
const assistantId = activeThread.assistants[0]?.assistant_id
|
||||||
switch (type) {
|
switch (type) {
|
||||||
|
case 'Engine':
|
||||||
case 'Thread':
|
case 'Thread':
|
||||||
filePath = await joinPath(['threads', activeThread.id, 'thread.json'])
|
filePath = await joinPath(['threads', activeThread.id, 'thread.json'])
|
||||||
break
|
break
|
||||||
@ -195,7 +200,7 @@ const Sidebar: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardSidebar>
|
</CardSidebar>
|
||||||
{Object.keys(modelSettingParams).length ? (
|
{experimentalFeatureEnabed && Object.keys(modelSettingParams).length ? (
|
||||||
<CardSidebar
|
<CardSidebar
|
||||||
title="Engine"
|
title="Engine"
|
||||||
onRevealInFinderClick={onReviewInFinderClick}
|
onRevealInFinderClick={onReviewInFinderClick}
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import { Button, Textarea } from '@janhq/uikit'
|
|||||||
|
|
||||||
import { useAtom, useAtomValue } from 'jotai'
|
import { useAtom, useAtomValue } from 'jotai'
|
||||||
|
|
||||||
|
import { debounce } from 'lodash'
|
||||||
import { StopCircle } from 'lucide-react'
|
import { StopCircle } from 'lucide-react'
|
||||||
import { twMerge } from 'tailwind-merge'
|
import { twMerge } from 'tailwind-merge'
|
||||||
|
|
||||||
@ -82,16 +83,20 @@ const ChatScreen = () => {
|
|||||||
}
|
}
|
||||||
}, [currentPrompt])
|
}, [currentPrompt])
|
||||||
|
|
||||||
const onKeyDown = async (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
const onKeyDown = debounce(
|
||||||
if (e.key === 'Enter') {
|
async (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||||
if (!e.shiftKey) {
|
if (e.key === 'Enter') {
|
||||||
e.preventDefault()
|
if (!e.shiftKey) {
|
||||||
if (messages[messages.length - 1]?.status !== MessageStatus.Pending)
|
e.preventDefault()
|
||||||
sendChatMessage()
|
if (messages[messages.length - 1]?.status !== MessageStatus.Pending)
|
||||||
else onStopInferenceClick()
|
sendChatMessage()
|
||||||
|
else onStopInferenceClick()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
50,
|
||||||
|
{ leading: false, trailing: true }
|
||||||
|
)
|
||||||
|
|
||||||
const onStopInferenceClick = async () => {
|
const onStopInferenceClick = async () => {
|
||||||
events.emit(EventName.OnInferenceStopped, {})
|
events.emit(EventName.OnInferenceStopped, {})
|
||||||
|
|||||||
@ -1,17 +1,58 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useContext } from 'react'
|
import { useContext, useEffect, useState } from 'react'
|
||||||
|
|
||||||
import { Switch, Button } from '@janhq/uikit'
|
import { Switch, Button } from '@janhq/uikit'
|
||||||
|
|
||||||
import { FeatureToggleContext } from '@/context/FeatureToggle'
|
import { FeatureToggleContext } from '@/context/FeatureToggle'
|
||||||
|
|
||||||
|
import { useSettings } from '@/hooks/useSettings'
|
||||||
|
|
||||||
const Advanced = () => {
|
const Advanced = () => {
|
||||||
const { experimentalFeatureEnabed, setExperimentalFeatureEnabled } =
|
const { experimentalFeatureEnabed, setExperimentalFeatureEnabled } =
|
||||||
useContext(FeatureToggleContext)
|
useContext(FeatureToggleContext)
|
||||||
|
const [gpuEnabled, setGpuEnabled] = useState<boolean>(false)
|
||||||
|
const { readSettings, saveSettings, validateSettings, setShowNotification } =
|
||||||
|
useSettings()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
readSettings().then((settings) => {
|
||||||
|
setGpuEnabled(settings.run_mode === 'gpu')
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="block w-full">
|
<div className="block w-full">
|
||||||
|
{/* CPU / GPU switching */}
|
||||||
|
{!isMac && (
|
||||||
|
<div className="flex w-full items-start justify-between border-b border-border py-4 first:pt-0 last:border-none">
|
||||||
|
<div className="w-4/5 flex-shrink-0 space-y-1.5">
|
||||||
|
<div className="flex gap-x-2">
|
||||||
|
<h6 className="text-sm font-semibold capitalize">NVidia GPU</h6>
|
||||||
|
</div>
|
||||||
|
<p className="whitespace-pre-wrap leading-relaxed">
|
||||||
|
Enable GPU acceleration for NVidia GPUs.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
checked={gpuEnabled}
|
||||||
|
onCheckedChange={(e: boolean) => {
|
||||||
|
if (e === true) {
|
||||||
|
saveSettings({ runMode: 'gpu' })
|
||||||
|
setGpuEnabled(true)
|
||||||
|
setShowNotification(false)
|
||||||
|
setTimeout(() => {
|
||||||
|
validateSettings()
|
||||||
|
}, 300)
|
||||||
|
} else {
|
||||||
|
saveSettings({ runMode: 'cpu' })
|
||||||
|
setGpuEnabled(false)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{/* Experimental */}
|
||||||
<div className="flex w-full items-start justify-between border-b border-border py-4 first:pt-0 last:border-none">
|
<div className="flex w-full items-start justify-between border-b border-border py-4 first:pt-0 last:border-none">
|
||||||
<div className="w-4/5 flex-shrink-0 space-y-1.5">
|
<div className="w-4/5 flex-shrink-0 space-y-1.5">
|
||||||
<div className="flex gap-x-2">
|
<div className="flex gap-x-2">
|
||||||
@ -20,8 +61,7 @@ const Advanced = () => {
|
|||||||
</h6>
|
</h6>
|
||||||
</div>
|
</div>
|
||||||
<p className="whitespace-pre-wrap leading-relaxed">
|
<p className="whitespace-pre-wrap leading-relaxed">
|
||||||
Enable experimental features that may be unstable
|
Enable experimental features that may be unstable tested.
|
||||||
tested.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Switch
|
<Switch
|
||||||
@ -44,7 +84,8 @@ const Advanced = () => {
|
|||||||
</h6>
|
</h6>
|
||||||
</div>
|
</div>
|
||||||
<p className="whitespace-pre-wrap leading-relaxed">
|
<p className="whitespace-pre-wrap leading-relaxed">
|
||||||
Open the directory where your app data, like conversation history and model configurations, is located.
|
Open the directory where your app data, like conversation history
|
||||||
|
and model configurations, is located.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@ -15,7 +15,7 @@ export default function Models() {
|
|||||||
const [searchValue, setsearchValue] = useState('')
|
const [searchValue, setsearchValue] = useState('')
|
||||||
|
|
||||||
const filteredDownloadedModels = downloadedModels.filter((x) => {
|
const filteredDownloadedModels = downloadedModels.filter((x) => {
|
||||||
return x.name.toLowerCase().includes(searchValue.toLowerCase())
|
return x.name?.toLowerCase().includes(searchValue.toLowerCase())
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -1,76 +0,0 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
||||||
import { toast } from 'react-toastify'
|
|
||||||
const API_BASE_PATH: string = '/api/v1'
|
|
||||||
|
|
||||||
export function openExternalUrl(url: string) {
|
|
||||||
window?.open(url, '_blank')
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function appVersion() {
|
|
||||||
return Promise.resolve(VERSION)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function invokeExtensionFunc(
|
|
||||||
modulePath: string,
|
|
||||||
extensionFunc: string,
|
|
||||||
...args: any
|
|
||||||
): Promise<any> {
|
|
||||||
return fetchApi(modulePath, extensionFunc, args).catch((err: Error) => {
|
|
||||||
throw err
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function downloadFile(downloadUrl: string, fileName: string) {
|
|
||||||
return fetchApi('', 'downloadFile', {
|
|
||||||
downloadUrl: downloadUrl,
|
|
||||||
fileName: fileName,
|
|
||||||
}).catch((err: Error) => {
|
|
||||||
throw err
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function deleteFile(fileName: string) {
|
|
||||||
return fetchApi('', 'deleteFile', fileName).catch((err: Error) => {
|
|
||||||
throw err
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function fetchApi(
|
|
||||||
modulePath: string,
|
|
||||||
extensionFunc: string,
|
|
||||||
args: any
|
|
||||||
): Promise<any> {
|
|
||||||
const response = await fetch(API_BASE_PATH + '/invokeFunction', {
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify({
|
|
||||||
modulePath: modulePath,
|
|
||||||
method: extensionFunc,
|
|
||||||
args: args,
|
|
||||||
}),
|
|
||||||
headers: { contentType: 'application/json', Authorization: '' },
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const json = await response.json()
|
|
||||||
if (json && json.error) {
|
|
||||||
toast.error(json.error, {
|
|
||||||
position: 'bottom-left',
|
|
||||||
autoClose: 5000,
|
|
||||||
hideProgressBar: false,
|
|
||||||
closeOnClick: true,
|
|
||||||
pauseOnHover: true,
|
|
||||||
draggable: true,
|
|
||||||
progress: undefined,
|
|
||||||
theme: 'light',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
const text = await response.text()
|
|
||||||
try {
|
|
||||||
const json = JSON.parse(text)
|
|
||||||
return Promise.resolve(json)
|
|
||||||
} catch (err) {
|
|
||||||
return Promise.resolve(text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import * as restAPI from './cloudNativeService'
|
|
||||||
import { EventEmitter } from './eventsService'
|
import { EventEmitter } from './eventsService'
|
||||||
|
import { restAPI } from './restService'
|
||||||
export const setupCoreServices = () => {
|
export const setupCoreServices = () => {
|
||||||
if (typeof window === 'undefined') {
|
if (typeof window === 'undefined') {
|
||||||
console.debug('undefine', window)
|
console.debug('undefine', window)
|
||||||
@ -10,9 +10,7 @@ export const setupCoreServices = () => {
|
|||||||
if (!window.core) {
|
if (!window.core) {
|
||||||
window.core = {
|
window.core = {
|
||||||
events: new EventEmitter(),
|
events: new EventEmitter(),
|
||||||
api: window.electronAPI ?? {
|
api: window.electronAPI ?? restAPI,
|
||||||
...restAPI,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,13 +15,10 @@ export const isCoreExtensionInstalled = () => {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
export const setupBaseExtensions = async () => {
|
export const setupBaseExtensions = async () => {
|
||||||
if (
|
if (typeof window === 'undefined') {
|
||||||
typeof window === 'undefined' ||
|
|
||||||
typeof window.electronAPI === 'undefined'
|
|
||||||
) {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const baseExtensions = await window.electronAPI.baseExtensions()
|
const baseExtensions = await window.core?.api.baseExtensions()
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!extensionManager.get(ExtensionType.Conversational) ||
|
!extensionManager.get(ExtensionType.Conversational) ||
|
||||||
|
|||||||
59
web/services/restService.ts
Normal file
59
web/services/restService.ts
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
import {
|
||||||
|
AppRoute,
|
||||||
|
DownloadRoute,
|
||||||
|
ExtensionRoute,
|
||||||
|
FileSystemRoute,
|
||||||
|
} from '@janhq/core'
|
||||||
|
|
||||||
|
import { safeJsonParse } from '@/utils/json'
|
||||||
|
|
||||||
|
// Function to open an external URL in a new browser window
|
||||||
|
export function openExternalUrl(url: string) {
|
||||||
|
window?.open(url, '_blank')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Async function to get the application version
|
||||||
|
export async function appVersion() {
|
||||||
|
return Promise.resolve(VERSION)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define API routes based on different route types
|
||||||
|
export const APIRoutes = [
|
||||||
|
...Object.values(AppRoute).map((r) => ({ path: 'app', route: r })),
|
||||||
|
...Object.values(DownloadRoute).map((r) => ({ path: `download`, route: r })),
|
||||||
|
...Object.values(ExtensionRoute).map((r) => ({
|
||||||
|
path: `extension`,
|
||||||
|
route: r,
|
||||||
|
})),
|
||||||
|
...Object.values(FileSystemRoute).map((r) => ({ path: `fs`, route: r })),
|
||||||
|
]
|
||||||
|
|
||||||
|
// Define the restAPI object with methods for each API route
|
||||||
|
export const restAPI = {
|
||||||
|
...Object.values(APIRoutes).reduce((acc, proxy) => {
|
||||||
|
return {
|
||||||
|
...acc,
|
||||||
|
[proxy.route]: (...args: any) => {
|
||||||
|
// For each route, define a function that sends a request to the API
|
||||||
|
return fetch(`${API_BASE_URL}/v1/${proxy.path}/${proxy.route}`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(args),
|
||||||
|
headers: { contentType: 'application/json' },
|
||||||
|
}).then(async (res) => {
|
||||||
|
try {
|
||||||
|
if (proxy.path === 'fs') {
|
||||||
|
const text = await res.text()
|
||||||
|
return safeJsonParse(text) ?? text
|
||||||
|
}
|
||||||
|
return await res.json()
|
||||||
|
} catch (err) {
|
||||||
|
console.debug('Op: ', proxy, args, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}, {}),
|
||||||
|
openExternalUrl,
|
||||||
|
appVersion,
|
||||||
|
}
|
||||||
1
web/types/index.d.ts
vendored
1
web/types/index.d.ts
vendored
@ -8,6 +8,7 @@ declare global {
|
|||||||
declare const VERSION: string
|
declare const VERSION: string
|
||||||
declare const ANALYTICS_ID: string
|
declare const ANALYTICS_ID: string
|
||||||
declare const ANALYTICS_HOST: string
|
declare const ANALYTICS_HOST: string
|
||||||
|
declare const API_BASE_URL: string
|
||||||
declare const isMac: boolean
|
declare const isMac: boolean
|
||||||
declare const isWindows: boolean
|
declare const isWindows: boolean
|
||||||
declare const isLinux: boolean
|
declare const isLinux: boolean
|
||||||
|
|||||||
9
web/utils/json.ts
Normal file
9
web/utils/json.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
export const safeJsonParse = <T>(str: string) => {
|
||||||
|
try {
|
||||||
|
const jsonValue: T = JSON.parse(str)
|
||||||
|
|
||||||
|
return jsonValue
|
||||||
|
} catch {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,10 +1,8 @@
|
|||||||
import { basename } from 'path'
|
|
||||||
|
|
||||||
import { Model } from '@janhq/core'
|
import { Model } from '@janhq/core'
|
||||||
|
|
||||||
export const modelBinFileName = (model: Model) => {
|
export const modelBinFileName = (model: Model) => {
|
||||||
const modelFormatExt = '.gguf'
|
const modelFormatExt = '.gguf'
|
||||||
const extractedFileName = basename(model.source_url) ?? model.id
|
const extractedFileName = model.source_url?.split('/').pop() ?? model.id
|
||||||
const fileName = extractedFileName.toLowerCase().endsWith(modelFormatExt)
|
const fileName = extractedFileName.toLowerCase().endsWith(modelFormatExt)
|
||||||
? extractedFileName
|
? extractedFileName
|
||||||
: model.id
|
: model.id
|
||||||
|
|||||||
@ -1,15 +1,36 @@
|
|||||||
import posthog, { Properties } from 'posthog-js'
|
import posthog, { Properties } from 'posthog-js'
|
||||||
|
|
||||||
|
// Initialize PostHog
|
||||||
posthog.init(ANALYTICS_ID, {
|
posthog.init(ANALYTICS_ID, {
|
||||||
api_host: ANALYTICS_HOST,
|
api_host: ANALYTICS_HOST,
|
||||||
autocapture: false,
|
autocapture: false,
|
||||||
|
capture_pageview: false,
|
||||||
|
capture_pageleave: false,
|
||||||
|
rageclick: false,
|
||||||
})
|
})
|
||||||
|
// Export the PostHog instance
|
||||||
export const instance = posthog
|
export const instance = posthog
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// Enum for Analytics Events
|
||||||
export const trackEvent = (name: string, properties?: Properties) => {
|
export enum AnalyticsEvent {
|
||||||
posthog.capture(name, properties)
|
Ping = 'Ping',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum AnalyticsEvent {}
|
// Function to determine the operating system
|
||||||
|
function getOperatingSystem(): string {
|
||||||
|
if (isMac) return 'MacOS'
|
||||||
|
if (isWindows) return 'Windows'
|
||||||
|
if (isLinux) return 'Linux'
|
||||||
|
return 'Unknown'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to capture app version and operating system
|
||||||
|
function captureAppVersionAndOS() {
|
||||||
|
const properties: Properties = {
|
||||||
|
appVersion: VERSION,
|
||||||
|
userOperatingSystem: getOperatingSystem(),
|
||||||
|
}
|
||||||
|
posthog.capture(AnalyticsEvent.Ping, properties)
|
||||||
|
}
|
||||||
|
|
||||||
|
captureAppVersionAndOS()
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user