diff --git a/README.md b/README.md index 2fd2b52..f0737d5 100644 --- a/README.md +++ b/README.md @@ -83,6 +83,28 @@ For Friendly Kobold to work with this fork, [this issue must be fixed first](htt Not all koboldcpp features have currently been ported over to the UI. As a workaround one may use the "Additional arguments" on the "Advanced" tab of the launcher to provide additional command line arguments if you know them. +## CLI Mode + +The `--cli` argument allows you to use the Friendly Kobold binary as a proxy to the downloaded KoboldCpp binary. This enables you to run KoboldCpp from the command line using the same binary that the GUI has downloaded. + +### Considerations + +You might want to run CLI Mode if you're looking to use a different frontend, such as SillyTavern or OpenWebUI, than the ones bundled (eg. KoboldAI Lite, Stable UI) with KoboldCpp AND you're looking to minimize any resource utilization of this app. Note that at the time of this writing, Friendly Kobold takes about ~200MB of RAM and ~100MB of VRAM for its Chromium-based UI. + +### Usage + +```bash +# Basic usage - launch KoboldCpp with no arguments +./friendly-kobold --cli + +# Pass arguments to KoboldCpp +./friendly-kobold --cli --help +./friendly-kobold --cli --port 5001 --model /path/to/model.gguf + +# Any KoboldCpp arguments are supported +./friendly-kobold --cli --model /path/to/model.gguf --port 5001 --host 0.0.0.0 --multiuser 2 +``` + ## For Local Dev ### Prerequisites diff --git a/package.json b/package.json index 6c893db..8fb5270 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "friendly-kobold", "productName": "Friendly Kobold", - "version": "0.6.5", + "version": "0.7.0", "description": "A desktop app for running Large Language Models locally", "main": "out/main/index.js", "homepage": "./", diff --git a/src/main/index.ts b/src/main/index.ts index a485f3f..b2cca89 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -11,6 +11,7 @@ import { GitHubService } from '@/main/services/GitHubService'; import { HardwareService } from '@/main/services/HardwareService'; import { BinaryService } from '@/main/services/BinaryService'; import { IPCHandlers } from '@/main/utils/IPCHandlers'; +import { CliHandler } from '@/main/utils/CliHandler'; import { APP_NAME, CONFIG_FILE_NAME } from '@/constants'; class FriendlyKoboldApp { @@ -146,8 +147,25 @@ class FriendlyKoboldApp { } } -const friendlyKoboldApp = new FriendlyKoboldApp(); -friendlyKoboldApp.initialize().catch((error) => { - // eslint-disable-next-line no-console - console.error('Failed to initialize FriendlyKobold:', error); -}); +const { isCliMode, args } = CliHandler.parseArguments(process.argv); + +if (isCliMode) { + async function runCliMode() { + try { + const cliHandler = new CliHandler(); + await cliHandler.handleCliMode(args); + } catch (error) { + // eslint-disable-next-line no-console + console.error('CLI mode error:', error); + process.exit(1); + } + } + + runCliMode(); +} else { + const friendlyKoboldApp = new FriendlyKoboldApp(); + friendlyKoboldApp.initialize().catch((error) => { + // eslint-disable-next-line no-console + console.error('Failed to initialize FriendlyKobold:', error); + }); +} diff --git a/src/main/utils/CliHandler.ts b/src/main/utils/CliHandler.ts new file mode 100644 index 0000000..db536a2 --- /dev/null +++ b/src/main/utils/CliHandler.ts @@ -0,0 +1,102 @@ +import { spawn } from 'child_process'; +import { existsSync } from 'fs'; +import { app } from 'electron'; +import { join } from 'path'; + +import { ConfigManager } from '@/main/managers/ConfigManager'; +import { LogManager } from '@/main/managers/LogManager'; +import { CONFIG_FILE_NAME } from '@/constants'; + +export class CliHandler { + private configManager: ConfigManager; + private logManager: LogManager; + + constructor() { + this.logManager = new LogManager(); + this.configManager = new ConfigManager( + this.getConfigPath(), + this.logManager + ); + } + + private getConfigPath(): string { + return join(app.getPath('userData'), CONFIG_FILE_NAME); + } + + async handleCliMode(args: string[]): Promise { + const currentBinary = this.configManager.getCurrentKoboldBinary(); + + if (!currentBinary) { + // eslint-disable-next-line no-console + console.error( + 'Error: No KoboldCpp binary found. Please run the GUI first to download KoboldCpp.' + ); + process.exit(1); + } + + if (!existsSync(currentBinary)) { + // eslint-disable-next-line no-console + console.error(`Error: KoboldCpp binary not found at: ${currentBinary}`); + // eslint-disable-next-line no-console + console.error('Please run the GUI to download or reconfigure KoboldCpp.'); + process.exit(1); + } + + // eslint-disable-next-line no-console + console.log(`Launching KoboldCpp: ${currentBinary}`); + // eslint-disable-next-line no-console + console.log(`Arguments: ${args.join(' ')}`); + // eslint-disable-next-line no-console + console.log('─'.repeat(60)); + + return new Promise((resolve, reject) => { + const child = spawn(currentBinary, args, { + stdio: 'inherit', + detached: false, + }); + + child.on('exit', (code, signal) => { + if (signal) { + // eslint-disable-next-line no-console + console.log(`\nProcess terminated with signal: ${signal}`); + process.exit(128 + (signal === 'SIGTERM' ? 15 : 2)); + } else if (code !== null) { + process.exit(code); + } else { + resolve(); + } + }); + + child.on('error', (error) => { + // eslint-disable-next-line no-console + console.error(`Failed to start KoboldCpp: ${error.message}`); + reject(error); + }); + + const handleSignal = () => { + // eslint-disable-next-line no-console + console.log('\nReceived termination signal, terminating KoboldCpp...'); + if (!child.killed) { + child.kill('SIGTERM'); + } + }; + + process.on('SIGINT', handleSignal); + process.on('SIGTERM', handleSignal); + }); + } + + static parseArguments(argv: string[]): { + isCliMode: boolean; + args: string[]; + } { + const cliIndex = argv.indexOf('--cli'); + + if (cliIndex === -1) { + return { isCliMode: false, args: [] }; + } + + const koboldArgs = argv.slice(cliIndex + 1); + return { isCliMode: true, args: koboldArgs }; + } +}