@kernel.chat/kbot 3.69.1 → 3.71.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.
@@ -301,7 +301,204 @@ export class AbletonM4L {
301
301
  return this.send({ action: 'lom_call', path, method, args });
302
302
  }
303
303
  }
304
- // ── Convenience export ──────────────────────────────────────────────
304
+ /**
305
+ * Client for the KBotBridge Remote Script (TCP 9998).
306
+ *
307
+ * This is separate from the M4L bridge (9999) because the Browser API
308
+ * (browser.load_item) is ONLY available from Python Remote Scripts,
309
+ * not from Max for Live.
310
+ *
311
+ * Use this to programmatically load any native device (Saturator,
312
+ * EQ Eight, Compressor, etc.) onto any track.
313
+ */
314
+ export class AbletonBrowserBridge {
315
+ static instance = null;
316
+ socket = null;
317
+ connected = false;
318
+ pending = new Map();
319
+ nextId = 1;
320
+ buffer = '';
321
+ static PORT = 9998;
322
+ static HOST = '127.0.0.1';
323
+ static TIMEOUT = 15_000; // Browser operations can be slow
324
+ constructor() { }
325
+ static getInstance() {
326
+ if (!AbletonBrowserBridge.instance) {
327
+ AbletonBrowserBridge.instance = new AbletonBrowserBridge();
328
+ }
329
+ return AbletonBrowserBridge.instance;
330
+ }
331
+ /**
332
+ * Connect to the KBotBridge Remote Script on port 9998.
333
+ * Returns true if connected and the bridge responds to ping.
334
+ */
335
+ async connect() {
336
+ if (this.connected && this.socket) {
337
+ try {
338
+ await this.send({ action: 'ping' });
339
+ return true;
340
+ }
341
+ catch {
342
+ this.disconnect();
343
+ }
344
+ }
345
+ return new Promise((resolve) => {
346
+ this.socket = new net.Socket();
347
+ this.buffer = '';
348
+ this.socket.on('data', (data) => {
349
+ this.buffer += data.toString();
350
+ const lines = this.buffer.split('\n');
351
+ this.buffer = lines.pop() || '';
352
+ for (const line of lines) {
353
+ const trimmed = line.trim();
354
+ if (!trimmed)
355
+ continue;
356
+ try {
357
+ const response = JSON.parse(trimmed);
358
+ this.handleResponse(response);
359
+ }
360
+ catch {
361
+ // Malformed JSON — skip
362
+ }
363
+ }
364
+ });
365
+ this.socket.on('error', () => {
366
+ if (!this.connected)
367
+ resolve(false);
368
+ this.handleDisconnect();
369
+ });
370
+ this.socket.on('close', () => {
371
+ this.handleDisconnect();
372
+ });
373
+ this.socket.connect(AbletonBrowserBridge.PORT, AbletonBrowserBridge.HOST, async () => {
374
+ this.connected = true;
375
+ try {
376
+ const pong = await this.send({ action: 'ping' });
377
+ resolve(pong.ok);
378
+ }
379
+ catch {
380
+ resolve(false);
381
+ }
382
+ });
383
+ setTimeout(() => {
384
+ if (!this.connected) {
385
+ this.socket?.destroy();
386
+ resolve(false);
387
+ }
388
+ }, 5000);
389
+ });
390
+ }
391
+ disconnect() {
392
+ this.connected = false;
393
+ if (this.socket) {
394
+ this.socket.destroy();
395
+ this.socket = null;
396
+ }
397
+ for (const [, req] of this.pending) {
398
+ clearTimeout(req.timer);
399
+ req.reject(new Error('Disconnected'));
400
+ }
401
+ this.pending.clear();
402
+ this.buffer = '';
403
+ }
404
+ async send(cmd) {
405
+ if (!this.connected || !this.socket) {
406
+ throw new Error('Not connected to KBotBridge Remote Script.\n' +
407
+ 'Make sure KBotBridge is selected as a Control Surface in Ableton Preferences.');
408
+ }
409
+ const id = this.nextId++;
410
+ const fullCmd = { id, ...cmd };
411
+ return new Promise((resolve, reject) => {
412
+ const timer = setTimeout(() => {
413
+ this.pending.delete(id);
414
+ reject(new Error(`Timeout: ${cmd.action}`));
415
+ }, AbletonBrowserBridge.TIMEOUT);
416
+ this.pending.set(id, { resolve, reject, timer });
417
+ this.socket.write(JSON.stringify(fullCmd) + '\n');
418
+ });
419
+ }
420
+ get isConnected() {
421
+ return this.connected;
422
+ }
423
+ handleResponse(response) {
424
+ if (response.id && this.pending.has(response.id)) {
425
+ const req = this.pending.get(response.id);
426
+ this.pending.delete(response.id);
427
+ clearTimeout(req.timer);
428
+ req.resolve(response);
429
+ }
430
+ }
431
+ handleDisconnect() {
432
+ if (!this.connected)
433
+ return;
434
+ this.connected = false;
435
+ this.socket = null;
436
+ for (const [, req] of this.pending) {
437
+ clearTimeout(req.timer);
438
+ req.reject(new Error('Connection lost'));
439
+ }
440
+ this.pending.clear();
441
+ }
442
+ // ── Browser convenience methods ─────────────────────────────────
443
+ async ping() {
444
+ try {
445
+ const r = await this.send({ action: 'ping' });
446
+ return r.ok;
447
+ }
448
+ catch {
449
+ return false;
450
+ }
451
+ }
452
+ /**
453
+ * Search Ableton's browser for items matching a query.
454
+ * @param query - Search string (case-insensitive)
455
+ * @param category - instruments/audio_effects/midi_effects/drums/samples/all
456
+ */
457
+ async browserSearch(query, category = 'all') {
458
+ const r = await this.send({ action: 'browser_search', query, category });
459
+ if (!r.ok)
460
+ throw new Error(r.error || 'Browser search failed');
461
+ return r.results || [];
462
+ }
463
+ /**
464
+ * Load a browser item by URI onto a track.
465
+ * Use the URI from a browserSearch() result.
466
+ */
467
+ async browserLoad(track, uri) {
468
+ return this.send({ action: 'browser_load', track, uri });
469
+ }
470
+ /**
471
+ * Search + load in one step. Finds first loadable match and loads it.
472
+ * @param track - 0-indexed track number
473
+ * @param name - Device name to search for (e.g., "Saturator", "EQ Eight")
474
+ * @param category - instruments/audio_effects/midi_effects/drums/samples/all
475
+ */
476
+ async browserLoadByName(track, name, category = 'all') {
477
+ return this.send({ action: 'browser_load_by_name', track, name, category });
478
+ }
479
+ /**
480
+ * List top-level browser categories with child counts.
481
+ */
482
+ async browserCategories() {
483
+ const r = await this.send({ action: 'browser_categories' });
484
+ if (!r.ok)
485
+ throw new Error(r.error || 'Failed to list categories');
486
+ return r.categories || [];
487
+ }
488
+ /**
489
+ * List all tracks with names and device counts.
490
+ */
491
+ async listTracks() {
492
+ return this.send({ action: 'list_tracks' });
493
+ }
494
+ /**
495
+ * List devices on a track with full parameter details.
496
+ */
497
+ async listDevices(track) {
498
+ return this.send({ action: 'list_devices', track });
499
+ }
500
+ }
501
+ // ── Convenience exports ─────────────────────────────────────────────
305
502
  /**
306
503
  * Get a connected M4L bridge instance.
307
504
  * Throws if the bridge is not available.
@@ -320,6 +517,41 @@ export async function ensureM4L() {
320
517
  }
321
518
  return m4l;
322
519
  }
520
+ /**
521
+ * Get a connected Browser bridge instance (KBotBridge Remote Script on port 9998).
522
+ * Throws if not available.
523
+ */
524
+ export async function ensureBrowserBridge() {
525
+ const bridge = AbletonBrowserBridge.getInstance();
526
+ if (bridge.isConnected)
527
+ return bridge;
528
+ const ok = await bridge.connect();
529
+ if (!ok) {
530
+ throw new Error('Cannot connect to KBotBridge Remote Script.\n\n' +
531
+ 'Make sure:\n' +
532
+ '1. Ableton Live is running\n' +
533
+ '2. KBotBridge is selected as a Control Surface in Preferences > Link, Tempo & MIDI\n' +
534
+ '3. Ableton status bar shows "KBotBridge: Listening on port 9998"\n\n' +
535
+ 'To install: kbot ableton install-bridge\n');
536
+ }
537
+ return bridge;
538
+ }
539
+ /**
540
+ * Connect to both M4L bridge (9999) and Browser bridge (9998).
541
+ * Returns whichever connections succeed. At least one must connect.
542
+ */
543
+ export async function connectBrowser() {
544
+ const m4l = AbletonM4L.getInstance();
545
+ const browser = AbletonBrowserBridge.getInstance();
546
+ const [m4lOk, browserOk] = await Promise.all([
547
+ m4l.connect().catch(() => false),
548
+ browser.connect().catch(() => false),
549
+ ]);
550
+ return {
551
+ m4l: m4lOk ? m4l : null,
552
+ browser: browserOk ? browser : null,
553
+ };
554
+ }
323
555
  /**
324
556
  * Format a friendly error message for M4L connection failures.
325
557
  */
