@kernel.chat/kbot 3.73.3 → 3.82.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 +9 -0
- package/dist/cli.js +241 -4
- package/dist/ide/mcp-server.js +58 -43
- package/dist/tools/ghost.d.ts +2 -0
- package/dist/tools/ghost.js +713 -0
- package/dist/tools/index.js +4 -0
- package/dist/tools/render-engine.d.ts +158 -0
- package/dist/tools/render-engine.js +1361 -0
- package/dist/tools/sprite-engine.d.ts +41 -0
- package/dist/tools/sprite-engine.js +1601 -0
- package/dist/tools/stream-brain.d.ts +70 -0
- package/dist/tools/stream-brain.js +699 -0
- package/dist/tools/stream-character.d.ts +2 -0
- package/dist/tools/stream-character.js +619 -0
- package/dist/tools/stream-intelligence.d.ts +172 -0
- package/dist/tools/stream-intelligence.js +2237 -0
- package/dist/tools/stream-renderer.d.ts +2 -0
- package/dist/tools/stream-renderer.js +3473 -0
- package/dist/tools/streaming.d.ts +2 -0
- package/dist/tools/streaming.js +491 -0
- package/package.json +1 -1
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
// kbot Streaming Tools — Multi-platform livestreaming to Twitch, Rumble, Kick
|
|
2
|
+
//
|
|
3
|
+
// Tools: stream_start, stream_stop, stream_status, stream_setup
|
|
4
|
+
//
|
|
5
|
+
// kbot captures screen/window/webcam via ffmpeg and simultaneously
|
|
6
|
+
// streams to multiple RTMP endpoints. Supports Twitch, Rumble, and Kick.
|
|
7
|
+
//
|
|
8
|
+
// Env: TWITCH_STREAM_KEY, RUMBLE_STREAM_KEY, KICK_STREAM_KEY
|
|
9
|
+
// Or configure via: kbot stream_setup
|
|
10
|
+
import { registerTool } from './index.js';
|
|
11
|
+
import { execFile, spawn } from 'node:child_process';
|
|
12
|
+
import { homedir, platform } from 'node:os';
|
|
13
|
+
import { join } from 'node:path';
|
|
14
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
15
|
+
const KBOT_DIR = join(homedir(), '.kbot');
|
|
16
|
+
const STREAM_STATE = join(KBOT_DIR, 'stream-state.json');
|
|
17
|
+
const STREAM_PID = join(KBOT_DIR, 'stream.pid');
|
|
18
|
+
// ─── RTMP Ingest Endpoints ────────────────────────────────────
|
|
19
|
+
const RTMP_ENDPOINTS = {
|
|
20
|
+
twitch: 'rtmp://live.twitch.tv/app',
|
|
21
|
+
rumble: 'rtmp://rtmp.rumble.com/live',
|
|
22
|
+
kick: 'rtmps://fa723fc1b171.global-contribute.live-video.net/app',
|
|
23
|
+
};
|
|
24
|
+
function loadState() {
|
|
25
|
+
try {
|
|
26
|
+
if (existsSync(STREAM_STATE))
|
|
27
|
+
return JSON.parse(readFileSync(STREAM_STATE, 'utf-8'));
|
|
28
|
+
}
|
|
29
|
+
catch { /* fresh state */ }
|
|
30
|
+
return {
|
|
31
|
+
active: false, platforms: [], startedAt: null,
|
|
32
|
+
source: 'screen', resolution: '1920x1080', bitrate: 4500,
|
|
33
|
+
pid: null, history: [],
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function saveState(state) {
|
|
37
|
+
if (!existsSync(KBOT_DIR))
|
|
38
|
+
mkdirSync(KBOT_DIR, { recursive: true });
|
|
39
|
+
if (state.history.length > 100)
|
|
40
|
+
state.history = state.history.slice(-100);
|
|
41
|
+
writeFileSync(STREAM_STATE, JSON.stringify(state, null, 2));
|
|
42
|
+
}
|
|
43
|
+
// ─── Stream Key Resolution ─────────────────────────────────────
|
|
44
|
+
function getStreamKey(platform) {
|
|
45
|
+
const envMap = {
|
|
46
|
+
twitch: 'TWITCH_STREAM_KEY',
|
|
47
|
+
rumble: 'RUMBLE_STREAM_KEY',
|
|
48
|
+
kick: 'KICK_STREAM_KEY',
|
|
49
|
+
};
|
|
50
|
+
const envVar = envMap[platform];
|
|
51
|
+
if (!envVar)
|
|
52
|
+
return null;
|
|
53
|
+
return process.env[envVar] || null;
|
|
54
|
+
}
|
|
55
|
+
function getConfiguredPlatforms() {
|
|
56
|
+
return Object.keys(RTMP_ENDPOINTS).filter(p => getStreamKey(p));
|
|
57
|
+
}
|
|
58
|
+
// ─── FFmpeg Check ──────────────────────────────────────────────
|
|
59
|
+
function checkFfmpeg() {
|
|
60
|
+
return new Promise(resolve => {
|
|
61
|
+
execFile('ffmpeg', ['-version'], { timeout: 5000 }, (err) => {
|
|
62
|
+
resolve(!err);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
// ─── Screen Capture Input (platform-specific) ──────────────────
|
|
67
|
+
function getInputArgs(source) {
|
|
68
|
+
const os = platform();
|
|
69
|
+
if (source === 'screen') {
|
|
70
|
+
if (os === 'darwin') {
|
|
71
|
+
// macOS: AVFoundation screen capture (device 1 = screen, 0 = webcam typically)
|
|
72
|
+
return ['-f', 'avfoundation', '-framerate', '30', '-i', '1:0'];
|
|
73
|
+
}
|
|
74
|
+
else if (os === 'linux') {
|
|
75
|
+
return ['-f', 'x11grab', '-framerate', '30', '-video_size', '1920x1080', '-i', ':0.0'];
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
// Windows
|
|
79
|
+
return ['-f', 'gdigrab', '-framerate', '30', '-i', 'desktop'];
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (source === 'webcam') {
|
|
83
|
+
if (os === 'darwin') {
|
|
84
|
+
return ['-f', 'avfoundation', '-framerate', '30', '-i', '0:0'];
|
|
85
|
+
}
|
|
86
|
+
else if (os === 'linux') {
|
|
87
|
+
return ['-f', 'v4l2', '-framerate', '30', '-i', '/dev/video0'];
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
return ['-f', 'dshow', '-framerate', '30', '-i', 'video=Integrated Camera'];
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// File input (e.g., a video file or test pattern)
|
|
94
|
+
if (source === 'test') {
|
|
95
|
+
return [
|
|
96
|
+
'-f', 'lavfi', '-i', 'testsrc=size=1920x1080:rate=30',
|
|
97
|
+
'-f', 'lavfi', '-i', 'sine=frequency=440:sample_rate=44100',
|
|
98
|
+
];
|
|
99
|
+
}
|
|
100
|
+
// Assume it's a file path
|
|
101
|
+
return ['-re', '-i', source];
|
|
102
|
+
}
|
|
103
|
+
// ─── Build FFmpeg Command ──────────────────────────────────────
|
|
104
|
+
function buildFfmpegArgs(opts) {
|
|
105
|
+
const { platforms, source, resolution, bitrate } = opts;
|
|
106
|
+
const inputArgs = getInputArgs(source);
|
|
107
|
+
// Video encoding settings
|
|
108
|
+
const videoArgs = [
|
|
109
|
+
'-c:v', 'libx264',
|
|
110
|
+
'-preset', 'veryfast',
|
|
111
|
+
'-b:v', `${bitrate}k`,
|
|
112
|
+
'-maxrate', `${bitrate}k`,
|
|
113
|
+
'-bufsize', `${bitrate * 2}k`,
|
|
114
|
+
'-s', resolution,
|
|
115
|
+
'-g', '60', // keyframe every 2s at 30fps
|
|
116
|
+
'-keyint_min', '60',
|
|
117
|
+
'-pix_fmt', 'yuv420p',
|
|
118
|
+
];
|
|
119
|
+
// Audio encoding
|
|
120
|
+
const audioArgs = [
|
|
121
|
+
'-c:a', 'aac',
|
|
122
|
+
'-b:a', '128k',
|
|
123
|
+
'-ar', '44100',
|
|
124
|
+
];
|
|
125
|
+
// For multi-platform: use tee muxer to send to all RTMP endpoints
|
|
126
|
+
if (platforms.length === 1) {
|
|
127
|
+
const key = getStreamKey(platforms[0]);
|
|
128
|
+
const endpoint = RTMP_ENDPOINTS[platforms[0]];
|
|
129
|
+
return [
|
|
130
|
+
...inputArgs,
|
|
131
|
+
...videoArgs,
|
|
132
|
+
...audioArgs,
|
|
133
|
+
'-f', 'flv',
|
|
134
|
+
`${endpoint}/${key}`,
|
|
135
|
+
];
|
|
136
|
+
}
|
|
137
|
+
// Multiple platforms: use tee muxer
|
|
138
|
+
const teeTargets = platforms.map(p => {
|
|
139
|
+
const key = getStreamKey(p);
|
|
140
|
+
const endpoint = RTMP_ENDPOINTS[p];
|
|
141
|
+
return `[f=flv]${endpoint}/${key}`;
|
|
142
|
+
}).join('|');
|
|
143
|
+
return [
|
|
144
|
+
...inputArgs,
|
|
145
|
+
...videoArgs,
|
|
146
|
+
...audioArgs,
|
|
147
|
+
'-f', 'tee',
|
|
148
|
+
'-map', '0:v',
|
|
149
|
+
'-map', source === 'test' ? '1:a' : '0:a',
|
|
150
|
+
teeTargets,
|
|
151
|
+
];
|
|
152
|
+
}
|
|
153
|
+
// ─── Active Process Tracking ───────────────────────────────────
|
|
154
|
+
let activeProcess = null;
|
|
155
|
+
function isStreamRunning() {
|
|
156
|
+
if (activeProcess && !activeProcess.killed)
|
|
157
|
+
return true;
|
|
158
|
+
// Check PID file for streams started in a different session
|
|
159
|
+
try {
|
|
160
|
+
if (existsSync(STREAM_PID)) {
|
|
161
|
+
const pid = parseInt(readFileSync(STREAM_PID, 'utf-8').trim());
|
|
162
|
+
process.kill(pid, 0); // signal 0 = check if process exists
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
// Process doesn't exist, clean up stale PID
|
|
168
|
+
try {
|
|
169
|
+
writeFileSync(STREAM_PID, '');
|
|
170
|
+
}
|
|
171
|
+
catch { /* ignore */ }
|
|
172
|
+
}
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
// ─── Register Tools ────────────────────────────────────────────
|
|
176
|
+
export function registerStreamingTools() {
|
|
177
|
+
registerTool({
|
|
178
|
+
name: 'stream_start',
|
|
179
|
+
description: 'Start livestreaming to one or more platforms simultaneously. Supports Twitch, Rumble, and Kick. Uses ffmpeg to capture screen/webcam and stream via RTMP.',
|
|
180
|
+
parameters: {
|
|
181
|
+
platforms: { type: 'string', description: 'Comma-separated platforms: "twitch,rumble,kick" or "all". Default: all configured', required: false },
|
|
182
|
+
source: { type: 'string', description: 'Video source: "screen" (default), "webcam", "test" (test pattern), or a file path' },
|
|
183
|
+
resolution: { type: 'string', description: 'Output resolution: "1920x1080" (default), "1280x720", "2560x1440"' },
|
|
184
|
+
bitrate: { type: 'string', description: 'Video bitrate in kbps. Default: 4500. Twitch max: 6000' },
|
|
185
|
+
title: { type: 'string', description: 'Stream title (for logging/state tracking)' },
|
|
186
|
+
},
|
|
187
|
+
tier: 'free',
|
|
188
|
+
timeout: 600_000, // 10 min timeout for the start command itself
|
|
189
|
+
execute: async (args) => {
|
|
190
|
+
// Pre-flight checks
|
|
191
|
+
const hasFfmpeg = await checkFfmpeg();
|
|
192
|
+
if (!hasFfmpeg) {
|
|
193
|
+
return 'Error: ffmpeg not found. Install it:\n macOS: brew install ffmpeg\n Linux: sudo apt install ffmpeg\n Windows: choco install ffmpeg';
|
|
194
|
+
}
|
|
195
|
+
if (isStreamRunning()) {
|
|
196
|
+
return 'Error: A stream is already active. Run stream_stop first.';
|
|
197
|
+
}
|
|
198
|
+
// Resolve platforms
|
|
199
|
+
const configured = getConfiguredPlatforms();
|
|
200
|
+
if (configured.length === 0) {
|
|
201
|
+
return 'Error: No stream keys configured. Set environment variables:\n TWITCH_STREAM_KEY=your_key\n RUMBLE_STREAM_KEY=your_key\n KICK_STREAM_KEY=your_key\n\nOr run stream_setup to configure interactively.';
|
|
202
|
+
}
|
|
203
|
+
let platforms;
|
|
204
|
+
if (args.platforms) {
|
|
205
|
+
const requested = String(args.platforms).toLowerCase();
|
|
206
|
+
if (requested === 'all') {
|
|
207
|
+
platforms = configured;
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
platforms = requested.split(',').map(p => p.trim());
|
|
211
|
+
// Validate
|
|
212
|
+
const missing = platforms.filter(p => !getStreamKey(p));
|
|
213
|
+
if (missing.length > 0) {
|
|
214
|
+
return `Error: Missing stream keys for: ${missing.join(', ')}\nConfigured platforms: ${configured.join(', ')}`;
|
|
215
|
+
}
|
|
216
|
+
const unknown = platforms.filter(p => !RTMP_ENDPOINTS[p]);
|
|
217
|
+
if (unknown.length > 0) {
|
|
218
|
+
return `Error: Unknown platforms: ${unknown.join(', ')}\nSupported: twitch, rumble, kick`;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
platforms = configured;
|
|
224
|
+
}
|
|
225
|
+
const source = String(args.source || 'screen');
|
|
226
|
+
const resolution = String(args.resolution || '1920x1080');
|
|
227
|
+
const bitrate = parseInt(String(args.bitrate || '4500'));
|
|
228
|
+
// Build ffmpeg command
|
|
229
|
+
const ffmpegArgs = buildFfmpegArgs({ platforms, source, resolution, bitrate });
|
|
230
|
+
// Spawn ffmpeg in background
|
|
231
|
+
const proc = spawn('ffmpeg', ffmpegArgs, {
|
|
232
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
233
|
+
detached: true,
|
|
234
|
+
});
|
|
235
|
+
activeProcess = proc;
|
|
236
|
+
const pid = proc.pid;
|
|
237
|
+
// Save PID for cross-session tracking
|
|
238
|
+
writeFileSync(STREAM_PID, String(pid));
|
|
239
|
+
// Collect stderr for initial connection status
|
|
240
|
+
let stderrBuffer = '';
|
|
241
|
+
proc.stderr?.on('data', (chunk) => {
|
|
242
|
+
stderrBuffer += chunk.toString();
|
|
243
|
+
});
|
|
244
|
+
// Wait a moment to check if ffmpeg started successfully
|
|
245
|
+
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
246
|
+
if (proc.exitCode !== null) {
|
|
247
|
+
// Process already exited — something went wrong
|
|
248
|
+
const error = stderrBuffer.slice(-500);
|
|
249
|
+
return `Error: ffmpeg exited immediately.\n\n${error}\n\nCheck your stream keys and source device.`;
|
|
250
|
+
}
|
|
251
|
+
// Detach so kbot can continue
|
|
252
|
+
proc.unref();
|
|
253
|
+
// Update state
|
|
254
|
+
const state = loadState();
|
|
255
|
+
state.active = true;
|
|
256
|
+
state.platforms = platforms;
|
|
257
|
+
state.startedAt = new Date().toISOString();
|
|
258
|
+
state.source = source;
|
|
259
|
+
state.resolution = resolution;
|
|
260
|
+
state.bitrate = bitrate;
|
|
261
|
+
state.pid = pid;
|
|
262
|
+
saveState(state);
|
|
263
|
+
const platformList = platforms.map(p => {
|
|
264
|
+
const urls = {
|
|
265
|
+
twitch: 'https://twitch.tv/your-channel',
|
|
266
|
+
rumble: 'https://rumble.com/your-channel',
|
|
267
|
+
kick: 'https://kick.com/your-channel',
|
|
268
|
+
};
|
|
269
|
+
return ` - ${p.charAt(0).toUpperCase() + p.slice(1)}: ${urls[p] || p}`;
|
|
270
|
+
}).join('\n');
|
|
271
|
+
return `Stream started!\n\nPlatforms:\n${platformList}\n\nSettings:\n Source: ${source}\n Resolution: ${resolution}\n Bitrate: ${bitrate}kbps\n PID: ${pid}\n\nUse stream_status to check health, stream_stop to end.`;
|
|
272
|
+
},
|
|
273
|
+
});
|
|
274
|
+
registerTool({
|
|
275
|
+
name: 'stream_stop',
|
|
276
|
+
description: 'Stop the active livestream. Gracefully terminates ffmpeg and records the session.',
|
|
277
|
+
parameters: {},
|
|
278
|
+
tier: 'free',
|
|
279
|
+
execute: async () => {
|
|
280
|
+
const state = loadState();
|
|
281
|
+
if (!state.active && !isStreamRunning()) {
|
|
282
|
+
return 'No active stream to stop.';
|
|
283
|
+
}
|
|
284
|
+
let stopped = false;
|
|
285
|
+
// Try in-process reference first
|
|
286
|
+
if (activeProcess && !activeProcess.killed) {
|
|
287
|
+
activeProcess.kill('SIGINT'); // graceful shutdown
|
|
288
|
+
activeProcess = null;
|
|
289
|
+
stopped = true;
|
|
290
|
+
}
|
|
291
|
+
// Try PID file
|
|
292
|
+
if (!stopped && state.pid) {
|
|
293
|
+
try {
|
|
294
|
+
process.kill(state.pid, 'SIGINT');
|
|
295
|
+
stopped = true;
|
|
296
|
+
}
|
|
297
|
+
catch {
|
|
298
|
+
// Process already dead
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
// Calculate duration
|
|
302
|
+
let durationMinutes = 0;
|
|
303
|
+
if (state.startedAt) {
|
|
304
|
+
durationMinutes = Math.round((Date.now() - new Date(state.startedAt).getTime()) / 60_000);
|
|
305
|
+
}
|
|
306
|
+
// Record in history
|
|
307
|
+
state.history.push({
|
|
308
|
+
date: state.startedAt || new Date().toISOString(),
|
|
309
|
+
platforms: state.platforms,
|
|
310
|
+
duration_minutes: durationMinutes,
|
|
311
|
+
source: state.source,
|
|
312
|
+
});
|
|
313
|
+
// Reset active state
|
|
314
|
+
state.active = false;
|
|
315
|
+
state.pid = null;
|
|
316
|
+
state.startedAt = null;
|
|
317
|
+
state.platforms = [];
|
|
318
|
+
saveState(state);
|
|
319
|
+
// Clean PID file
|
|
320
|
+
try {
|
|
321
|
+
writeFileSync(STREAM_PID, '');
|
|
322
|
+
}
|
|
323
|
+
catch { /* ignore */ }
|
|
324
|
+
return `Stream stopped after ${durationMinutes} minutes.\n\nSession recorded in history. Run stream_status to see past streams.`;
|
|
325
|
+
},
|
|
326
|
+
});
|
|
327
|
+
registerTool({
|
|
328
|
+
name: 'stream_status',
|
|
329
|
+
description: 'Check livestream status — active stream info, configured platforms, and stream history.',
|
|
330
|
+
parameters: {},
|
|
331
|
+
tier: 'free',
|
|
332
|
+
execute: async () => {
|
|
333
|
+
const state = loadState();
|
|
334
|
+
const configured = getConfiguredPlatforms();
|
|
335
|
+
const running = isStreamRunning();
|
|
336
|
+
const lines = [];
|
|
337
|
+
// Active stream
|
|
338
|
+
if (running && state.active) {
|
|
339
|
+
const elapsed = state.startedAt
|
|
340
|
+
? Math.round((Date.now() - new Date(state.startedAt).getTime()) / 60_000)
|
|
341
|
+
: 0;
|
|
342
|
+
lines.push('🔴 LIVE');
|
|
343
|
+
lines.push(` Platforms: ${state.platforms.join(', ')}`);
|
|
344
|
+
lines.push(` Duration: ${elapsed} minutes`);
|
|
345
|
+
lines.push(` Source: ${state.source}`);
|
|
346
|
+
lines.push(` Resolution: ${state.resolution}`);
|
|
347
|
+
lines.push(` Bitrate: ${state.bitrate}kbps`);
|
|
348
|
+
lines.push(` PID: ${state.pid}`);
|
|
349
|
+
}
|
|
350
|
+
else {
|
|
351
|
+
lines.push('⚫ Offline');
|
|
352
|
+
// Clean up stale state
|
|
353
|
+
if (state.active) {
|
|
354
|
+
state.active = false;
|
|
355
|
+
state.pid = null;
|
|
356
|
+
saveState(state);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
lines.push('');
|
|
360
|
+
lines.push('Configured platforms:');
|
|
361
|
+
for (const p of Object.keys(RTMP_ENDPOINTS)) {
|
|
362
|
+
const has = configured.includes(p);
|
|
363
|
+
lines.push(` ${has ? '✓' : '✗'} ${p.charAt(0).toUpperCase() + p.slice(1)}`);
|
|
364
|
+
}
|
|
365
|
+
// History
|
|
366
|
+
if (state.history.length > 0) {
|
|
367
|
+
lines.push('');
|
|
368
|
+
lines.push(`Stream history (${state.history.length} sessions):`);
|
|
369
|
+
for (const h of state.history.slice(-5).reverse()) {
|
|
370
|
+
lines.push(` ${h.date.split('T')[0]} — ${h.platforms.join(', ')} — ${h.duration_minutes}m — ${h.source}`);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
return lines.join('\n');
|
|
374
|
+
},
|
|
375
|
+
});
|
|
376
|
+
registerTool({
|
|
377
|
+
name: 'stream_setup',
|
|
378
|
+
description: 'Show setup instructions for configuring stream keys for Twitch, Rumble, and Kick.',
|
|
379
|
+
parameters: {
|
|
380
|
+
platform: { type: 'string', description: 'Platform to show setup for: "twitch", "rumble", "kick", or "all" (default)' },
|
|
381
|
+
},
|
|
382
|
+
tier: 'free',
|
|
383
|
+
execute: async (args) => {
|
|
384
|
+
const p = String(args.platform || 'all').toLowerCase();
|
|
385
|
+
const lines = ['Stream Setup — Multi-Platform Livestreaming', ''];
|
|
386
|
+
const instructions = {
|
|
387
|
+
twitch: `Twitch:
|
|
388
|
+
1. Go to https://dashboard.twitch.tv/settings → Stream
|
|
389
|
+
2. Copy your Primary Stream Key
|
|
390
|
+
3. Set: export TWITCH_STREAM_KEY="your_key"
|
|
391
|
+
4. Or add to ~/.kbot/.env: TWITCH_STREAM_KEY=your_key
|
|
392
|
+
|
|
393
|
+
RTMP endpoint: ${RTMP_ENDPOINTS.twitch}
|
|
394
|
+
Max bitrate: 6000kbps
|
|
395
|
+
Recommended: 1080p @ 4500kbps or 720p @ 2500kbps`,
|
|
396
|
+
rumble: `Rumble:
|
|
397
|
+
1. Go to https://rumble.com/account/live-stream
|
|
398
|
+
2. Create a stream → copy the Stream Key
|
|
399
|
+
3. Set: export RUMBLE_STREAM_KEY="your_key"
|
|
400
|
+
4. Or add to ~/.kbot/.env: RUMBLE_STREAM_KEY=your_key
|
|
401
|
+
|
|
402
|
+
RTMP endpoint: ${RTMP_ENDPOINTS.rumble}
|
|
403
|
+
Recommended: 1080p @ 4500kbps`,
|
|
404
|
+
kick: `Kick:
|
|
405
|
+
1. Go to https://kick.com/dashboard/settings/stream
|
|
406
|
+
2. Copy your Stream Key
|
|
407
|
+
3. Set: export KICK_STREAM_KEY="your_key"
|
|
408
|
+
4. Or add to ~/.kbot/.env: KICK_STREAM_KEY=your_key
|
|
409
|
+
|
|
410
|
+
RTMP endpoint: ${RTMP_ENDPOINTS.kick}
|
|
411
|
+
Recommended: 1080p @ 4500kbps`,
|
|
412
|
+
};
|
|
413
|
+
if (p === 'all') {
|
|
414
|
+
for (const [name, inst] of Object.entries(instructions)) {
|
|
415
|
+
lines.push(inst);
|
|
416
|
+
lines.push('');
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
else if (instructions[p]) {
|
|
420
|
+
lines.push(instructions[p]);
|
|
421
|
+
}
|
|
422
|
+
else {
|
|
423
|
+
return `Unknown platform: ${p}. Supported: twitch, rumble, kick`;
|
|
424
|
+
}
|
|
425
|
+
lines.push('Prerequisites:');
|
|
426
|
+
lines.push(' - ffmpeg installed (brew install ffmpeg / apt install ffmpeg)');
|
|
427
|
+
lines.push(' - Screen Recording permission granted (macOS)');
|
|
428
|
+
lines.push('');
|
|
429
|
+
lines.push('Quick test:');
|
|
430
|
+
lines.push(' kbot stream_start --source test --platforms all');
|
|
431
|
+
lines.push(' (sends test pattern to verify keys work)');
|
|
432
|
+
const configured = getConfiguredPlatforms();
|
|
433
|
+
if (configured.length > 0) {
|
|
434
|
+
lines.push('');
|
|
435
|
+
lines.push(`Currently configured: ${configured.join(', ')}`);
|
|
436
|
+
}
|
|
437
|
+
return lines.join('\n');
|
|
438
|
+
},
|
|
439
|
+
});
|
|
440
|
+
registerTool({
|
|
441
|
+
name: 'stream_scene',
|
|
442
|
+
description: 'Switch stream scene/source without stopping. Change between screen, webcam, file, or test pattern.',
|
|
443
|
+
parameters: {
|
|
444
|
+
source: { type: 'string', description: 'New source: "screen", "webcam", "test", or file path', required: true },
|
|
445
|
+
},
|
|
446
|
+
tier: 'free',
|
|
447
|
+
execute: async (args) => {
|
|
448
|
+
const state = loadState();
|
|
449
|
+
if (!state.active || !isStreamRunning()) {
|
|
450
|
+
return 'No active stream. Start one with stream_start first.';
|
|
451
|
+
}
|
|
452
|
+
// To switch source, we need to restart ffmpeg with new input
|
|
453
|
+
// Store current settings, stop, restart with new source
|
|
454
|
+
const platforms = state.platforms;
|
|
455
|
+
const resolution = state.resolution;
|
|
456
|
+
const bitrate = state.bitrate;
|
|
457
|
+
const newSource = String(args.source);
|
|
458
|
+
// Stop current
|
|
459
|
+
if (activeProcess && !activeProcess.killed) {
|
|
460
|
+
activeProcess.kill('SIGINT');
|
|
461
|
+
activeProcess = null;
|
|
462
|
+
}
|
|
463
|
+
else if (state.pid) {
|
|
464
|
+
try {
|
|
465
|
+
process.kill(state.pid, 'SIGINT');
|
|
466
|
+
}
|
|
467
|
+
catch { /* */ }
|
|
468
|
+
}
|
|
469
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
470
|
+
// Restart with new source
|
|
471
|
+
const ffmpegArgs = buildFfmpegArgs({ platforms, source: newSource, resolution, bitrate });
|
|
472
|
+
const proc = spawn('ffmpeg', ffmpegArgs, {
|
|
473
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
474
|
+
detached: true,
|
|
475
|
+
});
|
|
476
|
+
activeProcess = proc;
|
|
477
|
+
const pid = proc.pid;
|
|
478
|
+
writeFileSync(STREAM_PID, String(pid));
|
|
479
|
+
await new Promise(r => setTimeout(r, 3000));
|
|
480
|
+
if (proc.exitCode !== null) {
|
|
481
|
+
return `Error switching source to "${newSource}". ffmpeg exited.`;
|
|
482
|
+
}
|
|
483
|
+
proc.unref();
|
|
484
|
+
state.source = newSource;
|
|
485
|
+
state.pid = pid;
|
|
486
|
+
saveState(state);
|
|
487
|
+
return `Scene switched to: ${newSource}\nStream continues on: ${platforms.join(', ')}`;
|
|
488
|
+
},
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
//# sourceMappingURL=streaming.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kernel.chat/kbot",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.82.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": {
|