@kernel.chat/kbot 3.70.0 → 3.72.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.
@@ -0,0 +1,327 @@
1
+ /**
2
+ * ableton-bridge-tools.ts — Ableton Browser & Device Loading Tools
3
+ *
4
+ * Uses AbletonBridge (port 9001) for full Ableton Browser API access.
5
+ * Falls back to KBotBridge (port 9998) if AbletonBridge is unavailable.
6
+ *
7
+ * Tools:
8
+ * ableton_load_effect — Load any Ableton native effect by name onto a track
9
+ * ableton_browse — Search Ableton's browser (instruments, effects, presets, samples)
10
+ * ableton_load_preset — Load a preset onto a device
11
+ * ableton_effect_chain — Apply a full chain of effects to a track in sequence
12
+ */
13
+ import { registerTool } from './index.js';
14
+ import { tryAbletonBridge, tryKBotRemote, formatBridgeError, } from '../integrations/ableton-bridge.js';
15
+ // ── Helpers ─────────────────────────────────────────────────────────────
16
+ /** Convert 1-based user track to 0-based internal index. */
17
+ function userTrack(track) {
18
+ const n = Number(track);
19
+ return Math.max(0, n - 1);
20
+ }
21
+ /** Category aliases for user convenience. */
22
+ const CATEGORY_ALIASES = {
23
+ effects: 'audio_effects',
24
+ effect: 'audio_effects',
25
+ fx: 'audio_effects',
26
+ audio_fx: 'audio_effects',
27
+ midi_fx: 'midi_effects',
28
+ midi: 'midi_effects',
29
+ inst: 'instruments',
30
+ instrument: 'instruments',
31
+ drum: 'drums',
32
+ kit: 'drums',
33
+ sound: 'sounds',
34
+ pack: 'packs',
35
+ plugin: 'plugins',
36
+ vst: 'plugins',
37
+ au: 'plugins',
38
+ sample: 'samples',
39
+ preset: 'presets',
40
+ };
41
+ function resolveCategory(raw) {
42
+ if (!raw)
43
+ return undefined;
44
+ const lower = raw.toLowerCase().trim();
45
+ return CATEGORY_ALIASES[lower] ?? lower;
46
+ }
47
+ /** Format browser items for display. */
48
+ function formatBrowserItems(items, limit = 20) {
49
+ if (items.length === 0)
50
+ return 'No results found.';
51
+ const shown = items.slice(0, limit);
52
+ const lines = shown.map((item) => {
53
+ const tags = [];
54
+ if (item.isDevice)
55
+ tags.push('device');
56
+ if (item.isFolder)
57
+ tags.push('folder');
58
+ if (item.isLoadable)
59
+ tags.push('loadable');
60
+ const tagStr = tags.length > 0 ? ` [${tags.join(', ')}]` : '';
61
+ return `- **${item.name}**${tagStr}\n URI: \`${item.uri}\``;
62
+ });
63
+ let result = lines.join('\n');
64
+ if (items.length > limit) {
65
+ result += `\n\n_...and ${items.length - limit} more results_`;
66
+ }
67
+ return result;
68
+ }
69
+ // ── Tool Registration ───────────────────────────────────────────────────
70
+ export function registerAbletonBridgeTools() {
71
+ // ─── 1. Load Effect ───────────────────────────────────────────────────
72
+ registerTool({
73
+ name: 'ableton_load_effect',
74
+ description: 'Load any Ableton native audio effect by name onto a track. ' +
75
+ 'This is the primary tool for adding effects like Saturator, Reverb, Compressor, EQ Eight, Auto Filter, etc. ' +
76
+ 'Searches Ableton\'s browser via AbletonBridge and loads the device directly. ' +
77
+ 'Supports position control to place the effect before or after existing devices.',
78
+ parameters: {
79
+ track: { type: 'number', description: 'Track number (1-based)', required: true },
80
+ name: { type: 'string', description: 'Effect name (e.g. "Saturator", "Reverb", "Compressor", "EQ Eight", "Auto Filter", "Chorus-Ensemble")', required: true },
81
+ position: {
82
+ type: 'string',
83
+ description: 'Where to place the effect: "before" (start of chain), "after" (after last device), "end" (same as after). Default: "end"',
84
+ },
85
+ },
86
+ tier: 'free',
87
+ timeout: 15_000,
88
+ async execute(args) {
89
+ const t = userTrack(args.track);
90
+ const name = String(args.name).trim();
91
+ const position = String(args.position ?? 'end').toLowerCase();
92
+ try {
93
+ // Try AbletonBridge first (full browser API)
94
+ const ab = await tryAbletonBridge();
95
+ if (ab) {
96
+ // Search specifically in audio_effects category
97
+ const items = await ab.searchBrowser(name, 'audio_effects');
98
+ // Find exact name match first, then partial match
99
+ const exactMatch = items.find((item) => item.isLoadable && item.name.toLowerCase() === name.toLowerCase());
100
+ const partialMatch = items.find((item) => item.isLoadable && item.name.toLowerCase().includes(name.toLowerCase()));
101
+ const target = exactMatch ?? partialMatch;
102
+ if (!target) {
103
+ // Try broader search without category filter
104
+ const broadItems = await ab.searchBrowser(name);
105
+ const broadMatch = broadItems.find((item) => item.isLoadable && (item.isDevice || item.name.toLowerCase().includes(name.toLowerCase())));
106
+ if (broadMatch) {
107
+ await ab.loadDevice(t, broadMatch.uri);
108
+ return `Loaded **${broadMatch.name}** on track ${args.track} (via browser search)`;
109
+ }
110
+ return `Effect "${name}" not found in Ableton's browser. Check the exact name (e.g. "EQ Eight" not "EQ8").`;
111
+ }
112
+ await ab.loadDevice(t, target.uri);
113
+ // Handle position if not "end" — move device within chain
114
+ if (position === 'before') {
115
+ const chain = await ab.getEffectChain(t);
116
+ if (chain.length > 1) {
117
+ // The newly loaded device is at the end — note this for the user
118
+ return `Loaded **${target.name}** on track ${args.track} (at end of chain — ${chain.length} devices total). Note: position reordering requires manual adjustment in Ableton.`;
119
+ }
120
+ }
121
+ return `Loaded **${target.name}** on track ${args.track}`;
122
+ }
123
+ // Fallback to KBotBridge Remote Script
124
+ const kb = await tryKBotRemote();
125
+ if (kb) {
126
+ const ok = await kb.loadDevice(t, name);
127
+ if (ok) {
128
+ return `Loaded **${name}** on track ${args.track} (via KBotBridge)`;
129
+ }
130
+ return `KBotBridge could not load "${name}". The device may not be found in the browser.`;
131
+ }
132
+ // Neither bridge available
133
+ return formatBridgeError();
134
+ }
135
+ catch (err) {
136
+ return `Failed to load effect: ${err.message}\n\n${formatBridgeError()}`;
137
+ }
138
+ },
139
+ });
140
+ // ─── 2. Browse ────────────────────────────────────────────────────────
141
+ registerTool({
142
+ name: 'ableton_browse',
143
+ description: 'Search Ableton\'s browser for instruments, effects, presets, samples, packs, and plugins. ' +
144
+ 'Returns matching items with their URIs for loading. ' +
145
+ 'Use category to narrow results: "instruments", "audio_effects", "midi_effects", "drums", "sounds", "packs", "plugins", "samples", "presets".',
146
+ parameters: {
147
+ query: { type: 'string', description: 'Search query (e.g. "reverb", "piano", "808")', required: true },
148
+ category: {
149
+ type: 'string',
150
+ description: 'Category filter: instruments, audio_effects (or "fx"), midi_effects (or "midi_fx"), drums, sounds, packs, plugins (or "vst"), samples, presets',
151
+ },
152
+ },
153
+ tier: 'free',
154
+ timeout: 15_000,
155
+ async execute(args) {
156
+ const query = String(args.query).trim();
157
+ const category = resolveCategory(args.category);
158
+ try {
159
+ // Try AbletonBridge first
160
+ const ab = await tryAbletonBridge();
161
+ if (ab) {
162
+ const items = await ab.searchBrowser(query, category);
163
+ const header = category
164
+ ? `## Browser Search: "${query}" in ${category}`
165
+ : `## Browser Search: "${query}"`;
166
+ return `${header}\n\n${formatBrowserItems(items)}`;
167
+ }
168
+ // Fallback to KBotBridge
169
+ const kb = await tryKBotRemote();
170
+ if (kb) {
171
+ const items = await kb.searchBrowser(query);
172
+ return `## Browser Search: "${query}" (via KBotBridge)\n\n${formatBrowserItems(items)}`;
173
+ }
174
+ return formatBridgeError();
175
+ }
176
+ catch (err) {
177
+ return `Browse failed: ${err.message}\n\n${formatBridgeError()}`;
178
+ }
179
+ },
180
+ });
181
+ // ─── 3. Load Preset ───────────────────────────────────────────────────
182
+ registerTool({
183
+ name: 'ableton_load_preset',
184
+ description: 'Load a preset onto a device on a track. Searches available presets for the device and loads the best match. ' +
185
+ 'Use ableton_browse first to find the device URI if needed.',
186
+ parameters: {
187
+ track: { type: 'number', description: 'Track number (1-based)', required: true },
188
+ device: { type: 'number', description: 'Device index on the track (0-based, first device = 0)', required: true },
189
+ preset_name: { type: 'string', description: 'Preset name to search for (e.g. "Warm Pad", "Clean Lead")', required: true },
190
+ },
191
+ tier: 'free',
192
+ timeout: 15_000,
193
+ async execute(args) {
194
+ const t = userTrack(args.track);
195
+ const deviceIdx = Number(args.device);
196
+ const presetName = String(args.preset_name).trim();
197
+ try {
198
+ const ab = await tryAbletonBridge();
199
+ if (!ab) {
200
+ return 'Preset loading requires AbletonBridge (port 9001). KBotBridge does not support preset browsing.\n\n' + formatBridgeError();
201
+ }
202
+ // Get the device chain to find the device URI
203
+ const chain = await ab.getEffectChain(t);
204
+ if (chain.length === 0) {
205
+ return `No devices on track ${args.track}. Load a device first with ableton_load_effect.`;
206
+ }
207
+ if (deviceIdx >= chain.length) {
208
+ return `Track ${args.track} has ${chain.length} device(s) (indices 0-${chain.length - 1}). Device index ${deviceIdx} is out of range.`;
209
+ }
210
+ const device = chain[deviceIdx];
211
+ // Search for presets matching the device + preset name
212
+ const presetItems = await ab.searchBrowser(`${device.name} ${presetName}`, 'presets');
213
+ // Try to find a matching preset
214
+ const exactMatch = presetItems.find((p) => p.isLoadable && p.name.toLowerCase() === presetName.toLowerCase());
215
+ const partialMatch = presetItems.find((p) => p.isLoadable && p.name.toLowerCase().includes(presetName.toLowerCase()));
216
+ const target = exactMatch ?? partialMatch ?? presetItems.find((p) => p.isLoadable);
217
+ if (!target) {
218
+ // List available presets for the device
219
+ const devicePresets = await ab.searchBrowser(device.name, 'presets');
220
+ if (devicePresets.length > 0) {
221
+ const presetList = devicePresets
222
+ .filter((p) => p.isLoadable)
223
+ .slice(0, 10)
224
+ .map((p) => ` - ${p.name}`)
225
+ .join('\n');
226
+ return `No preset matching "${presetName}" for ${device.name}.\n\nAvailable presets:\n${presetList}`;
227
+ }
228
+ return `No presets found for "${presetName}" on ${device.name} (device ${deviceIdx}).`;
229
+ }
230
+ await ab.loadPreset(t, deviceIdx, target.uri);
231
+ return `Loaded preset **${target.name}** onto **${device.name}** (track ${args.track}, device ${deviceIdx})`;
232
+ }
233
+ catch (err) {
234
+ return `Failed to load preset: ${err.message}`;
235
+ }
236
+ },
237
+ });
238
+ // ─── 4. Effect Chain ──────────────────────────────────────────────────
239
+ registerTool({
240
+ name: 'ableton_effect_chain',
241
+ description: 'Apply a full chain of audio effects to a track in sequence. ' +
242
+ 'Loads each effect one by one from Ableton\'s browser. ' +
243
+ 'Great for setting up standard chains like "Compressor → EQ Eight → Saturator → Reverb".',
244
+ parameters: {
245
+ track: { type: 'number', description: 'Track number (1-based)', required: true },
246
+ chain: {
247
+ type: 'array',
248
+ description: 'Array of effect names to load in order (e.g. ["Compressor", "EQ Eight", "Saturator", "Reverb"])',
249
+ required: true,
250
+ items: { type: 'string' },
251
+ },
252
+ },
253
+ tier: 'free',
254
+ timeout: 60_000,
255
+ async execute(args) {
256
+ const t = userTrack(args.track);
257
+ const chain = args.chain;
258
+ if (!Array.isArray(chain) || chain.length === 0) {
259
+ return 'Error: `chain` must be an array of effect names (e.g. ["Compressor", "EQ Eight", "Reverb"]).';
260
+ }
261
+ const results = [`## Effect Chain → Track ${args.track}`, ''];
262
+ let loaded = 0;
263
+ let failed = 0;
264
+ try {
265
+ // Try AbletonBridge first
266
+ const ab = await tryAbletonBridge();
267
+ if (ab) {
268
+ for (const effectName of chain) {
269
+ const name = String(effectName).trim();
270
+ try {
271
+ const items = await ab.searchBrowser(name, 'audio_effects');
272
+ const exactMatch = items.find((item) => item.isLoadable && item.name.toLowerCase() === name.toLowerCase());
273
+ const partialMatch = items.find((item) => item.isLoadable && item.name.toLowerCase().includes(name.toLowerCase()));
274
+ const target = exactMatch ?? partialMatch;
275
+ if (target) {
276
+ await ab.loadDevice(t, target.uri);
277
+ results.push(`- **${target.name}** loaded`);
278
+ loaded++;
279
+ }
280
+ else {
281
+ results.push(`- **${name}** — not found in browser`);
282
+ failed++;
283
+ }
284
+ }
285
+ catch (err) {
286
+ results.push(`- **${name}** — error: ${err.message}`);
287
+ failed++;
288
+ }
289
+ }
290
+ results.push('');
291
+ results.push(`**${loaded}** loaded, **${failed}** failed out of ${chain.length} effects.`);
292
+ return results.join('\n');
293
+ }
294
+ // Fallback to KBotBridge — try loading each effect
295
+ const kb = await tryKBotRemote();
296
+ if (kb) {
297
+ for (const effectName of chain) {
298
+ const name = String(effectName).trim();
299
+ try {
300
+ const ok = await kb.loadDevice(t, name);
301
+ if (ok) {
302
+ results.push(`- **${name}** loaded`);
303
+ loaded++;
304
+ }
305
+ else {
306
+ results.push(`- **${name}** — not found`);
307
+ failed++;
308
+ }
309
+ }
310
+ catch (err) {
311
+ results.push(`- **${name}** — error: ${err.message}`);
312
+ failed++;
313
+ }
314
+ }
315
+ results.push('');
316
+ results.push(`**${loaded}** loaded, **${failed}** failed out of ${chain.length} effects (via KBotBridge).`);
317
+ return results.join('\n');
318
+ }
319
+ return formatBridgeError();
320
+ }
321
+ catch (err) {
322
+ return `Effect chain failed: ${err.message}\n\n${formatBridgeError()}`;
323
+ }
324
+ },
325
+ });
326
+ }
327
+ //# sourceMappingURL=ableton-bridge-tools.js.map
@@ -279,6 +279,7 @@ const LAZY_MODULE_IMPORTS = [
279
279
  { path: './lab-frontier.js', registerFn: 'registerFrontierTools' },
280
280
  { path: './ableton.js', registerFn: 'registerAbletonTools' },
281
281
  { path: './ableton-knowledge.js', registerFn: 'registerAbletonKnowledgeTools' },
282
+ { path: './ableton-bridge-tools.js', registerFn: 'registerAbletonBridgeTools' },
282
283
  { path: './producer-engine.js', registerFn: 'registerProducerEngine' },
283
284
  { path: './sound-designer.js', registerFn: 'registerSoundDesignerTools' },
284
285
  { path: './arrangement-engine.js', registerFn: 'registerArrangementEngine' },
@@ -310,6 +311,8 @@ const LAZY_MODULE_IMPORTS = [
310
311
  { path: './financial-analysis.js', registerFn: 'registerFinancialAnalysisTools' },
311
312
  { path: './ai-analysis.js', registerFn: 'registerAIAnalysisTools' },
312
313
  { path: './music-gen.js', registerFn: 'registerMusicGenTools' },
314
+ { path: './mobile-automation.js', registerFn: 'registerMobileAutomationTools' },
315
+ { path: './iphone.js', registerFn: 'registerIPhoneTools' },
313
316
  ];
314
317
  /** Track whether lazy tools have been registered */
315
318
  let lazyToolsRegistered = false;
@@ -0,0 +1,2 @@
1
+ export declare function registerIPhoneTools(): void;
2
+ //# sourceMappingURL=iphone.d.ts.map