@@ -335,4 +567,23 @@ export function formatM4LError() {
335
567
  'The M4L bridge gives kbot full control over Ableton — instruments, effects, clips, mixing, everything.',
336
568
  ].join('\n');
337
569
  }
570
+ /**
571
+ * Format a friendly error message for Browser bridge connection failures.
572
+ */
573
+ export function formatBrowserBridgeError() {
574
+ return [
575
+ '**KBotBridge Remote Script not connected**',
576
+ '',
577
+ 'The Browser API (for loading native devices) requires the KBotBridge Remote Script:',
578
+ '1. Install: `kbot ableton install-bridge`',
579
+ '2. Open Ableton Live Preferences (Cmd+,)',
580
+ '3. Go to Link, Tempo & MIDI',
581
+ '4. Set a Control Surface to "KBotBridge"',
582
+ '5. Close Preferences',
583
+ '',
584
+ 'This runs alongside the M4L bridge — they use different ports:',
585
+ '- KBotBridge: TCP 9998 (Browser API, device loading)',
586
+ '- M4L Bridge: TCP 9999 (LOM access, clips, mixing)',
587
+ ].join('\n');
588
+ }
338
589
  //# sourceMappingURL=ableton-m4l.js.map
@@ -0,0 +1,23 @@
1
+ /**
2
+ * install-remote-script.ts — Install KBotBridge Remote Script into Ableton Live
3
+ *
4
+ * Copies the KBotBridge Python Remote Script to Ableton's User Library,
5
+ * enabling the Browser API bridge on TCP port 9998.
6
+ *
7
+ * The Remote Script exposes Ableton's browser.load_item() API, which is
8
+ * ONLY available from Python Remote Scripts (not from Max for Live).
9
+ * This lets kbot programmatically load any native device (Saturator,
10
+ * EQ Eight, Compressor, etc.) onto any track.
11
+ *
12
+ * Usage:
13
+ * npx tsx packages/kbot/src/integrations/install-remote-script.ts
14
+ * kbot ableton install-bridge
15
+ */
16
+ export declare function installKBotBridge(): Promise<string>;
17
+ export declare function isKBotBridgeInstalled(): boolean;
18
+ export declare function uninstallKBotBridge(): string;
19
+ /**
20
+ * Get the path to the KBotBridge log file inside Ableton's Remote Scripts.
21
+ */
22
+ export declare function getKBotBridgeLogPath(): string | null;
23
+ //# sourceMappingURL=install-remote-script.d.ts.map
@@ -0,0 +1,121 @@
1
+ /**
2
+ * install-remote-script.ts — Install KBotBridge Remote Script into Ableton Live
3
+ *
4
+ * Copies the KBotBridge Python Remote Script to Ableton's User Library,
5
+ * enabling the Browser API bridge on TCP port 9998.
6
+ *
7
+ * The Remote Script exposes Ableton's browser.load_item() API, which is
8
+ * ONLY available from Python Remote Scripts (not from Max for Live).
9
+ * This lets kbot programmatically load any native device (Saturator,
10
+ * EQ Eight, Compressor, etc.) onto any track.
11
+ *
12
+ * Usage:
13
+ * npx tsx packages/kbot/src/integrations/install-remote-script.ts
14
+ * kbot ableton install-bridge
15
+ */
16
+ import * as fs from 'node:fs';
17
+ import * as path from 'node:path';
18
+ import * as os from 'node:os';
19
+ const SCRIPT_NAME = 'KBotBridge';
20
+ const SOURCE_FILES = [
21
+ '__init__.py',
22
+ 'kbot_control_surface.py',
23
+ 'tcp_server.py',
24
+ ];
25
+ function getRemoteScriptsDir() {
26
+ const home = os.homedir();
27
+ return path.join(home, 'Music', 'Ableton', 'User Library', 'Remote Scripts');
28
+ }
29
+ function getDestDir() {
30
+ return path.join(getRemoteScriptsDir(), SCRIPT_NAME);
31
+ }
32
+ function getSourceDir() {
33
+ // Source files live alongside this installer in the integrations directory
34
+ return path.join(__dirname, SCRIPT_NAME);
35
+ }
36
+ export async function installKBotBridge() {
37
+ const lines = [];
38
+ const log = (msg) => { lines.push(msg); console.log(msg); };
39
+ log('Installing KBotBridge Remote Script...');
40
+ log('');
41
+ const sourceDir = getSourceDir();
42
+ const destDir = getDestDir();
43
+ const remoteScriptsDir = getRemoteScriptsDir();
44
+ // Verify source files exist
45
+ for (const file of SOURCE_FILES) {
46
+ const srcPath = path.join(sourceDir, file);
47
+ if (!fs.existsSync(srcPath)) {
48
+ log(` ERROR: Source file missing: ${srcPath}`);
49
+ log(' Run from the kbot package directory.');
50
+ return lines.join('\n');
51
+ }
52
+ }
53
+ // Create Remote Scripts directory if it doesn't exist
54
+ if (!fs.existsSync(remoteScriptsDir)) {
55
+ fs.mkdirSync(remoteScriptsDir, { recursive: true });
56
+ log(` Created: ${remoteScriptsDir}`);
57
+ }
58
+ // Remove old installation if present
59
+ if (fs.existsSync(destDir)) {
60
+ fs.rmSync(destDir, { recursive: true });
61
+ log(' Removed previous KBotBridge installation');
62
+ }
63
+ // Copy files
64
+ fs.mkdirSync(destDir, { recursive: true });
65
+ for (const file of SOURCE_FILES) {
66
+ const src = path.join(sourceDir, file);
67
+ const dst = path.join(destDir, file);
68
+ fs.copyFileSync(src, dst);
69
+ log(` Copied: ${file}`);
70
+ }
71
+ // Create logs directory (the script will log here)
72
+ const logsDir = path.join(destDir, 'logs');
73
+ if (!fs.existsSync(logsDir)) {
74
+ fs.mkdirSync(logsDir, { recursive: true });
75
+ }
76
+ log('');
77
+ log(`KBotBridge installed to: ${destDir}`);
78
+ log('');
79
+ log('To activate:');
80
+ log(' 1. Open Ableton Live (or restart if already running)');
81
+ log(' 2. Preferences (Cmd+,) > Link, Tempo & MIDI');
82
+ log(' 3. Set a Control Surface slot to "KBotBridge"');
83
+ log(' 4. Input/Output can be left as "None"');
84
+ log(' 5. Close Preferences');
85
+ log('');
86
+ log('Verify:');
87
+ log(' - Ableton status bar shows "KBotBridge: Listening on port 9998"');
88
+ log(' - Run: echo \'{"id":1,"action":"ping"}\\n\' | nc localhost 9998');
89
+ log('');
90
+ log('KBotBridge runs alongside AbletonOSC — they use different ports:');
91
+ log(' - KBotBridge: TCP 9998 (Browser API, device loading)');
92
+ log(' - M4L Bridge: TCP 9999 (LOM access, clips, mixing)');
93
+ log(' - AbletonOSC: UDP 11000/11001 (OSC, legacy)');
94
+ return lines.join('\n');
95
+ }
96
+ export function isKBotBridgeInstalled() {
97
+ const destDir = getDestDir();
98
+ return fs.existsSync(path.join(destDir, '__init__.py'))
99
+ && fs.existsSync(path.join(destDir, 'kbot_control_surface.py'))
100
+ && fs.existsSync(path.join(destDir, 'tcp_server.py'));
101
+ }
102
+ export function uninstallKBotBridge() {
103
+ const destDir = getDestDir();
104
+ if (fs.existsSync(destDir)) {
105
+ fs.rmSync(destDir, { recursive: true });
106
+ return `KBotBridge removed from ${destDir}`;
107
+ }
108
+ return 'KBotBridge was not installed.';
109
+ }
110
+ /**
111
+ * Get the path to the KBotBridge log file inside Ableton's Remote Scripts.
112
+ */
113
+ export function getKBotBridgeLogPath() {
114
+ const logPath = path.join(getDestDir(), 'logs', 'kbot_bridge.log');
115
+ return fs.existsSync(logPath) ? logPath : null;
116
+ }
117
+ // ── CLI entrypoint ──────────────────────────────────────────────────
118
+ if (require.main === module || process.argv[1]?.includes('install-remote-script')) {
119
+ installKBotBridge().catch(console.error);
120
+ }
121
+ //# sourceMappingURL=install-remote-script.js.map
package/dist/machine.d.ts CHANGED
@@ -73,6 +73,7 @@ export interface MachineProfile {
73
73
  canRunLocalModels: boolean;
74
74
  gpuAcceleration: 'metal' | 'cuda' | 'vulkan' | 'cpu-only';
75
75
  recommendedModelSize: string;
76
+ mlxAvailable: boolean;
76
77
  probedAt: string;
77
78
  }
