@kernel.chat/kbot 3.73.3 → 3.74.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +207 -4
- package/dist/tools/ghost.d.ts +2 -0
- package/dist/tools/ghost.js +713 -0
- package/dist/tools/index.js +1 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1975,9 +1975,7 @@ async function main() {
|
|
|
1975
1975
|
port: parseInt(opts.port, 10),
|
|
1976
1976
|
token: opts.token,
|
|
1977
1977
|
computerUse: opts.computerUse,
|
|
1978
|
-
https: opts.
|
|
1979
|
-
cert: opts.cert,
|
|
1980
|
-
key: opts.key,
|
|
1978
|
+
...(opts.https ? { https: true, cert: opts.cert, key: opts.key } : {}),
|
|
1981
1979
|
});
|
|
1982
1980
|
});
|
|
1983
1981
|
program
|
|
@@ -3638,6 +3636,211 @@ async function main() {
|
|
|
3638
3636
|
buddyCmd.action(() => {
|
|
3639
3637
|
buddyCmd.commands.find(c => c.name() === 'status')?.parse(['', '', 'status']);
|
|
3640
3638
|
});
|
|
3639
|
+
// ── Ghost Commands ──
|
|
3640
|
+
const pikaCmd = program
|
|
3641
|
+
.command('ghost')
|
|
3642
|
+
.description('Ghost — AI video meeting bots, avatars, voice cloning');
|
|
3643
|
+
pikaCmd
|
|
3644
|
+
.command('install')
|
|
3645
|
+
.description('Install Ghost (clones repo, installs Python dependencies)')
|
|
3646
|
+
.option('-f, --force', 'Force re-clone even if already installed')
|
|
3647
|
+
.action(async (opts) => {
|
|
3648
|
+
const { registerGhostTools } = await import('./tools/ghost.js');
|
|
3649
|
+
const { executeTool: execTool } = await import('./tools/index.js');
|
|
3650
|
+
registerGhostTools();
|
|
3651
|
+
printInfo('Installing Ghost...');
|
|
3652
|
+
const result = await execTool({ id: 'cli', name: 'pika_install', arguments: { force: opts.force ?? false } });
|
|
3653
|
+
try {
|
|
3654
|
+
const data = JSON.parse(result.result);
|
|
3655
|
+
if (data.success) {
|
|
3656
|
+
printSuccess('Ghost installed successfully!');
|
|
3657
|
+
printInfo(` Path: ${data.installed_at}`);
|
|
3658
|
+
printInfo(` Python: ${data.python?.version || 'unknown'} (${data.python?.path || 'unknown'})`);
|
|
3659
|
+
printInfo(` API Key: ${data.pika_dev_key}`);
|
|
3660
|
+
printInfo(` Deps: ${data.pip_dependencies}`);
|
|
3661
|
+
printInfo(` ffmpeg: ${data.ffmpeg}`);
|
|
3662
|
+
printInfo(` Skills: ${data.skills_found} found`);
|
|
3663
|
+
if (data.skills?.length > 0) {
|
|
3664
|
+
for (const s of data.skills) {
|
|
3665
|
+
printInfo(` - ${s.name}: ${s.description || '(no description)'}`);
|
|
3666
|
+
}
|
|
3667
|
+
}
|
|
3668
|
+
}
|
|
3669
|
+
else {
|
|
3670
|
+
printError(`Installation failed: ${data.error}`);
|
|
3671
|
+
if (data.fix)
|
|
3672
|
+
printInfo(` Fix: ${data.fix}`);
|
|
3673
|
+
}
|
|
3674
|
+
}
|
|
3675
|
+
catch {
|
|
3676
|
+
console.log(result.result);
|
|
3677
|
+
}
|
|
3678
|
+
process.exit(result.error ? 1 : 0);
|
|
3679
|
+
});
|
|
3680
|
+
pikaCmd
|
|
3681
|
+
.command('join <meet-url>')
|
|
3682
|
+
.description('Join a Google Meet call with an AI avatar bot')
|
|
3683
|
+
.option('-n, --name <name>', 'Bot name (default: "kbot Assistant")')
|
|
3684
|
+
.option('-a, --avatar <path>', 'Avatar image path')
|
|
3685
|
+
.option('-v, --voice <voice-id>', 'Voice ID from ghost voice clone')
|
|
3686
|
+
.option('-p, --prompt <text>', 'System prompt for bot behavior')
|
|
3687
|
+
.action(async (meetUrl, opts) => {
|
|
3688
|
+
const { registerGhostTools } = await import('./tools/ghost.js');
|
|
3689
|
+
const { executeTool: execTool } = await import('./tools/index.js');
|
|
3690
|
+
registerGhostTools();
|
|
3691
|
+
printInfo(`Joining meeting: ${meetUrl}`);
|
|
3692
|
+
const result = await execTool({
|
|
3693
|
+
id: 'cli',
|
|
3694
|
+
name: 'pika_meeting_join',
|
|
3695
|
+
arguments: {
|
|
3696
|
+
meet_url: meetUrl,
|
|
3697
|
+
bot_name: opts.name,
|
|
3698
|
+
avatar: opts.avatar,
|
|
3699
|
+
voice_id: opts.voice,
|
|
3700
|
+
system_prompt: opts.prompt,
|
|
3701
|
+
},
|
|
3702
|
+
});
|
|
3703
|
+
try {
|
|
3704
|
+
const data = JSON.parse(result.result);
|
|
3705
|
+
if (data.success) {
|
|
3706
|
+
printSuccess(`Bot "${data.bot_name}" joining ${data.meet_url}`);
|
|
3707
|
+
printInfo(` Session ID: ${data.session_id}`);
|
|
3708
|
+
printInfo(` To leave: kbot ghost leave ${data.session_id}`);
|
|
3709
|
+
}
|
|
3710
|
+
else {
|
|
3711
|
+
printError(`Failed to join: ${data.error}`);
|
|
3712
|
+
}
|
|
3713
|
+
}
|
|
3714
|
+
catch {
|
|
3715
|
+
console.log(result.result);
|
|
3716
|
+
}
|
|
3717
|
+
process.exit(result.error ? 1 : 0);
|
|
3718
|
+
});
|
|
3719
|
+
pikaCmd
|
|
3720
|
+
.command('leave <session-id>')
|
|
3721
|
+
.description('Leave an active Pika meeting session')
|
|
3722
|
+
.action(async (sessionId) => {
|
|
3723
|
+
const { registerGhostTools } = await import('./tools/ghost.js');
|
|
3724
|
+
const { executeTool: execTool } = await import('./tools/index.js');
|
|
3725
|
+
registerGhostTools();
|
|
3726
|
+
printInfo(`Leaving session: ${sessionId}`);
|
|
3727
|
+
const result = await execTool({
|
|
3728
|
+
id: 'cli',
|
|
3729
|
+
name: 'pika_meeting_leave',
|
|
3730
|
+
arguments: { session_id: sessionId },
|
|
3731
|
+
});
|
|
3732
|
+
try {
|
|
3733
|
+
const data = JSON.parse(result.result);
|
|
3734
|
+
if (data.success) {
|
|
3735
|
+
printSuccess(data.message);
|
|
3736
|
+
}
|
|
3737
|
+
else {
|
|
3738
|
+
printError(`Failed to leave: ${data.error}`);
|
|
3739
|
+
}
|
|
3740
|
+
}
|
|
3741
|
+
catch {
|
|
3742
|
+
console.log(result.result);
|
|
3743
|
+
}
|
|
3744
|
+
process.exit(result.error ? 1 : 0);
|
|
3745
|
+
});
|
|
3746
|
+
pikaCmd
|
|
3747
|
+
.command('avatar <prompt>')
|
|
3748
|
+
.description('Generate an AI avatar image for meetings')
|
|
3749
|
+
.option('-o, --output <path>', 'Output file path')
|
|
3750
|
+
.action(async (prompt, opts) => {
|
|
3751
|
+
const { registerGhostTools } = await import('./tools/ghost.js');
|
|
3752
|
+
const { executeTool: execTool } = await import('./tools/index.js');
|
|
3753
|
+
registerGhostTools();
|
|
3754
|
+
printInfo(`Generating avatar: "${prompt}"`);
|
|
3755
|
+
const result = await execTool({
|
|
3756
|
+
id: 'cli',
|
|
3757
|
+
name: 'pika_generate_avatar',
|
|
3758
|
+
arguments: { prompt, output_path: opts.output },
|
|
3759
|
+
});
|
|
3760
|
+
try {
|
|
3761
|
+
const data = JSON.parse(result.result);
|
|
3762
|
+
if (data.success) {
|
|
3763
|
+
printSuccess('Avatar generated!');
|
|
3764
|
+
printInfo(` Path: ${data.avatar_path}`);
|
|
3765
|
+
printInfo(` Use with: kbot ghost join <meet-url> --avatar ${data.avatar_path}`);
|
|
3766
|
+
}
|
|
3767
|
+
else {
|
|
3768
|
+
printError(`Avatar generation failed: ${data.error}`);
|
|
3769
|
+
}
|
|
3770
|
+
}
|
|
3771
|
+
catch {
|
|
3772
|
+
console.log(result.result);
|
|
3773
|
+
}
|
|
3774
|
+
process.exit(result.error ? 1 : 0);
|
|
3775
|
+
});
|
|
3776
|
+
pikaCmd
|
|
3777
|
+
.command('voice <audio-file>')
|
|
3778
|
+
.description('Clone a voice from an audio file')
|
|
3779
|
+
.option('--noise-reduction', 'Apply noise reduction (requires ffmpeg)')
|
|
3780
|
+
.action(async (audioFile, opts) => {
|
|
3781
|
+
const { registerGhostTools } = await import('./tools/ghost.js');
|
|
3782
|
+
const { executeTool: execTool } = await import('./tools/index.js');
|
|
3783
|
+
registerGhostTools();
|
|
3784
|
+
printInfo(`Cloning voice from: ${audioFile}`);
|
|
3785
|
+
const result = await execTool({
|
|
3786
|
+
id: 'cli',
|
|
3787
|
+
name: 'pika_clone_voice',
|
|
3788
|
+
arguments: { audio_path: audioFile, noise_reduction: opts.noiseReduction ?? false },
|
|
3789
|
+
});
|
|
3790
|
+
try {
|
|
3791
|
+
const data = JSON.parse(result.result);
|
|
3792
|
+
if (data.success) {
|
|
3793
|
+
printSuccess('Voice cloned!');
|
|
3794
|
+
printInfo(` Voice ID: ${data.voice_id}`);
|
|
3795
|
+
printInfo(` Use with: kbot ghost join <meet-url> --voice ${data.voice_id}`);
|
|
3796
|
+
}
|
|
3797
|
+
else {
|
|
3798
|
+
printError(`Voice cloning failed: ${data.error}`);
|
|
3799
|
+
}
|
|
3800
|
+
}
|
|
3801
|
+
catch {
|
|
3802
|
+
console.log(result.result);
|
|
3803
|
+
}
|
|
3804
|
+
process.exit(result.error ? 1 : 0);
|
|
3805
|
+
});
|
|
3806
|
+
pikaCmd
|
|
3807
|
+
.command('status')
|
|
3808
|
+
.description('Check Ghost installation status')
|
|
3809
|
+
.action(async () => {
|
|
3810
|
+
const { registerGhostTools } = await import('./tools/ghost.js');
|
|
3811
|
+
const { executeTool: execTool } = await import('./tools/index.js');
|
|
3812
|
+
registerGhostTools();
|
|
3813
|
+
const result = await execTool({ id: 'cli', name: 'pika_status', arguments: {} });
|
|
3814
|
+
try {
|
|
3815
|
+
const data = JSON.parse(result.result);
|
|
3816
|
+
console.log();
|
|
3817
|
+
console.log(` ${chalk.bold('Ghost Status')}`);
|
|
3818
|
+
console.log(` ${chalk.dim('─'.repeat(40))}`);
|
|
3819
|
+
console.log(` Installed: ${data.installed ? chalk.green('yes') : chalk.red('no')}`);
|
|
3820
|
+
console.log(` Path: ${data.install_path}`);
|
|
3821
|
+
console.log(` Python: ${data.python?.status === 'ok' ? chalk.green(`${data.python.version}`) : chalk.red(data.python?.fix || 'not found')}`);
|
|
3822
|
+
console.log(` PIKA_DEV_KEY: ${data.pika_dev_key?.status === 'configured' ? chalk.green(`${data.pika_dev_key.preview}`) : chalk.red(data.pika_dev_key?.fix || 'not set')}`);
|
|
3823
|
+
console.log(` ffmpeg: ${data.ffmpeg === 'available' ? chalk.green('available') : chalk.yellow(data.ffmpeg)}`);
|
|
3824
|
+
console.log(` Meeting Skill: ${data.meeting_skill === 'ready' ? chalk.green('ready') : chalk.yellow(data.meeting_skill)}`);
|
|
3825
|
+
console.log(` Skills: ${data.skills_count}`);
|
|
3826
|
+
if (data.active_sessions?.length > 0) {
|
|
3827
|
+
console.log();
|
|
3828
|
+
console.log(` ${chalk.bold('Active Sessions')}`);
|
|
3829
|
+
console.log(` ${chalk.dim('─'.repeat(40))}`);
|
|
3830
|
+
for (const s of data.active_sessions) {
|
|
3831
|
+
console.log(` ${s.id} — ${s.meetUrl} (started ${s.startedAt})`);
|
|
3832
|
+
}
|
|
3833
|
+
}
|
|
3834
|
+
console.log();
|
|
3835
|
+
}
|
|
3836
|
+
catch {
|
|
3837
|
+
console.log(result.result);
|
|
3838
|
+
}
|
|
3839
|
+
process.exit(0);
|
|
3840
|
+
});
|
|
3841
|
+
pikaCmd.action(() => {
|
|
3842
|
+
pikaCmd.commands.find(c => c.name() === 'status')?.parse(['', '', 'status']);
|
|
3843
|
+
});
|
|
3641
3844
|
program.parse(process.argv);
|
|
3642
3845
|
const opts = program.opts();
|
|
3643
3846
|
const promptArgs = program.args;
|
|
@@ -3645,7 +3848,7 @@ async function main() {
|
|
|
3645
3848
|
if (opts.quiet)
|
|
3646
3849
|
setQuiet(true);
|
|
3647
3850
|
// If a sub-command was run, we're done
|
|
3648
|
-
if (['byok', 'auth', 'ide', 'local', 'ollama', 'kbot-local', 'pull', 'doctor', 'serve', 'agents', 'watch', 'voice', 'export', 'plugins', 'changelog', 'release', 'completions', 'automate', 'status', 'spec', 'a2a', 'init', 'email-agent', 'imessage-agent', 'consultation', 'observe', 'discovery', 'bench', 'lab', 'teach', 'sessions', 'admin', 'monitor', 'analytics', 'deploy', 'env', 'db', 'dream'].includes(program.args[0]))
|
|
3851
|
+
if (['byok', 'auth', 'ide', 'local', 'ollama', 'kbot-local', 'pull', 'doctor', 'serve', 'agents', 'watch', 'voice', 'export', 'plugins', 'changelog', 'release', 'completions', 'automate', 'status', 'spec', 'a2a', 'init', 'email-agent', 'imessage-agent', 'consultation', 'observe', 'discovery', 'bench', 'lab', 'teach', 'sessions', 'admin', 'monitor', 'analytics', 'deploy', 'env', 'db', 'dream', 'ghost'].includes(program.args[0]))
|
|
3649
3852
|
return;
|
|
3650
3853
|
// ── Ollama Launch Integration ──
|
|
3651
3854
|
// Detect when kbot is started via `ollama launch kbot` or `kbot --ollama-launch`
|
|
@@ -0,0 +1,713 @@
|
|
|
1
|
+
// kbot Pika Skills Integration — AI video meeting bots, avatars, voice cloning
|
|
2
|
+
//
|
|
3
|
+
// Wraps the Pika-Labs/Pika-Skills Python toolkit so kbot users can:
|
|
4
|
+
// - Install the Pika Skills repo to ~/.kbot/pika-skills/
|
|
5
|
+
// - Join Google Meet calls with an AI avatar bot
|
|
6
|
+
// - Leave active meetings
|
|
7
|
+
// - Generate AI avatar images
|
|
8
|
+
// - Clone voices from audio files
|
|
9
|
+
// - Check installation status and list available skills
|
|
10
|
+
//
|
|
11
|
+
// Requires: Python 3.10+, PIKA_DEV_KEY env var, optional ffmpeg
|
|
12
|
+
// All tools are tier: 'free' — Pika's API handles its own billing.
|
|
13
|
+
import { registerTool } from './index.js';
|
|
14
|
+
import { execSync, spawn } from 'node:child_process';
|
|
15
|
+
import { existsSync, readFileSync, readdirSync, mkdirSync } from 'node:fs';
|
|
16
|
+
import { join } from 'node:path';
|
|
17
|
+
import { homedir } from 'node:os';
|
|
18
|
+
const PIKA_DIR = join(homedir(), '.kbot', 'pika-skills');
|
|
19
|
+
const MEETING_SKILL_DIR = join(PIKA_DIR, 'pikastream-video-meeting');
|
|
20
|
+
const REPO_URL = 'https://github.com/Pika-Labs/Pika-Skills.git';
|
|
21
|
+
// Track active meeting sessions (pid-based, lives in memory for the kbot process)
|
|
22
|
+
const activeSessions = new Map();
|
|
23
|
+
// ── Helpers ─────────────────────────────────────────────────────────────
|
|
24
|
+
/** Check if Python 3.10+ is available */
|
|
25
|
+
function checkPython() {
|
|
26
|
+
for (const bin of ['python3', 'python']) {
|
|
27
|
+
try {
|
|
28
|
+
const raw = execSync(`${bin} --version 2>&1`, { encoding: 'utf-8', timeout: 5000 }).trim();
|
|
29
|
+
const match = raw.match(/Python\s+(\d+)\.(\d+)\.(\d+)/);
|
|
30
|
+
if (match) {
|
|
31
|
+
const major = parseInt(match[1], 10);
|
|
32
|
+
const minor = parseInt(match[2], 10);
|
|
33
|
+
if (major >= 3 && minor >= 10) {
|
|
34
|
+
const fullPath = execSync(`which ${bin}`, { encoding: 'utf-8', timeout: 5000 }).trim();
|
|
35
|
+
return { ok: true, version: `${major}.${minor}.${match[3]}`, path: fullPath };
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
catch { /* try next binary */ }
|
|
40
|
+
}
|
|
41
|
+
return { ok: false, version: '', path: '' };
|
|
42
|
+
}
|
|
43
|
+
/** Get the PIKA_DEV_KEY from env or kbot config */
|
|
44
|
+
function getPikaKey() {
|
|
45
|
+
// Check environment variable first
|
|
46
|
+
if (process.env.PIKA_DEV_KEY)
|
|
47
|
+
return process.env.PIKA_DEV_KEY;
|
|
48
|
+
// Check ~/.kbot/config.json (stored alongside other API keys)
|
|
49
|
+
try {
|
|
50
|
+
const configPath = join(homedir(), '.kbot', 'config.json');
|
|
51
|
+
if (existsSync(configPath)) {
|
|
52
|
+
const config = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
53
|
+
if (config.pika_dev_key)
|
|
54
|
+
return config.pika_dev_key;
|
|
55
|
+
if (config.PIKA_DEV_KEY)
|
|
56
|
+
return config.PIKA_DEV_KEY;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
catch { /* not found */ }
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
/** Check if ffmpeg is available */
|
|
63
|
+
function checkFfmpeg() {
|
|
64
|
+
try {
|
|
65
|
+
execSync('ffmpeg -version', { encoding: 'utf-8', timeout: 5000, stdio: 'pipe' });
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/** Check if the Pika Skills repo is cloned and ready */
|
|
73
|
+
function isInstalled() {
|
|
74
|
+
return existsSync(PIKA_DIR) && existsSync(join(PIKA_DIR, '.git'));
|
|
75
|
+
}
|
|
76
|
+
/** Check if the video meeting skill specifically is installed */
|
|
77
|
+
function isMeetingSkillInstalled() {
|
|
78
|
+
return existsSync(MEETING_SKILL_DIR) && existsSync(join(MEETING_SKILL_DIR, 'SKILL.md'));
|
|
79
|
+
}
|
|
80
|
+
/** Generate a unique session ID */
|
|
81
|
+
function generateSessionId() {
|
|
82
|
+
const ts = Date.now().toString(36);
|
|
83
|
+
const rand = Math.random().toString(36).slice(2, 8);
|
|
84
|
+
return `pika-${ts}-${rand}`;
|
|
85
|
+
}
|
|
86
|
+
/** Run a Python script in the meeting skill directory */
|
|
87
|
+
function runPythonScript(scriptName, args, opts) {
|
|
88
|
+
const python = checkPython();
|
|
89
|
+
if (!python.ok) {
|
|
90
|
+
throw new Error('Python 3.10+ is required but not found. Install from https://python.org');
|
|
91
|
+
}
|
|
92
|
+
const scriptPath = join(MEETING_SKILL_DIR, 'scripts', scriptName);
|
|
93
|
+
if (!existsSync(scriptPath)) {
|
|
94
|
+
throw new Error(`Script not found: ${scriptPath}. Run ghost_install first.`);
|
|
95
|
+
}
|
|
96
|
+
const pikaKey = getPikaKey();
|
|
97
|
+
const env = { ...process.env };
|
|
98
|
+
if (pikaKey)
|
|
99
|
+
env.PIKA_DEV_KEY = pikaKey;
|
|
100
|
+
const timeout = opts?.timeout ?? 60_000;
|
|
101
|
+
const result = execSync(`${python.path} ${scriptPath} ${args.map(a => `"${a.replace(/"/g, '\\"')}"`).join(' ')}`, {
|
|
102
|
+
cwd: MEETING_SKILL_DIR,
|
|
103
|
+
encoding: 'utf-8',
|
|
104
|
+
timeout,
|
|
105
|
+
env,
|
|
106
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
107
|
+
});
|
|
108
|
+
return result.trim();
|
|
109
|
+
}
|
|
110
|
+
/** Spawn a long-running Python process (for meeting join which stays alive) */
|
|
111
|
+
function spawnPythonProcess(scriptName, args) {
|
|
112
|
+
const python = checkPython();
|
|
113
|
+
if (!python.ok) {
|
|
114
|
+
throw new Error('Python 3.10+ is required but not found. Install from https://python.org');
|
|
115
|
+
}
|
|
116
|
+
const scriptPath = join(MEETING_SKILL_DIR, 'scripts', scriptName);
|
|
117
|
+
if (!existsSync(scriptPath)) {
|
|
118
|
+
throw new Error(`Script not found: ${scriptPath}. Run ghost_install first.`);
|
|
119
|
+
}
|
|
120
|
+
const pikaKey = getPikaKey();
|
|
121
|
+
const env = { ...process.env };
|
|
122
|
+
if (pikaKey)
|
|
123
|
+
env.PIKA_DEV_KEY = pikaKey;
|
|
124
|
+
const sessionId = generateSessionId();
|
|
125
|
+
const child = spawn(python.path, [scriptPath, ...args], {
|
|
126
|
+
cwd: MEETING_SKILL_DIR,
|
|
127
|
+
env,
|
|
128
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
129
|
+
detached: true,
|
|
130
|
+
});
|
|
131
|
+
// Allow the parent process to exit without waiting for the child
|
|
132
|
+
child.unref();
|
|
133
|
+
return { process: child, sessionId };
|
|
134
|
+
}
|
|
135
|
+
function scanSkills() {
|
|
136
|
+
if (!isInstalled())
|
|
137
|
+
return [];
|
|
138
|
+
const skills = [];
|
|
139
|
+
try {
|
|
140
|
+
const entries = readdirSync(PIKA_DIR, { withFileTypes: true });
|
|
141
|
+
for (const entry of entries) {
|
|
142
|
+
if (!entry.isDirectory())
|
|
143
|
+
continue;
|
|
144
|
+
if (entry.name.startsWith('.'))
|
|
145
|
+
continue; // skip .git etc
|
|
146
|
+
const skillDir = join(PIKA_DIR, entry.name);
|
|
147
|
+
const skillMdPath = join(skillDir, 'SKILL.md');
|
|
148
|
+
const hasSkillMd = existsSync(skillMdPath);
|
|
149
|
+
let description = '';
|
|
150
|
+
if (hasSkillMd) {
|
|
151
|
+
try {
|
|
152
|
+
const content = readFileSync(skillMdPath, 'utf-8');
|
|
153
|
+
// Extract first non-empty, non-heading line as description
|
|
154
|
+
const lines = content.split('\n');
|
|
155
|
+
for (const line of lines) {
|
|
156
|
+
const trimmed = line.trim();
|
|
157
|
+
if (trimmed && !trimmed.startsWith('#') && !trimmed.startsWith('---')) {
|
|
158
|
+
description = trimmed.slice(0, 120);
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
catch { /* skip */ }
|
|
164
|
+
}
|
|
165
|
+
skills.push({
|
|
166
|
+
name: entry.name,
|
|
167
|
+
path: skillDir,
|
|
168
|
+
hasSkillMd,
|
|
169
|
+
description,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
catch { /* dir unreadable */ }
|
|
174
|
+
return skills;
|
|
175
|
+
}
|
|
176
|
+
// ── Tool Registration ───────────────────────────────────────────────────
|
|
177
|
+
export function registerGhostTools() {
|
|
178
|
+
// ── ghost_install ──────────────────────────────────────────────────────
|
|
179
|
+
registerTool({
|
|
180
|
+
name: 'ghost_install',
|
|
181
|
+
description: 'Install Pika Skills — clone the repo to ~/.kbot/pika-skills/, install Python dependencies. Checks for Python 3.10+ and PIKA_DEV_KEY.',
|
|
182
|
+
parameters: {
|
|
183
|
+
force: {
|
|
184
|
+
type: 'boolean',
|
|
185
|
+
description: 'Force re-clone even if already installed',
|
|
186
|
+
required: false,
|
|
187
|
+
default: false,
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
tier: 'free',
|
|
191
|
+
timeout: 300_000, // 5 min for clone + pip install
|
|
192
|
+
async execute(args) {
|
|
193
|
+
const force = args.force === true;
|
|
194
|
+
// 1. Check Python
|
|
195
|
+
const python = checkPython();
|
|
196
|
+
if (!python.ok) {
|
|
197
|
+
return JSON.stringify({
|
|
198
|
+
success: false,
|
|
199
|
+
error: 'Python 3.10+ is required but not found.',
|
|
200
|
+
fix: 'Install Python 3.10+ from https://python.org or via your package manager (brew install python, apt install python3.10)',
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
// 2. Check PIKA_DEV_KEY
|
|
204
|
+
const pikaKey = getPikaKey();
|
|
205
|
+
const keyStatus = pikaKey
|
|
206
|
+
? `configured (${pikaKey.slice(0, 6)}...)`
|
|
207
|
+
: 'NOT SET — set PIKA_DEV_KEY env var or add pika_dev_key to ~/.kbot/config.json';
|
|
208
|
+
// 3. Clone or update
|
|
209
|
+
const kbotDir = join(homedir(), '.kbot');
|
|
210
|
+
if (!existsSync(kbotDir))
|
|
211
|
+
mkdirSync(kbotDir, { recursive: true });
|
|
212
|
+
if (isInstalled() && !force) {
|
|
213
|
+
// Pull latest
|
|
214
|
+
try {
|
|
215
|
+
execSync('git pull --ff-only', { cwd: PIKA_DIR, encoding: 'utf-8', timeout: 30_000, stdio: 'pipe' });
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
// Not critical — repo exists, may be offline
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
// Clone fresh
|
|
223
|
+
if (existsSync(PIKA_DIR)) {
|
|
224
|
+
execSync(`rm -rf "${PIKA_DIR}"`, { encoding: 'utf-8', timeout: 15_000 });
|
|
225
|
+
}
|
|
226
|
+
try {
|
|
227
|
+
execSync(`git clone --depth 1 "${REPO_URL}" "${PIKA_DIR}"`, {
|
|
228
|
+
encoding: 'utf-8',
|
|
229
|
+
timeout: 60_000,
|
|
230
|
+
stdio: 'pipe',
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
catch (err) {
|
|
234
|
+
return JSON.stringify({
|
|
235
|
+
success: false,
|
|
236
|
+
error: `Failed to clone Pika-Skills repo: ${err instanceof Error ? err.message : String(err)}`,
|
|
237
|
+
fix: `Check your internet connection and try again. Repo: ${REPO_URL}`,
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
// 4. Install Python dependencies for the video meeting skill
|
|
242
|
+
let pipStatus = 'skipped';
|
|
243
|
+
if (isMeetingSkillInstalled()) {
|
|
244
|
+
const requirementsPath = join(MEETING_SKILL_DIR, 'requirements.txt');
|
|
245
|
+
if (existsSync(requirementsPath)) {
|
|
246
|
+
try {
|
|
247
|
+
execSync(`${python.path} -m pip install -r "${requirementsPath}" --quiet`, {
|
|
248
|
+
cwd: MEETING_SKILL_DIR,
|
|
249
|
+
encoding: 'utf-8',
|
|
250
|
+
timeout: 120_000,
|
|
251
|
+
stdio: 'pipe',
|
|
252
|
+
});
|
|
253
|
+
pipStatus = 'installed';
|
|
254
|
+
}
|
|
255
|
+
catch (err) {
|
|
256
|
+
pipStatus = `failed: ${err instanceof Error ? err.message : String(err)}`;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
pipStatus = 'no requirements.txt found';
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
// 5. Check ffmpeg
|
|
264
|
+
const hasFfmpeg = checkFfmpeg();
|
|
265
|
+
// 6. Scan available skills
|
|
266
|
+
const skills = scanSkills();
|
|
267
|
+
return JSON.stringify({
|
|
268
|
+
success: true,
|
|
269
|
+
installed_at: PIKA_DIR,
|
|
270
|
+
python: { version: python.version, path: python.path },
|
|
271
|
+
pika_dev_key: keyStatus,
|
|
272
|
+
pip_dependencies: pipStatus,
|
|
273
|
+
ffmpeg: hasFfmpeg ? 'available' : 'not found (optional, needed for voice cloning)',
|
|
274
|
+
skills_found: skills.length,
|
|
275
|
+
skills: skills.map(s => ({ name: s.name, description: s.description })),
|
|
276
|
+
meeting_skill: isMeetingSkillInstalled() ? 'ready' : 'not found in repo',
|
|
277
|
+
});
|
|
278
|
+
},
|
|
279
|
+
});
|
|
280
|
+
// ── ghost_join ─────────────────────────────────────────────────
|
|
281
|
+
registerTool({
|
|
282
|
+
name: 'ghost_join',
|
|
283
|
+
description: 'Join a Google Meet call with a Pika AI avatar bot. The bot can participate in the meeting with custom behavior defined by a system prompt. Returns a session ID for later leaving.',
|
|
284
|
+
parameters: {
|
|
285
|
+
meet_url: {
|
|
286
|
+
type: 'string',
|
|
287
|
+
description: 'Google Meet URL (e.g. https://meet.google.com/abc-defg-hij)',
|
|
288
|
+
required: true,
|
|
289
|
+
},
|
|
290
|
+
bot_name: {
|
|
291
|
+
type: 'string',
|
|
292
|
+
description: 'Name of the bot that appears in the meeting (default: "kbot Assistant")',
|
|
293
|
+
required: false,
|
|
294
|
+
default: 'kbot Assistant',
|
|
295
|
+
},
|
|
296
|
+
avatar: {
|
|
297
|
+
type: 'string',
|
|
298
|
+
description: 'Path to avatar image file, or a previously generated avatar path',
|
|
299
|
+
required: false,
|
|
300
|
+
},
|
|
301
|
+
voice_id: {
|
|
302
|
+
type: 'string',
|
|
303
|
+
description: 'Voice ID from a previous ghost_voice call',
|
|
304
|
+
required: false,
|
|
305
|
+
},
|
|
306
|
+
system_prompt: {
|
|
307
|
+
type: 'string',
|
|
308
|
+
description: 'Instructions for what the bot should say and do in the meeting (e.g. "Take notes and summarize action items")',
|
|
309
|
+
required: false,
|
|
310
|
+
},
|
|
311
|
+
},
|
|
312
|
+
tier: 'free',
|
|
313
|
+
timeout: 120_000,
|
|
314
|
+
async execute(args) {
|
|
315
|
+
// Validate installation
|
|
316
|
+
if (!isMeetingSkillInstalled()) {
|
|
317
|
+
return JSON.stringify({
|
|
318
|
+
success: false,
|
|
319
|
+
error: 'Pika Skills not installed. Run ghost_install first.',
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
// Validate PIKA_DEV_KEY
|
|
323
|
+
const pikaKey = getPikaKey();
|
|
324
|
+
if (!pikaKey) {
|
|
325
|
+
return JSON.stringify({
|
|
326
|
+
success: false,
|
|
327
|
+
error: 'PIKA_DEV_KEY not set. Set the environment variable or add pika_dev_key to ~/.kbot/config.json',
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
const meetUrl = String(args.meet_url || '').trim();
|
|
331
|
+
if (!meetUrl || !meetUrl.includes('meet.google.com')) {
|
|
332
|
+
return JSON.stringify({
|
|
333
|
+
success: false,
|
|
334
|
+
error: 'Invalid meet_url — must be a Google Meet link (e.g. https://meet.google.com/abc-defg-hij)',
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
const botName = String(args.bot_name || 'kbot Assistant');
|
|
338
|
+
const avatar = args.avatar ? String(args.avatar) : undefined;
|
|
339
|
+
const voiceId = args.voice_id ? String(args.voice_id) : undefined;
|
|
340
|
+
const systemPrompt = args.system_prompt ? String(args.system_prompt) : undefined;
|
|
341
|
+
// Build command args
|
|
342
|
+
const scriptArgs = ['--meet-url', meetUrl, '--bot-name', botName];
|
|
343
|
+
if (avatar)
|
|
344
|
+
scriptArgs.push('--avatar', avatar);
|
|
345
|
+
if (voiceId)
|
|
346
|
+
scriptArgs.push('--voice-id', voiceId);
|
|
347
|
+
if (systemPrompt)
|
|
348
|
+
scriptArgs.push('--system-prompt', systemPrompt);
|
|
349
|
+
try {
|
|
350
|
+
const { process: child, sessionId } = spawnPythonProcess('join.py', scriptArgs);
|
|
351
|
+
// Wait briefly for early errors
|
|
352
|
+
let earlyOutput = '';
|
|
353
|
+
let earlyError = '';
|
|
354
|
+
await new Promise((resolve) => {
|
|
355
|
+
const timer = setTimeout(() => resolve(), 5000);
|
|
356
|
+
child.stdout?.on('data', (data) => {
|
|
357
|
+
earlyOutput += data.toString();
|
|
358
|
+
});
|
|
359
|
+
child.stderr?.on('data', (data) => {
|
|
360
|
+
earlyError += data.toString();
|
|
361
|
+
});
|
|
362
|
+
child.on('error', (err) => {
|
|
363
|
+
earlyError += err.message;
|
|
364
|
+
clearTimeout(timer);
|
|
365
|
+
resolve();
|
|
366
|
+
});
|
|
367
|
+
child.on('exit', (code) => {
|
|
368
|
+
if (code !== null && code !== 0) {
|
|
369
|
+
earlyError += `Process exited with code ${code}`;
|
|
370
|
+
}
|
|
371
|
+
clearTimeout(timer);
|
|
372
|
+
resolve();
|
|
373
|
+
});
|
|
374
|
+
});
|
|
375
|
+
// Check for immediate failure
|
|
376
|
+
if (earlyError && !child.pid) {
|
|
377
|
+
return JSON.stringify({
|
|
378
|
+
success: false,
|
|
379
|
+
error: `Failed to start meeting bot: ${earlyError}`,
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
// Track the session
|
|
383
|
+
if (child.pid) {
|
|
384
|
+
activeSessions.set(sessionId, {
|
|
385
|
+
pid: child.pid,
|
|
386
|
+
meetUrl,
|
|
387
|
+
startedAt: new Date().toISOString(),
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
return JSON.stringify({
|
|
391
|
+
success: true,
|
|
392
|
+
session_id: sessionId,
|
|
393
|
+
pid: child.pid,
|
|
394
|
+
meet_url: meetUrl,
|
|
395
|
+
bot_name: botName,
|
|
396
|
+
avatar: avatar ?? 'default',
|
|
397
|
+
voice_id: voiceId ?? 'default',
|
|
398
|
+
system_prompt: systemPrompt ?? 'none',
|
|
399
|
+
early_output: earlyOutput || undefined,
|
|
400
|
+
message: `Bot "${botName}" is joining ${meetUrl}. Use ghost_leave with session_id "${sessionId}" to end the session.`,
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
catch (err) {
|
|
404
|
+
return JSON.stringify({
|
|
405
|
+
success: false,
|
|
406
|
+
error: `Failed to join meeting: ${err instanceof Error ? err.message : String(err)}`,
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
},
|
|
410
|
+
});
|
|
411
|
+
// ── ghost_leave ────────────────────────────────────────────────
|
|
412
|
+
registerTool({
|
|
413
|
+
name: 'ghost_leave',
|
|
414
|
+
description: 'Leave an active Pika meeting session. Terminates the bot process.',
|
|
415
|
+
parameters: {
|
|
416
|
+
session_id: {
|
|
417
|
+
type: 'string',
|
|
418
|
+
description: 'Session ID returned from ghost_join',
|
|
419
|
+
required: true,
|
|
420
|
+
},
|
|
421
|
+
},
|
|
422
|
+
tier: 'free',
|
|
423
|
+
timeout: 30_000,
|
|
424
|
+
async execute(args) {
|
|
425
|
+
const sessionId = String(args.session_id || '').trim();
|
|
426
|
+
if (!sessionId) {
|
|
427
|
+
return JSON.stringify({
|
|
428
|
+
success: false,
|
|
429
|
+
error: 'session_id is required',
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
const session = activeSessions.get(sessionId);
|
|
433
|
+
// Try the leave script first (graceful leave)
|
|
434
|
+
if (isMeetingSkillInstalled()) {
|
|
435
|
+
try {
|
|
436
|
+
const output = runPythonScript('leave.py', ['--session-id', sessionId], { timeout: 15_000 });
|
|
437
|
+
activeSessions.delete(sessionId);
|
|
438
|
+
return JSON.stringify({
|
|
439
|
+
success: true,
|
|
440
|
+
session_id: sessionId,
|
|
441
|
+
message: 'Bot left the meeting gracefully.',
|
|
442
|
+
output,
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
catch {
|
|
446
|
+
// Fall through to process kill
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
// Fallback: kill the process directly
|
|
450
|
+
if (session) {
|
|
451
|
+
try {
|
|
452
|
+
process.kill(session.pid, 'SIGTERM');
|
|
453
|
+
activeSessions.delete(sessionId);
|
|
454
|
+
return JSON.stringify({
|
|
455
|
+
success: true,
|
|
456
|
+
session_id: sessionId,
|
|
457
|
+
message: `Process ${session.pid} terminated. Bot removed from meeting.`,
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
catch (err) {
|
|
461
|
+
activeSessions.delete(sessionId);
|
|
462
|
+
return JSON.stringify({
|
|
463
|
+
success: true,
|
|
464
|
+
session_id: sessionId,
|
|
465
|
+
message: `Session cleaned up. Process may have already exited: ${err instanceof Error ? err.message : String(err)}`,
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
return JSON.stringify({
|
|
470
|
+
success: false,
|
|
471
|
+
error: `Session "${sessionId}" not found. It may have already ended. Active sessions: ${activeSessions.size}`,
|
|
472
|
+
});
|
|
473
|
+
},
|
|
474
|
+
});
|
|
475
|
+
// ── ghost_avatar ──────────────────────────────────────────────
|
|
476
|
+
registerTool({
|
|
477
|
+
name: 'ghost_avatar',
|
|
478
|
+
description: 'Generate an AI avatar image using Pika. Returns the path to the generated image for use in ghost_join.',
|
|
479
|
+
parameters: {
|
|
480
|
+
prompt: {
|
|
481
|
+
type: 'string',
|
|
482
|
+
description: 'Description of the avatar (e.g. "professional woman with short brown hair in a blue blazer")',
|
|
483
|
+
required: true,
|
|
484
|
+
},
|
|
485
|
+
output_path: {
|
|
486
|
+
type: 'string',
|
|
487
|
+
description: 'Optional path to save the avatar image. Defaults to ~/.kbot/pika-skills/avatars/<timestamp>.png',
|
|
488
|
+
required: false,
|
|
489
|
+
},
|
|
490
|
+
},
|
|
491
|
+
tier: 'free',
|
|
492
|
+
timeout: 120_000,
|
|
493
|
+
async execute(args) {
|
|
494
|
+
if (!isMeetingSkillInstalled()) {
|
|
495
|
+
return JSON.stringify({
|
|
496
|
+
success: false,
|
|
497
|
+
error: 'Pika Skills not installed. Run ghost_install first.',
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
const pikaKey = getPikaKey();
|
|
501
|
+
if (!pikaKey) {
|
|
502
|
+
return JSON.stringify({
|
|
503
|
+
success: false,
|
|
504
|
+
error: 'PIKA_DEV_KEY not set. Set the environment variable or add pika_dev_key to ~/.kbot/config.json',
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
const prompt = String(args.prompt || '').trim();
|
|
508
|
+
if (!prompt) {
|
|
509
|
+
return JSON.stringify({ success: false, error: 'prompt is required' });
|
|
510
|
+
}
|
|
511
|
+
const outputPath = args.output_path
|
|
512
|
+
? String(args.output_path)
|
|
513
|
+
: undefined;
|
|
514
|
+
const scriptArgs = ['--prompt', prompt];
|
|
515
|
+
if (outputPath)
|
|
516
|
+
scriptArgs.push('--output', outputPath);
|
|
517
|
+
try {
|
|
518
|
+
const output = runPythonScript('generate-avatar.py', scriptArgs, { timeout: 90_000 });
|
|
519
|
+
// Try to extract the file path from script output
|
|
520
|
+
const pathMatch = output.match(/(?:saved|generated|output|path)[:\s]+(.+\.(?:png|jpg|jpeg|webp))/i);
|
|
521
|
+
const generatedPath = pathMatch ? pathMatch[1].trim() : output.trim();
|
|
522
|
+
return JSON.stringify({
|
|
523
|
+
success: true,
|
|
524
|
+
prompt,
|
|
525
|
+
avatar_path: generatedPath,
|
|
526
|
+
output,
|
|
527
|
+
message: `Avatar generated. Use the avatar_path with ghost_join to join meetings with this avatar.`,
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
catch (err) {
|
|
531
|
+
return JSON.stringify({
|
|
532
|
+
success: false,
|
|
533
|
+
error: `Avatar generation failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
},
|
|
537
|
+
});
|
|
538
|
+
// ── ghost_voice ──────────────────────────────────────────────────
|
|
539
|
+
registerTool({
|
|
540
|
+
name: 'ghost_voice',
|
|
541
|
+
description: 'Clone a voice from an audio file using Pika. Returns a voice_id for use in ghost_join.',
|
|
542
|
+
parameters: {
|
|
543
|
+
audio_path: {
|
|
544
|
+
type: 'string',
|
|
545
|
+
description: 'Path to an audio file (WAV, MP3, etc.) containing the voice to clone',
|
|
546
|
+
required: true,
|
|
547
|
+
},
|
|
548
|
+
noise_reduction: {
|
|
549
|
+
type: 'boolean',
|
|
550
|
+
description: 'Apply noise reduction before cloning (recommended for noisy recordings)',
|
|
551
|
+
required: false,
|
|
552
|
+
default: false,
|
|
553
|
+
},
|
|
554
|
+
},
|
|
555
|
+
tier: 'free',
|
|
556
|
+
timeout: 120_000,
|
|
557
|
+
async execute(args) {
|
|
558
|
+
if (!isMeetingSkillInstalled()) {
|
|
559
|
+
return JSON.stringify({
|
|
560
|
+
success: false,
|
|
561
|
+
error: 'Pika Skills not installed. Run ghost_install first.',
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
const pikaKey = getPikaKey();
|
|
565
|
+
if (!pikaKey) {
|
|
566
|
+
return JSON.stringify({
|
|
567
|
+
success: false,
|
|
568
|
+
error: 'PIKA_DEV_KEY not set. Set the environment variable or add pika_dev_key to ~/.kbot/config.json',
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
const audioPath = String(args.audio_path || '').trim();
|
|
572
|
+
if (!audioPath) {
|
|
573
|
+
return JSON.stringify({ success: false, error: 'audio_path is required' });
|
|
574
|
+
}
|
|
575
|
+
if (!existsSync(audioPath)) {
|
|
576
|
+
return JSON.stringify({
|
|
577
|
+
success: false,
|
|
578
|
+
error: `Audio file not found: ${audioPath}`,
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
const noiseReduction = args.noise_reduction === true;
|
|
582
|
+
// Check ffmpeg if noise reduction requested
|
|
583
|
+
if (noiseReduction && !checkFfmpeg()) {
|
|
584
|
+
return JSON.stringify({
|
|
585
|
+
success: false,
|
|
586
|
+
error: 'Noise reduction requires ffmpeg. Install: brew install ffmpeg (macOS) or apt install ffmpeg (Linux)',
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
const scriptArgs = ['--audio', audioPath];
|
|
590
|
+
if (noiseReduction)
|
|
591
|
+
scriptArgs.push('--noise-reduction');
|
|
592
|
+
try {
|
|
593
|
+
const output = runPythonScript('clone-voice.py', scriptArgs, { timeout: 90_000 });
|
|
594
|
+
// Try to extract voice_id from script output
|
|
595
|
+
const idMatch = output.match(/(?:voice[_-]?id|id)[:\s]+([a-zA-Z0-9_-]+)/i);
|
|
596
|
+
const voiceId = idMatch ? idMatch[1].trim() : output.trim();
|
|
597
|
+
return JSON.stringify({
|
|
598
|
+
success: true,
|
|
599
|
+
voice_id: voiceId,
|
|
600
|
+
audio_path: audioPath,
|
|
601
|
+
noise_reduction: noiseReduction,
|
|
602
|
+
output,
|
|
603
|
+
message: `Voice cloned. Use voice_id "${voiceId}" with ghost_join.`,
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
catch (err) {
|
|
607
|
+
return JSON.stringify({
|
|
608
|
+
success: false,
|
|
609
|
+
error: `Voice cloning failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
},
|
|
613
|
+
});
|
|
614
|
+
// ── ghost_status ───────────────────────────────────────────────────────
|
|
615
|
+
registerTool({
|
|
616
|
+
name: 'ghost_status',
|
|
617
|
+
description: 'Check Pika Skills installation status, API key configuration, Python version, ffmpeg, and active meeting sessions.',
|
|
618
|
+
parameters: {},
|
|
619
|
+
tier: 'free',
|
|
620
|
+
async execute() {
|
|
621
|
+
const python = checkPython();
|
|
622
|
+
const pikaKey = getPikaKey();
|
|
623
|
+
const hasFfmpeg = checkFfmpeg();
|
|
624
|
+
const installed = isInstalled();
|
|
625
|
+
const meetingSkill = isMeetingSkillInstalled();
|
|
626
|
+
const skills = installed ? scanSkills() : [];
|
|
627
|
+
// Check active sessions
|
|
628
|
+
const sessions = [];
|
|
629
|
+
for (const [id, session] of activeSessions) {
|
|
630
|
+
let alive = false;
|
|
631
|
+
try {
|
|
632
|
+
process.kill(session.pid, 0); // Check if process is alive (signal 0)
|
|
633
|
+
alive = true;
|
|
634
|
+
}
|
|
635
|
+
catch {
|
|
636
|
+
activeSessions.delete(id); // Clean up dead sessions
|
|
637
|
+
}
|
|
638
|
+
if (alive) {
|
|
639
|
+
sessions.push({ id, meetUrl: session.meetUrl, startedAt: session.startedAt, alive });
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
return JSON.stringify({
|
|
643
|
+
installed,
|
|
644
|
+
install_path: PIKA_DIR,
|
|
645
|
+
python: python.ok
|
|
646
|
+
? { status: 'ok', version: python.version, path: python.path }
|
|
647
|
+
: { status: 'missing', fix: 'Install Python 3.10+ from https://python.org' },
|
|
648
|
+
pika_dev_key: pikaKey
|
|
649
|
+
? { status: 'configured', preview: `${pikaKey.slice(0, 6)}...` }
|
|
650
|
+
: { status: 'not set', fix: 'Set PIKA_DEV_KEY env var or add pika_dev_key to ~/.kbot/config.json' },
|
|
651
|
+
ffmpeg: hasFfmpeg ? 'available' : 'not found (optional, needed for voice cloning noise reduction)',
|
|
652
|
+
meeting_skill: meetingSkill ? 'ready' : installed ? 'not found in repo' : 'not installed',
|
|
653
|
+
skills_count: skills.length,
|
|
654
|
+
skills: skills.map(s => ({ name: s.name, description: s.description })),
|
|
655
|
+
active_sessions: sessions,
|
|
656
|
+
});
|
|
657
|
+
},
|
|
658
|
+
});
|
|
659
|
+
// ── ghost_skills ──────────────────────────────────────────────────
|
|
660
|
+
registerTool({
|
|
661
|
+
name: 'ghost_skills',
|
|
662
|
+
description: 'List all available Pika Skills installed at ~/.kbot/pika-skills/. Scans for directories containing SKILL.md files.',
|
|
663
|
+
parameters: {
|
|
664
|
+
verbose: {
|
|
665
|
+
type: 'boolean',
|
|
666
|
+
description: 'Include full SKILL.md content for each skill',
|
|
667
|
+
required: false,
|
|
668
|
+
default: false,
|
|
669
|
+
},
|
|
670
|
+
},
|
|
671
|
+
tier: 'free',
|
|
672
|
+
async execute(args) {
|
|
673
|
+
if (!isInstalled()) {
|
|
674
|
+
return JSON.stringify({
|
|
675
|
+
success: false,
|
|
676
|
+
error: 'Pika Skills not installed. Run ghost_install first.',
|
|
677
|
+
install_command: 'Use the ghost_install tool to clone the Pika-Skills repository.',
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
const verbose = args.verbose === true;
|
|
681
|
+
const skills = scanSkills();
|
|
682
|
+
if (skills.length === 0) {
|
|
683
|
+
return JSON.stringify({
|
|
684
|
+
success: true,
|
|
685
|
+
skills: [],
|
|
686
|
+
message: 'No skills found. The Pika-Skills repository may be empty or have a different structure. Try running ghost_install with force=true.',
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
const result = skills.map(s => {
|
|
690
|
+
const entry = {
|
|
691
|
+
name: s.name,
|
|
692
|
+
path: s.path,
|
|
693
|
+
has_skill_md: s.hasSkillMd,
|
|
694
|
+
description: s.description,
|
|
695
|
+
};
|
|
696
|
+
if (verbose && s.hasSkillMd) {
|
|
697
|
+
try {
|
|
698
|
+
entry.skill_md = readFileSync(join(s.path, 'SKILL.md'), 'utf-8');
|
|
699
|
+
}
|
|
700
|
+
catch { /* skip */ }
|
|
701
|
+
}
|
|
702
|
+
return entry;
|
|
703
|
+
});
|
|
704
|
+
return JSON.stringify({
|
|
705
|
+
success: true,
|
|
706
|
+
install_path: PIKA_DIR,
|
|
707
|
+
skills_count: skills.length,
|
|
708
|
+
skills: result,
|
|
709
|
+
});
|
|
710
|
+
},
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
//# sourceMappingURL=ghost.js.map
|
package/dist/tools/index.js
CHANGED
|
@@ -313,6 +313,7 @@ const LAZY_MODULE_IMPORTS = [
|
|
|
313
313
|
{ path: './music-gen.js', registerFn: 'registerMusicGenTools' },
|
|
314
314
|
{ path: './mobile-automation.js', registerFn: 'registerMobileAutomationTools' },
|
|
315
315
|
{ path: './iphone.js', registerFn: 'registerIPhoneTools' },
|
|
316
|
+
{ path: './ghost.js', registerFn: 'registerGhostTools' },
|
|
316
317
|
];
|
|
317
318
|
/** Track whether lazy tools have been registered */
|
|
318
319
|
let lazyToolsRegistered = false;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kernel.chat/kbot",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.74.0",
|
|
4
4
|
"description": "Open-source terminal AI agent. 764+ tools, 35 agents, 20 providers. Dreams, learns, watches your system. Controls your phone. Fully local, fully sovereign. MIT.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|