@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.
- package/dist/a2a.d.ts +50 -5
- package/dist/a2a.js +305 -44
- package/dist/auth.d.ts +4 -0
- package/dist/auth.js +43 -4
- package/dist/doctor.js +53 -7
- package/dist/integrations/ableton-bridge.d.ts +158 -0
- package/dist/integrations/ableton-bridge.js +486 -0
- package/dist/integrations/ableton-m4l.d.ts +94 -0
- package/dist/integrations/ableton-m4l.js +252 -1
- package/dist/integrations/install-remote-script.d.ts +23 -0
- package/dist/integrations/install-remote-script.js +121 -0
- package/dist/machine.d.ts +1 -0
- package/dist/machine.js +17 -1
- package/dist/serve.js +3 -2
- package/dist/tools/a2a.d.ts +2 -0
- package/dist/tools/a2a.js +233 -0
- package/dist/tools/ableton-bridge-tools.d.ts +14 -0
- package/dist/tools/ableton-bridge-tools.js +327 -0
- package/dist/tools/ai-analysis.d.ts +2 -0
- package/dist/tools/ai-analysis.js +677 -0
- package/dist/tools/financial-analysis.d.ts +2 -0
- package/dist/tools/financial-analysis.js +945 -0
- package/dist/tools/index.js +6 -0
- package/dist/tools/music-gen.d.ts +2 -0
- package/dist/tools/music-gen.js +1006 -0
- package/dist/tools/threat-intel.d.ts +2 -0
- package/dist/tools/threat-intel.js +1619 -0
- package/package.json +2 -2
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ableton-bridge.ts — kbot ↔ AbletonBridge TCP Client
|
|
3
|
+
*
|
|
4
|
+
* Connects to AbletonBridge (https://github.com/hidingwill/AbletonBridge),
|
|
5
|
+
* a 353-tool Remote Script that exposes Ableton's full Browser API
|
|
6
|
+
* via a TCP server on localhost:9001.
|
|
7
|
+
*
|
|
8
|
+
* Protocol:
|
|
9
|
+
* Send: {"id": 1, "method": "search_browser", "params": {...}}\n
|
|
10
|
+
* Receive: {"id": 1, "result": {...}}\n
|
|
11
|
+
*
|
|
12
|
+
* Fallback chain (used by tools):
|
|
13
|
+
* 1. AbletonBridge (port 9001) — full browser API
|
|
14
|
+
* 2. KBotBridge (port 9998) — kbot's own Remote Script
|
|
15
|
+
* 3. Error with install instructions
|
|
16
|
+
*
|
|
17
|
+
* Follows the same singleton + newline-delimited JSON pattern as AbletonM4L.
|
|
18
|
+
*/
|
|
19
|
+
import * as net from 'node:net';
|
|
20
|
+
// ── Client ────────────────────────────────────────────────────────────
|
|
21
|
+
export class AbletonBridgeClient {
|
|
22
|
+
static instance = null;
|
|
23
|
+
socket = null;
|
|
24
|
+
connected = false;
|
|
25
|
+
pending = new Map();
|
|
26
|
+
nextId = 1;
|
|
27
|
+
buffer = '';
|
|
28
|
+
static PORT = 9001;
|
|
29
|
+
static HOST = '127.0.0.1';
|
|
30
|
+
static TIMEOUT = 15_000;
|
|
31
|
+
static CONNECT_TIMEOUT = 5_000;
|
|
32
|
+
constructor() { }
|
|
33
|
+
/**
|
|
34
|
+
* Get the singleton instance.
|
|
35
|
+
*/
|
|
36
|
+
static getInstance() {
|
|
37
|
+
if (!AbletonBridgeClient.instance) {
|
|
38
|
+
AbletonBridgeClient.instance = new AbletonBridgeClient();
|
|
39
|
+
}
|
|
40
|
+
return AbletonBridgeClient.instance;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Connect to AbletonBridge TCP server.
|
|
44
|
+
* Returns true if connected and responds to a ping/handshake.
|
|
45
|
+
*/
|
|
46
|
+
async connect() {
|
|
47
|
+
if (this.connected && this.socket) {
|
|
48
|
+
// Already connected — verify with a lightweight call
|
|
49
|
+
try {
|
|
50
|
+
await this.send('ping');
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
// Connection stale, reconnect
|
|
55
|
+
this.disconnect();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return new Promise((resolve) => {
|
|
59
|
+
this.socket = new net.Socket();
|
|
60
|
+
this.buffer = '';
|
|
61
|
+
this.socket.on('data', (data) => {
|
|
62
|
+
this.buffer += data.toString();
|
|
63
|
+
const lines = this.buffer.split('\n');
|
|
64
|
+
this.buffer = lines.pop() || '';
|
|
65
|
+
for (const line of lines) {
|
|
66
|
+
const trimmed = line.trim();
|
|
67
|
+
if (!trimmed)
|
|
68
|
+
continue;
|
|
69
|
+
try {
|
|
70
|
+
const response = JSON.parse(trimmed);
|
|
71
|
+
this.handleResponse(response);
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// Malformed JSON — skip
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
this.socket.on('error', () => {
|
|
79
|
+
if (!this.connected) {
|
|
80
|
+
resolve(false);
|
|
81
|
+
}
|
|
82
|
+
this.handleDisconnect();
|
|
83
|
+
});
|
|
84
|
+
this.socket.on('close', () => {
|
|
85
|
+
this.handleDisconnect();
|
|
86
|
+
});
|
|
87
|
+
this.socket.connect(AbletonBridgeClient.PORT, AbletonBridgeClient.HOST, async () => {
|
|
88
|
+
this.connected = true;
|
|
89
|
+
// Verify connectivity
|
|
90
|
+
try {
|
|
91
|
+
const pong = await this.send('ping');
|
|
92
|
+
resolve(!pong.error);
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
// Even if ping fails, we may still be connected to a bridge
|
|
96
|
+
// that doesn't support ping — consider it connected
|
|
97
|
+
resolve(true);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
// Connection timeout
|
|
101
|
+
setTimeout(() => {
|
|
102
|
+
if (!this.connected) {
|
|
103
|
+
this.socket?.destroy();
|
|
104
|
+
resolve(false);
|
|
105
|
+
}
|
|
106
|
+
}, AbletonBridgeClient.CONNECT_TIMEOUT);
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Check if connected.
|
|
111
|
+
*/
|
|
112
|
+
isConnected() {
|
|
113
|
+
return this.connected;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Disconnect from the bridge.
|
|
117
|
+
*/
|
|
118
|
+
disconnect() {
|
|
119
|
+
this.connected = false;
|
|
120
|
+
if (this.socket) {
|
|
121
|
+
this.socket.destroy();
|
|
122
|
+
this.socket = null;
|
|
123
|
+
}
|
|
124
|
+
// Reject all pending requests
|
|
125
|
+
for (const [, req] of this.pending) {
|
|
126
|
+
clearTimeout(req.timer);
|
|
127
|
+
req.reject(new Error('Disconnected'));
|
|
128
|
+
}
|
|
129
|
+
this.pending.clear();
|
|
130
|
+
this.buffer = '';
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Send a method call and wait for a response.
|
|
134
|
+
*/
|
|
135
|
+
async send(method, params) {
|
|
136
|
+
if (!this.connected || !this.socket) {
|
|
137
|
+
throw new Error('Not connected to AbletonBridge. Is Ableton running with the AbletonBridge Remote Script?');
|
|
138
|
+
}
|
|
139
|
+
const id = this.nextId++;
|
|
140
|
+
const cmd = { id, method };
|
|
141
|
+
if (params)
|
|
142
|
+
cmd.params = params;
|
|
143
|
+
return new Promise((resolve, reject) => {
|
|
144
|
+
const timer = setTimeout(() => {
|
|
145
|
+
this.pending.delete(id);
|
|
146
|
+
reject(new Error(`Timeout: ${method}`));
|
|
147
|
+
}, AbletonBridgeClient.TIMEOUT);
|
|
148
|
+
this.pending.set(id, { resolve, reject, timer });
|
|
149
|
+
const json = JSON.stringify(cmd) + '\n';
|
|
150
|
+
this.socket.write(json);
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
// ── Response handling ─────────────────────────────────────────────
|
|
154
|
+
handleResponse(response) {
|
|
155
|
+
if (response.id && this.pending.has(response.id)) {
|
|
156
|
+
const req = this.pending.get(response.id);
|
|
157
|
+
this.pending.delete(response.id);
|
|
158
|
+
clearTimeout(req.timer);
|
|
159
|
+
req.resolve(response);
|
|
160
|
+
}
|
|
161
|
+
// No event/push support for AbletonBridge — all request/response
|
|
162
|
+
}
|
|
163
|
+
handleDisconnect() {
|
|
164
|
+
if (!this.connected)
|
|
165
|
+
return;
|
|
166
|
+
this.connected = false;
|
|
167
|
+
this.socket = null;
|
|
168
|
+
// Reject pending
|
|
169
|
+
for (const [, req] of this.pending) {
|
|
170
|
+
clearTimeout(req.timer);
|
|
171
|
+
req.reject(new Error('Connection lost'));
|
|
172
|
+
}
|
|
173
|
+
this.pending.clear();
|
|
174
|
+
}
|
|
175
|
+
// ── Browser API ───────────────────────────────────────────────────
|
|
176
|
+
/**
|
|
177
|
+
* Search Ableton's browser for items matching a query.
|
|
178
|
+
* Optionally filter by category: "instruments", "audio_effects", "midi_effects",
|
|
179
|
+
* "drums", "sounds", "packs", "plugins", "samples", "presets".
|
|
180
|
+
*/
|
|
181
|
+
async searchBrowser(query, category) {
|
|
182
|
+
const params = { query };
|
|
183
|
+
if (category)
|
|
184
|
+
params.category = category;
|
|
185
|
+
const resp = await this.send('search_browser', params);
|
|
186
|
+
if (resp.error)
|
|
187
|
+
throw new Error(resp.error);
|
|
188
|
+
const items = resp.result;
|
|
189
|
+
if (!Array.isArray(items))
|
|
190
|
+
return [];
|
|
191
|
+
return items.map((item) => ({
|
|
192
|
+
name: String(item.name ?? ''),
|
|
193
|
+
uri: String(item.uri ?? ''),
|
|
194
|
+
isLoadable: Boolean(item.is_loadable ?? item.isLoadable ?? false),
|
|
195
|
+
isDevice: Boolean(item.is_device ?? item.isDevice ?? false),
|
|
196
|
+
isFolder: Boolean(item.is_folder ?? item.isFolder ?? false),
|
|
197
|
+
}));
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Load a device onto a track by its browser URI.
|
|
201
|
+
*/
|
|
202
|
+
async loadDevice(trackIndex, uri) {
|
|
203
|
+
const resp = await this.send('load_device', { track: trackIndex, uri });
|
|
204
|
+
if (resp.error)
|
|
205
|
+
throw new Error(resp.error);
|
|
206
|
+
return Boolean(resp.result);
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Search for a device by name and load the first loadable match onto a track.
|
|
210
|
+
* Optionally filter by category to narrow results.
|
|
211
|
+
*/
|
|
212
|
+
async loadDeviceByName(trackIndex, name, category) {
|
|
213
|
+
const items = await this.searchBrowser(name, category);
|
|
214
|
+
// Find the first loadable device
|
|
215
|
+
const device = items.find((item) => item.isLoadable && item.isDevice);
|
|
216
|
+
if (!device) {
|
|
217
|
+
// Fallback: try any loadable item
|
|
218
|
+
const loadable = items.find((item) => item.isLoadable);
|
|
219
|
+
if (!loadable) {
|
|
220
|
+
throw new Error(`No loadable device found for "${name}"${category ? ` in category "${category}"` : ''}`);
|
|
221
|
+
}
|
|
222
|
+
return this.loadDevice(trackIndex, loadable.uri);
|
|
223
|
+
}
|
|
224
|
+
return this.loadDevice(trackIndex, device.uri);
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* List presets available for a device by its URI.
|
|
228
|
+
*/
|
|
229
|
+
async listPresets(deviceUri) {
|
|
230
|
+
const resp = await this.send('list_presets', { uri: deviceUri });
|
|
231
|
+
if (resp.error)
|
|
232
|
+
throw new Error(resp.error);
|
|
233
|
+
const presets = resp.result;
|
|
234
|
+
if (!Array.isArray(presets))
|
|
235
|
+
return [];
|
|
236
|
+
return presets.map((p) => ({
|
|
237
|
+
name: String(p.name ?? ''),
|
|
238
|
+
uri: String(p.uri ?? ''),
|
|
239
|
+
}));
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Load a preset onto a device on a specific track.
|
|
243
|
+
*/
|
|
244
|
+
async loadPreset(trackIndex, deviceIndex, presetUri) {
|
|
245
|
+
const resp = await this.send('load_preset', {
|
|
246
|
+
track: trackIndex,
|
|
247
|
+
device: deviceIndex,
|
|
248
|
+
uri: presetUri,
|
|
249
|
+
});
|
|
250
|
+
if (resp.error)
|
|
251
|
+
throw new Error(resp.error);
|
|
252
|
+
return Boolean(resp.result);
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Get the effect/device chain on a track.
|
|
256
|
+
*/
|
|
257
|
+
async getEffectChain(trackIndex) {
|
|
258
|
+
const resp = await this.send('get_device_chain', { track: trackIndex });
|
|
259
|
+
if (resp.error)
|
|
260
|
+
throw new Error(resp.error);
|
|
261
|
+
const devices = resp.result;
|
|
262
|
+
if (!Array.isArray(devices))
|
|
263
|
+
return [];
|
|
264
|
+
return devices.map((d, i) => ({
|
|
265
|
+
name: String(d.name ?? ''),
|
|
266
|
+
className: String(d.class_name ?? d.className ?? ''),
|
|
267
|
+
index: typeof d.index === 'number' ? d.index : i,
|
|
268
|
+
}));
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
// ── KBotBridge fallback (port 9998) ────────────────────────────────────
|
|
272
|
+
/**
|
|
273
|
+
* Lightweight TCP probe for the kbot Remote Script on port 9998.
|
|
274
|
+
* Uses the same newline-delimited JSON protocol as AbletonM4L.
|
|
275
|
+
*/
|
|
276
|
+
export class KBotRemoteClient {
|
|
277
|
+
static instance = null;
|
|
278
|
+
socket = null;
|
|
279
|
+
connected = false;
|
|
280
|
+
pending = new Map();
|
|
281
|
+
nextId = 1;
|
|
282
|
+
buffer = '';
|
|
283
|
+
static PORT = 9998;
|
|
284
|
+
static HOST = '127.0.0.1';
|
|
285
|
+
static TIMEOUT = 10_000;
|
|
286
|
+
static CONNECT_TIMEOUT = 3_000;
|
|
287
|
+
constructor() { }
|
|
288
|
+
static getInstance() {
|
|
289
|
+
if (!KBotRemoteClient.instance) {
|
|
290
|
+
KBotRemoteClient.instance = new KBotRemoteClient();
|
|
291
|
+
}
|
|
292
|
+
return KBotRemoteClient.instance;
|
|
293
|
+
}
|
|
294
|
+
async connect() {
|
|
295
|
+
if (this.connected && this.socket) {
|
|
296
|
+
try {
|
|
297
|
+
await this.send({ action: 'ping' });
|
|
298
|
+
return true;
|
|
299
|
+
}
|
|
300
|
+
catch {
|
|
301
|
+
this.disconnect();
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
return new Promise((resolve) => {
|
|
305
|
+
this.socket = new net.Socket();
|
|
306
|
+
this.buffer = '';
|
|
307
|
+
this.socket.on('data', (data) => {
|
|
308
|
+
this.buffer += data.toString();
|
|
309
|
+
const lines = this.buffer.split('\n');
|
|
310
|
+
this.buffer = lines.pop() || '';
|
|
311
|
+
for (const line of lines) {
|
|
312
|
+
const trimmed = line.trim();
|
|
313
|
+
if (!trimmed)
|
|
314
|
+
continue;
|
|
315
|
+
try {
|
|
316
|
+
const response = JSON.parse(trimmed);
|
|
317
|
+
this.handleResponse(response);
|
|
318
|
+
}
|
|
319
|
+
catch {
|
|
320
|
+
// skip
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
this.socket.on('error', () => {
|
|
325
|
+
if (!this.connected)
|
|
326
|
+
resolve(false);
|
|
327
|
+
this.handleDisconnect();
|
|
328
|
+
});
|
|
329
|
+
this.socket.on('close', () => {
|
|
330
|
+
this.handleDisconnect();
|
|
331
|
+
});
|
|
332
|
+
this.socket.connect(KBotRemoteClient.PORT, KBotRemoteClient.HOST, async () => {
|
|
333
|
+
this.connected = true;
|
|
334
|
+
try {
|
|
335
|
+
const pong = await this.send({ action: 'ping' });
|
|
336
|
+
resolve(Boolean(pong.ok));
|
|
337
|
+
}
|
|
338
|
+
catch {
|
|
339
|
+
resolve(true); // Connected but no ping support — still usable
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
setTimeout(() => {
|
|
343
|
+
if (!this.connected) {
|
|
344
|
+
this.socket?.destroy();
|
|
345
|
+
resolve(false);
|
|
346
|
+
}
|
|
347
|
+
}, KBotRemoteClient.CONNECT_TIMEOUT);
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
isConnected() {
|
|
351
|
+
return this.connected;
|
|
352
|
+
}
|
|
353
|
+
disconnect() {
|
|
354
|
+
this.connected = false;
|
|
355
|
+
if (this.socket) {
|
|
356
|
+
this.socket.destroy();
|
|
357
|
+
this.socket = null;
|
|
358
|
+
}
|
|
359
|
+
for (const [, req] of this.pending) {
|
|
360
|
+
clearTimeout(req.timer);
|
|
361
|
+
req.reject(new Error('Disconnected'));
|
|
362
|
+
}
|
|
363
|
+
this.pending.clear();
|
|
364
|
+
this.buffer = '';
|
|
365
|
+
}
|
|
366
|
+
async send(cmd) {
|
|
367
|
+
if (!this.connected || !this.socket) {
|
|
368
|
+
throw new Error('Not connected to KBotBridge Remote Script');
|
|
369
|
+
}
|
|
370
|
+
const id = this.nextId++;
|
|
371
|
+
const fullCmd = { id, ...cmd };
|
|
372
|
+
return new Promise((resolve, reject) => {
|
|
373
|
+
const timer = setTimeout(() => {
|
|
374
|
+
this.pending.delete(id);
|
|
375
|
+
reject(new Error(`Timeout: ${cmd.action ?? 'unknown'}`));
|
|
376
|
+
}, KBotRemoteClient.TIMEOUT);
|
|
377
|
+
this.pending.set(id, { resolve, reject, timer });
|
|
378
|
+
this.socket.write(JSON.stringify(fullCmd) + '\n');
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
/** Load a device by name via the kbot Remote Script's search. */
|
|
382
|
+
async loadDevice(trackIndex, name) {
|
|
383
|
+
const resp = await this.send({ action: 'load_device', track: trackIndex, name });
|
|
384
|
+
return Boolean(resp.ok);
|
|
385
|
+
}
|
|
386
|
+
/** Search the browser via the kbot Remote Script. */
|
|
387
|
+
async searchBrowser(query) {
|
|
388
|
+
const resp = await this.send({ action: 'search_browser', query });
|
|
389
|
+
const items = resp.results;
|
|
390
|
+
if (!Array.isArray(items))
|
|
391
|
+
return [];
|
|
392
|
+
return items.map((item) => ({
|
|
393
|
+
name: String(item.name ?? ''),
|
|
394
|
+
uri: String(item.uri ?? ''),
|
|
395
|
+
isLoadable: Boolean(item.is_loadable ?? false),
|
|
396
|
+
isDevice: Boolean(item.is_device ?? false),
|
|
397
|
+
isFolder: Boolean(item.is_folder ?? false),
|
|
398
|
+
}));
|
|
399
|
+
}
|
|
400
|
+
handleResponse(response) {
|
|
401
|
+
const id = response.id;
|
|
402
|
+
if (id && this.pending.has(id)) {
|
|
403
|
+
const req = this.pending.get(id);
|
|
404
|
+
this.pending.delete(id);
|
|
405
|
+
clearTimeout(req.timer);
|
|
406
|
+
req.resolve(response);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
handleDisconnect() {
|
|
410
|
+
if (!this.connected)
|
|
411
|
+
return;
|
|
412
|
+
this.connected = false;
|
|
413
|
+
this.socket = null;
|
|
414
|
+
for (const [, req] of this.pending) {
|
|
415
|
+
clearTimeout(req.timer);
|
|
416
|
+
req.reject(new Error('Connection lost'));
|
|
417
|
+
}
|
|
418
|
+
this.pending.clear();
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
// ── Convenience exports ────────────────────────────────────────────────
|
|
422
|
+
/**
|
|
423
|
+
* Try to connect to AbletonBridge (port 9001).
|
|
424
|
+
* Returns the connected client or null if unavailable.
|
|
425
|
+
*/
|
|
426
|
+
export async function tryAbletonBridge() {
|
|
427
|
+
const client = AbletonBridgeClient.getInstance();
|
|
428
|
+
if (client.isConnected())
|
|
429
|
+
return client;
|
|
430
|
+
const ok = await client.connect();
|
|
431
|
+
return ok ? client : null;
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* Try to connect to KBotBridge Remote Script (port 9998).
|
|
435
|
+
* Returns the connected client or null if unavailable.
|
|
436
|
+
*/
|
|
437
|
+
export async function tryKBotRemote() {
|
|
438
|
+
const client = KBotRemoteClient.getInstance();
|
|
439
|
+
if (client.isConnected())
|
|
440
|
+
return client;
|
|
441
|
+
const ok = await client.connect();
|
|
442
|
+
return ok ? client : null;
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* Get any available bridge, trying AbletonBridge first, then KBotBridge.
|
|
446
|
+
* Returns { bridge, type } or null if neither is available.
|
|
447
|
+
*/
|
|
448
|
+
export async function getAvailableBridge() {
|
|
449
|
+
// Try AbletonBridge first (full browser API)
|
|
450
|
+
const ab = await tryAbletonBridge();
|
|
451
|
+
if (ab)
|
|
452
|
+
return { bridge: ab, type: 'ableton-bridge' };
|
|
453
|
+
// Fallback to KBotBridge Remote Script
|
|
454
|
+
const kb = await tryKBotRemote();
|
|
455
|
+
if (kb)
|
|
456
|
+
return { bridge: kb, type: 'kbot-remote' };
|
|
457
|
+
return null;
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* Format a helpful error message when no bridge is available.
|
|
461
|
+
*/
|
|
462
|
+
export function formatBridgeError() {
|
|
463
|
+
return [
|
|
464
|
+
'**No Ableton bridge connected**',
|
|
465
|
+
'',
|
|
466
|
+
'kbot tried two connection methods and neither is available:',
|
|
467
|
+
'',
|
|
468
|
+
'**Option 1 — AbletonBridge (recommended)**',
|
|
469
|
+
' Full browser API with 353 tools. Install:',
|
|
470
|
+
' 1. Download from https://github.com/hidingwill/AbletonBridge',
|
|
471
|
+
' 2. Copy the `AbletonBridge` folder to your Remote Scripts:',
|
|
472
|
+
' macOS: ~/Music/Ableton/User Library/Remote Scripts/',
|
|
473
|
+
' Win: ~\\Documents\\Ableton\\User Library\\Remote Scripts\\',
|
|
474
|
+
' 3. In Ableton: Preferences → Link/Tempo/MIDI → Control Surface → AbletonBridge',
|
|
475
|
+
' 4. Verify: TCP server starts on localhost:9001',
|
|
476
|
+
'',
|
|
477
|
+
'**Option 2 — KBotBridge**',
|
|
478
|
+
' kbot\'s own Remote Script. Install:',
|
|
479
|
+
' 1. Run `kbot ableton install` or copy KBotBridge to Remote Scripts',
|
|
480
|
+
' 2. Enable in Ableton: Preferences → Link/Tempo/MIDI → Control Surface → KBotBridge',
|
|
481
|
+
' 3. Verify: TCP server starts on localhost:9998',
|
|
482
|
+
'',
|
|
483
|
+
'Both require Ableton Live to be running.',
|
|
484
|
+
].join('\n');
|
|
485
|
+
}
|
|
486
|
+
//# sourceMappingURL=ableton-bridge.js.map
|
|
@@ -112,13 +112,107 @@ export declare class AbletonM4L {
|
|
|
112
112
|
/** Generic LOM method call — call any method at any path */
|
|
113
113
|
lomCall(path: string, method: string, args?: unknown[]): Promise<M4LResponse>;
|
|
114
114
|
}
|
|
115
|
+
export interface BrowserSearchResult {
|
|
116
|
+
name: string;
|
|
117
|
+
uri: string;
|
|
118
|
+
is_loadable: boolean;
|
|
119
|
+
is_device: boolean;
|
|
120
|
+
}
|
|
121
|
+
export interface BrowserCategory {
|
|
122
|
+
name: string;
|
|
123
|
+
display_name: string;
|
|
124
|
+
child_count: number;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Client for the KBotBridge Remote Script (TCP 9998).
|
|
128
|
+
*
|
|
129
|
+
* This is separate from the M4L bridge (9999) because the Browser API
|
|
130
|
+
* (browser.load_item) is ONLY available from Python Remote Scripts,
|
|
131
|
+
* not from Max for Live.
|
|
132
|
+
*
|
|
133
|
+
* Use this to programmatically load any native device (Saturator,
|
|
134
|
+
* EQ Eight, Compressor, etc.) onto any track.
|
|
135
|
+
*/
|
|
136
|
+
export declare class AbletonBrowserBridge {
|
|
137
|
+
private static instance;
|
|
138
|
+
private socket;
|
|
139
|
+
private connected;
|
|
140
|
+
private pending;
|
|
141
|
+
private nextId;
|
|
142
|
+
private buffer;
|
|
143
|
+
static PORT: number;
|
|
144
|
+
static HOST: string;
|
|
145
|
+
static TIMEOUT: number;
|
|
146
|
+
private constructor();
|
|
147
|
+
static getInstance(): AbletonBrowserBridge;
|
|
148
|
+
/**
|
|
149
|
+
* Connect to the KBotBridge Remote Script on port 9998.
|
|
150
|
+
* Returns true if connected and the bridge responds to ping.
|
|
151
|
+
*/
|
|
152
|
+
connect(): Promise<boolean>;
|
|
153
|
+
disconnect(): void;
|
|
154
|
+
send(cmd: Omit<M4LCommand, 'id'> & {
|
|
155
|
+
action: string;
|
|
156
|
+
}): Promise<M4LResponse>;
|
|
157
|
+
get isConnected(): boolean;
|
|
158
|
+
private handleResponse;
|
|
159
|
+
private handleDisconnect;
|
|
160
|
+
ping(): Promise<boolean>;
|
|
161
|
+
/**
|
|
162
|
+
* Search Ableton's browser for items matching a query.
|
|
163
|
+
* @param query - Search string (case-insensitive)
|
|
164
|
+
* @param category - instruments/audio_effects/midi_effects/drums/samples/all
|
|
165
|
+
*/
|
|
166
|
+
browserSearch(query: string, category?: string): Promise<BrowserSearchResult[]>;
|
|
167
|
+
/**
|
|
168
|
+
* Load a browser item by URI onto a track.
|
|
169
|
+
* Use the URI from a browserSearch() result.
|
|
170
|
+
*/
|
|
171
|
+
browserLoad(track: number, uri: string): Promise<M4LResponse>;
|
|
172
|
+
/**
|
|
173
|
+
* Search + load in one step. Finds first loadable match and loads it.
|
|
174
|
+
* @param track - 0-indexed track number
|
|
175
|
+
* @param name - Device name to search for (e.g., "Saturator", "EQ Eight")
|
|
176
|
+
* @param category - instruments/audio_effects/midi_effects/drums/samples/all
|
|
177
|
+
*/
|
|
178
|
+
browserLoadByName(track: number, name: string, category?: string): Promise<M4LResponse>;
|
|
179
|
+
/**
|
|
180
|
+
* List top-level browser categories with child counts.
|
|
181
|
+
*/
|
|
182
|
+
browserCategories(): Promise<BrowserCategory[]>;
|
|
183
|
+
/**
|
|
184
|
+
* List all tracks with names and device counts.
|
|
185
|
+
*/
|
|
186
|
+
listTracks(): Promise<M4LResponse>;
|
|
187
|
+
/**
|
|
188
|
+
* List devices on a track with full parameter details.
|
|
189
|
+
*/
|
|
190
|
+
listDevices(track: number): Promise<M4LResponse>;
|
|
191
|
+
}
|
|
115
192
|
/**
|
|
116
193
|
* Get a connected M4L bridge instance.
|
|
117
194
|
* Throws if the bridge is not available.
|
|
118
195
|
*/
|
|
119
196
|
export declare function ensureM4L(): Promise<AbletonM4L>;
|
|
197
|
+
/**
|
|
198
|
+
* Get a connected Browser bridge instance (KBotBridge Remote Script on port 9998).
|
|
199
|
+
* Throws if not available.
|
|
200
|
+
*/
|
|
201
|
+
export declare function ensureBrowserBridge(): Promise<AbletonBrowserBridge>;
|
|
202
|
+
/**
|
|
203
|
+
* Connect to both M4L bridge (9999) and Browser bridge (9998).
|
|
204
|
+
* Returns whichever connections succeed. At least one must connect.
|
|
205
|
+
*/
|
|
206
|
+
export declare function connectBrowser(): Promise<{
|
|
207
|
+
m4l: AbletonM4L | null;
|
|
208
|
+
browser: AbletonBrowserBridge | null;
|
|
209
|
+
}>;
|
|
120
210
|
/**
|
|
121
211
|
* Format a friendly error message for M4L connection failures.
|
|
122
212
|
*/
|
|
123
213
|
export declare function formatM4LError(): string;
|
|
214
|
+
/**
|
|
215
|
+
* Format a friendly error message for Browser bridge connection failures.
|
|
216
|
+
*/
|
|
217
|
+
export declare function formatBrowserBridgeError(): string;
|
|
124
218
|
//# sourceMappingURL=ableton-m4l.d.ts.map
|