@kernel.chat/kbot 3.73.2 → 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/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
  </p>
7
7
 
8
8
  <p align="center">
9
- <img src="tools/video-assets/demo.gif" alt="kbot demo" width="700">
9
+ <img src="https://raw.githubusercontent.com/isaacsight/kernel/main/tools/video-assets/demo.gif" alt="kbot demo" width="700">
10
10
  </p>
11
11
 
12
12
  <p align="center">
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.https,
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,2 @@
1
+ export declare function registerGhostTools(): void;
2
+ //# sourceMappingURL=ghost.d.ts.map
@@ -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
@@ -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.73.2",
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": {