new CLI mode to proxy terminal commands to the kcpp binary

This commit is contained in:
Egor 2025-08-24 14:09:21 -07:00
parent 835eba34e1
commit a24ec72088
4 changed files with 148 additions and 6 deletions

View file

@ -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

View file

@ -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": "./",

View file

@ -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);
});
}

View file

@ -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<void> {
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<void>((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 };
}
}