78
79
  export declare function probeMachine(): Promise<MachineProfile>;
package/dist/machine.js CHANGED
@@ -314,6 +314,18 @@ function detectGpuAcceleration(gpus) {
314
314
  return 'vulkan';
315
315
  return 'cpu-only';
316
316
  }
317
+ // ── MLX framework detection (Apple Silicon) ──
318
+ function detectMLX() {
319
+ if (platform() !== 'darwin' || arch() !== 'arm64')
320
+ return false;
321
+ // Quick check: try importing mlx in Python
322
+ const result = exec('python3 -c "import mlx; print(mlx.__version__)" 2>/dev/null', 3000);
323
+ if (result && !result.includes('ModuleNotFoundError'))
324
+ return true;
325
+ // Fallback: check common pip install path
326
+ const globCheck = exec('ls /usr/local/lib/python3.*/site-packages/mlx/__init__.py 2>/dev/null || ls ~/Library/Python/3.*/lib/python/site-packages/mlx/__init__.py 2>/dev/null', 2000);
327
+ return !!globCheck;
328
+ }
317
329
  // ── Model size recommendation ──
318
330
  function recommendModelSize(totalMemoryGB, gpuAccel) {
319
331
  // Conservative: leave room for OS + apps
@@ -374,6 +386,7 @@ export async function probeMachine() {
374
386
  osInfo = probeLinuxOs();
375
387
  }
376
388
  const gpuAccel = detectGpuAcceleration(gpus);
389
+ const mlxAvailable = detectMLX();
377
390
  const devTools = probeDevTools();
378
391
  // Uptime
379
392
  const uptimeSeconds = plat === 'darwin'
@@ -420,6 +433,7 @@ export async function probeMachine() {
420
433
  canRunLocalModels: gpuAccel !== 'cpu-only' || totalGB >= 8,
421
434
  gpuAcceleration: gpuAccel,
422
435
  recommendedModelSize: recommendModelSize(totalGB, gpuAccel),
436
+ mlxAvailable,
423
437
  probedAt: new Date().toISOString(),
424
438
  };
425
439
  cached = profile;
@@ -505,6 +519,8 @@ export function formatMachineProfile(p) {
505
519
  lines.push('');
506
520
  lines.push(' AI Capabilities');
507
521
  lines.push(` Acceleration ${p.gpuAcceleration}`);
522
+ if (p.mlxAvailable)
523
+ lines.push(` MLX framework available (Apple Silicon accelerated)`);
508
524
  lines.push(` Local models ${p.canRunLocalModels ? 'yes' : 'no'}`);
509
525
  lines.push(` Recommended up to ${p.recommendedModelSize} parameters`);
510
526
  lines.push('');
@@ -529,7 +545,7 @@ export function formatMachineForPrompt(p) {
529
545
  if (p.battery.present) {
530
546
  parts.push(`Battery: ${p.battery.percent}% ${p.battery.charging ? 'charging' : 'discharging'}`);
531
547
  }
532
- parts.push(`GPU accel: ${p.gpuAcceleration} — local models up to ${p.recommendedModelSize}`);
548
+ parts.push(`GPU accel: ${p.gpuAcceleration}${p.mlxAvailable ? ' + MLX' : ''} — local models up to ${p.recommendedModelSize}`);
533
549
  const toolNames = p.devTools.map(t => `${t.name} ${t.version}`).join(', ');
534
550
  if (toolNames)
535
551
  parts.push(`Tools: ${toolNames}`);
package/dist/serve.js CHANGED
@@ -235,8 +235,9 @@ export async function startServe(options) {
235
235
  printInfo(` GET /metrics — Execution metrics`);
236
236
  printInfo(` GET /apps — List MCP App-capable tools`);
237
237
  printInfo(` GET /.well-known/agent.json — A2A Agent Card`);
238
- printInfo(` POST /a2a/tasks — A2A submit task`);
239
- printInfo(` GET /a2a/tasks/:id — A2A task status`);
238
+ printInfo(` POST /a2a — A2A JSON-RPC endpoint`);
239
+ printInfo(` POST /a2a/tasks — A2A submit task (REST)`);
240
+ printInfo(` GET /a2a/tasks/:id — A2A task status (REST)`);
240
241
  if (options.token) {
241
242
  printInfo(` Auth: Bearer token required`);
242
243
  }
@@ -0,0 +1,2 @@
1
+ export declare function registerA2ATools(): void;
2
+ //# sourceMappingURL=a2a.d.ts.map