@kernel.chat/kbot 3.48.0 → 3.50.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.
Files changed (51) hide show
  1. package/dist/agent-teams.d.ts.map +1 -1
  2. package/dist/agent-teams.js +7 -0
  3. package/dist/agent-teams.js.map +1 -1
  4. package/dist/agents/producer.d.ts +21 -0
  5. package/dist/agents/producer.d.ts.map +1 -0
  6. package/dist/agents/producer.js +139 -0
  7. package/dist/agents/producer.js.map +1 -0
  8. package/dist/agents/specialists.d.ts.map +1 -1
  9. package/dist/agents/specialists.js +19 -0
  10. package/dist/agents/specialists.js.map +1 -1
  11. package/dist/completions.d.ts.map +1 -1
  12. package/dist/completions.js +1 -0
  13. package/dist/completions.js.map +1 -1
  14. package/dist/integrations/ableton-osc.d.ts +146 -0
  15. package/dist/integrations/ableton-osc.d.ts.map +1 -0
  16. package/dist/integrations/ableton-osc.js +590 -0
  17. package/dist/integrations/ableton-osc.js.map +1 -0
  18. package/dist/learned-router.d.ts.map +1 -1
  19. package/dist/learned-router.js +20 -0
  20. package/dist/learned-router.js.map +1 -1
  21. package/dist/tools/ableton-knowledge.d.ts +2 -0
  22. package/dist/tools/ableton-knowledge.d.ts.map +1 -0
  23. package/dist/tools/ableton-knowledge.js +419 -0
  24. package/dist/tools/ableton-knowledge.js.map +1 -0
  25. package/dist/tools/ableton.d.ts +2 -0
  26. package/dist/tools/ableton.d.ts.map +1 -0
  27. package/dist/tools/ableton.js +769 -0
  28. package/dist/tools/ableton.js.map +1 -0
  29. package/dist/tools/index.d.ts.map +1 -1
  30. package/dist/tools/index.js +3 -0
  31. package/dist/tools/index.js.map +1 -1
  32. package/dist/tools/lab-frontier.d.ts +2 -0
  33. package/dist/tools/lab-frontier.d.ts.map +1 -0
  34. package/dist/tools/lab-frontier.js +926 -0
  35. package/dist/tools/lab-frontier.js.map +1 -0
  36. package/dist/tools/lab-physics.d.ts.map +1 -1
  37. package/dist/tools/lab-physics.js +2 -1
  38. package/dist/tools/lab-physics.js.map +1 -1
  39. package/dist/tools/music-theory.d.ts +175 -0
  40. package/dist/tools/music-theory.d.ts.map +1 -0
  41. package/dist/tools/music-theory.js +1020 -0
  42. package/dist/tools/music-theory.js.map +1 -0
  43. package/package.json +1 -1
  44. package/dist/tools/image-variation.d.ts +0 -2
  45. package/dist/tools/image-variation.d.ts.map +0 -1
  46. package/dist/tools/image-variation.js +0 -30
  47. package/dist/tools/image-variation.js.map +0 -1
  48. package/dist/tools/summarize.d.ts +0 -2
  49. package/dist/tools/summarize.d.ts.map +0 -1
  50. package/dist/tools/summarize.js +0 -28
  51. package/dist/tools/summarize.js.map +0 -1
@@ -0,0 +1,769 @@
1
+ // kbot Ableton Live Tools — Natural language DAW control over OSC
2
+ // Zero external dependencies. Talks to AbletonOSC via UDP on localhost.
3
+ //
4
+ // Tools:
5
+ // ableton_transport — play, stop, record, tempo, time sig, seek
6
+ // ableton_track — list, create, mute, solo, arm, volume, pan, rename, delete
7
+ // ableton_clip — fire, stop, create, delete, duplicate, info
8
+ // ableton_scene — fire, list, create, duplicate
9
+ // ableton_midi — write/read/clear MIDI notes in clips
10
+ // ableton_device — list, get/set params, enable/disable devices
11
+ // ableton_mixer — snapshot levels, batch set, sends
12
+ // ableton_create_progression — chord progressions → MIDI in clips
13
+ // ableton_session_info — full session state snapshot
14
+ // ableton_knowledge — deep Ableton knowledge base queries (registered in ableton-knowledge.ts)
15
+ //
16
+ // Requires: AbletonOSC loaded in Ableton Live (Preferences → Link/Tempo/MIDI → Control Surface)
17
+ import { registerTool } from './index.js';
18
+ import { ensureAbleton, formatAbletonError } from '../integrations/ableton-osc.js';
19
+ import { parseProgression, voiceChord, arpeggiate, NAMED_PROGRESSIONS, RHYTHM_PATTERNS, noteNameToMidi, midiToNoteName, } from './music-theory.js';
20
+ // ── Helpers ─────────────────────────────────────────────────────────────────
21
+ function extractArgs(args) {
22
+ return args.map(a => {
23
+ if (a.type === 'b')
24
+ return '[blob]';
25
+ return a.value;
26
+ });
27
+ }
28
+ function userTrack(track) {
29
+ // Users say "track 1" (1-based), OSC uses 0-based
30
+ const n = Number(track);
31
+ return Math.max(0, n - 1);
32
+ }
33
+ function displayTrack(oscIndex) {
34
+ return oscIndex + 1;
35
+ }
36
+ // ── Tool Registration ───────────────────────────────────────────────────────
37
+ export function registerAbletonTools() {
38
+ // ─── 1. Transport ─────────────────────────────────────────────────────
39
+ registerTool({
40
+ name: 'ableton_transport',
41
+ description: 'Control Ableton Live transport — play, stop, record, set tempo, time signature, and seek position. Requires AbletonOSC running in Live.',
42
+ parameters: {
43
+ action: { type: 'string', description: 'Action: "play", "stop", "record", "toggle", "tempo", "time_sig", "position", "status"', required: true },
44
+ value: { type: 'number', description: 'BPM for tempo, beat position for seek, numerator for time_sig' },
45
+ value2: { type: 'number', description: 'Denominator for time_sig (e.g. 4 for x/4)' },
46
+ },
47
+ tier: 'free',
48
+ timeout: 10_000,
49
+ async execute(args) {
50
+ const action = String(args.action).toLowerCase();
51
+ try {
52
+ const osc = await ensureAbleton();
53
+ switch (action) {
54
+ case 'play':
55
+ case 'start':
56
+ osc.send('/live/song/start_playing');
57
+ return '▶ Playing';
58
+ case 'stop':
59
+ osc.send('/live/song/stop_playing');
60
+ return '⏹ Stopped';
61
+ case 'record':
62
+ osc.send('/live/song/set/record_mode', 1);
63
+ osc.send('/live/song/start_playing');
64
+ return '⏺ Recording';
65
+ case 'toggle':
66
+ osc.send('/live/song/start_playing');
67
+ return '⏯ Toggled playback';
68
+ case 'tempo':
69
+ case 'bpm': {
70
+ const bpm = Number(args.value);
71
+ if (!bpm || bpm < 20 || bpm > 999)
72
+ return 'Error: BPM must be between 20 and 999';
73
+ osc.send('/live/song/set/tempo', bpm);
74
+ return `Tempo set to **${bpm} BPM**`;
75
+ }
76
+ case 'time_sig':
77
+ case 'time_signature': {
78
+ const num = Number(args.value) || 4;
79
+ const den = Number(args.value2) || 4;
80
+ osc.send('/live/song/set/signature_numerator', num);
81
+ osc.send('/live/song/set/signature_denominator', den);
82
+ return `Time signature set to **${num}/${den}**`;
83
+ }
84
+ case 'position':
85
+ case 'seek': {
86
+ const pos = Number(args.value) || 0;
87
+ osc.send('/live/song/set/current_song_time', pos);
88
+ return `Seeked to beat **${pos}**`;
89
+ }
90
+ case 'status': {
91
+ const tempo = await osc.query('/live/song/get/tempo');
92
+ const playing = await osc.query('/live/song/get/is_playing');
93
+ const recording = await osc.query('/live/song/get/record_mode');
94
+ return [
95
+ '## Transport Status',
96
+ `- Tempo: **${extractArgs(tempo)[0]} BPM**`,
97
+ `- Playing: ${extractArgs(playing)[0] ? '▶ Yes' : '⏹ No'}`,
98
+ `- Recording: ${extractArgs(recording)[0] ? '⏺ Yes' : 'No'}`,
99
+ ].join('\n');
100
+ }
101
+ default:
102
+ return `Unknown action "${action}". Options: play, stop, record, toggle, tempo, time_sig, position, status`;
103
+ }
104
+ }
105
+ catch (err) {
106
+ return `Ableton connection failed: ${err.message}\n\n${formatAbletonError()}`;
107
+ }
108
+ },
109
+ });
110
+ // ─── 2. Track Control ─────────────────────────────────────────────────
111
+ registerTool({
112
+ name: 'ableton_track',
113
+ description: 'Control Ableton Live tracks — list all tracks, mute, solo, arm, set volume/pan, rename, create, delete. Track numbers are 1-based (track 1 = first track).',
114
+ parameters: {
115
+ action: { type: 'string', description: 'Action: "list", "mute", "unmute", "solo", "unsolo", "arm", "disarm", "volume", "pan", "rename", "info"', required: true },
116
+ track: { type: 'number', description: 'Track number (1-based). Required for all actions except "list"' },
117
+ value: { type: 'string', description: 'Volume (0-1), pan (-1 to 1), or new name for rename' },
118
+ },
119
+ tier: 'free',
120
+ timeout: 15_000,
121
+ async execute(args) {
122
+ const action = String(args.action).toLowerCase();
123
+ try {
124
+ const osc = await ensureAbleton();
125
+ if (action === 'list') {
126
+ const countResult = await osc.query('/live/song/get/num_tracks');
127
+ const count = Number(extractArgs(countResult)[0]) || 0;
128
+ const lines = ['## Tracks', ''];
129
+ lines.push('| # | Name | Volume | Pan | Mute | Solo | Armed |');
130
+ lines.push('|---|------|--------|-----|------|------|-------|');
131
+ for (let i = 0; i < Math.min(count, 32); i++) {
132
+ try {
133
+ const name = await osc.query('/live/track/get/name', i);
134
+ const vol = await osc.query('/live/track/get/volume', i);
135
+ const pan = await osc.query('/live/track/get/panning', i);
136
+ const mute = await osc.query('/live/track/get/mute', i);
137
+ const solo = await osc.query('/live/track/get/solo', i);
138
+ const arm = await osc.query('/live/track/get/arm', i);
139
+ lines.push(`| ${i + 1} | ${extractArgs(name)[1] || '?'} | ${(Number(extractArgs(vol)[1]) * 100).toFixed(0)}% | ${Number(extractArgs(pan)[1]).toFixed(2)} | ${extractArgs(mute)[1] ? '🔇' : ''} | ${extractArgs(solo)[1] ? '🔊' : ''} | ${extractArgs(arm)[1] ? '⏺' : ''} |`);
140
+ }
141
+ catch { /* skip unreachable tracks */ }
142
+ }
143
+ return lines.join('\n');
144
+ }
145
+ const t = userTrack(args.track);
146
+ if (!args.track)
147
+ return 'Error: track number required (1-based)';
148
+ switch (action) {
149
+ case 'mute':
150
+ osc.send('/live/track/set/mute', t, 1);
151
+ return `Track ${args.track} muted 🔇`;
152
+ case 'unmute':
153
+ osc.send('/live/track/set/mute', t, 0);
154
+ return `Track ${args.track} unmuted`;
155
+ case 'solo':
156
+ osc.send('/live/track/set/solo', t, 1);
157
+ return `Track ${args.track} soloed 🔊`;
158
+ case 'unsolo':
159
+ osc.send('/live/track/set/solo', t, 0);
160
+ return `Track ${args.track} unsoloed`;
161
+ case 'arm':
162
+ osc.send('/live/track/set/arm', t, 1);
163
+ return `Track ${args.track} armed ⏺`;
164
+ case 'disarm':
165
+ osc.send('/live/track/set/arm', t, 0);
166
+ return `Track ${args.track} disarmed`;
167
+ case 'volume':
168
+ case 'vol': {
169
+ const v = Math.max(0, Math.min(1, Number(args.value)));
170
+ osc.send('/live/track/set/volume', t, v);
171
+ return `Track ${args.track} volume → **${(v * 100).toFixed(0)}%**`;
172
+ }
173
+ case 'pan': {
174
+ const p = Math.max(-1, Math.min(1, Number(args.value)));
175
+ osc.send('/live/track/set/panning', t, p);
176
+ const panLabel = p === 0 ? 'Center' : p < 0 ? `${Math.abs(p * 100).toFixed(0)}% Left` : `${(p * 100).toFixed(0)}% Right`;
177
+ return `Track ${args.track} pan → **${panLabel}**`;
178
+ }
179
+ case 'rename': {
180
+ const name = String(args.value || 'Track');
181
+ osc.send('/live/track/set/name', t, name);
182
+ return `Track ${args.track} renamed to **${name}**`;
183
+ }
184
+ case 'info': {
185
+ const name = await osc.query('/live/track/get/name', t);
186
+ const vol = await osc.query('/live/track/get/volume', t);
187
+ const pan = await osc.query('/live/track/get/panning', t);
188
+ const mute = await osc.query('/live/track/get/mute', t);
189
+ const solo = await osc.query('/live/track/get/solo', t);
190
+ const arm = await osc.query('/live/track/get/arm', t);
191
+ return [
192
+ `## Track ${args.track}: ${extractArgs(name)[1]}`,
193
+ `- Volume: ${(Number(extractArgs(vol)[1]) * 100).toFixed(0)}%`,
194
+ `- Pan: ${Number(extractArgs(pan)[1]).toFixed(2)}`,
195
+ `- Muted: ${extractArgs(mute)[1] ? 'Yes' : 'No'}`,
196
+ `- Soloed: ${extractArgs(solo)[1] ? 'Yes' : 'No'}`,
197
+ `- Armed: ${extractArgs(arm)[1] ? 'Yes' : 'No'}`,
198
+ ].join('\n');
199
+ }
200
+ default:
201
+ return `Unknown action "${action}". Options: list, mute, unmute, solo, unsolo, arm, disarm, volume, pan, rename, info`;
202
+ }
203
+ }
204
+ catch (err) {
205
+ return `Ableton connection failed: ${err.message}\n\n${formatAbletonError()}`;
206
+ }
207
+ },
208
+ });
209
+ // ─── 3. Clip Control ──────────────────────────────────────────────────
210
+ registerTool({
211
+ name: 'ableton_clip',
212
+ description: 'Control Ableton Live clips in Session View — fire, stop, create, delete, duplicate, get info. Track and clip numbers are 1-based.',
213
+ parameters: {
214
+ action: { type: 'string', description: '"fire", "stop", "create", "delete", "duplicate", "info", "list"', required: true },
215
+ track: { type: 'number', description: 'Track number (1-based)', required: true },
216
+ clip: { type: 'number', description: 'Clip slot number (1-based). Default: 1' },
217
+ name: { type: 'string', description: 'Clip name (for create)' },
218
+ length: { type: 'number', description: 'Clip length in beats (for create, default: 16 = 4 bars)' },
219
+ },
220
+ tier: 'free',
221
+ timeout: 10_000,
222
+ async execute(args) {
223
+ const action = String(args.action).toLowerCase();
224
+ const t = userTrack(args.track);
225
+ const c = Math.max(0, (Number(args.clip) || 1) - 1);
226
+ try {
227
+ const osc = await ensureAbleton();
228
+ switch (action) {
229
+ case 'fire':
230
+ case 'launch':
231
+ osc.send('/live/clip_slot/fire', t, c);
232
+ return `Fired clip ${args.clip || 1} on track ${args.track} ▶`;
233
+ case 'stop':
234
+ osc.send('/live/clip_slot/stop', t, c);
235
+ return `Stopped clip ${args.clip || 1} on track ${args.track} ⏹`;
236
+ case 'create': {
237
+ const length = Number(args.length) || 16;
238
+ const name = String(args.name || `Clip ${c + 1}`);
239
+ osc.send('/live/clip_slot/create_clip', t, c, length);
240
+ // Small delay for clip creation
241
+ await new Promise(r => setTimeout(r, 200));
242
+ osc.send('/live/clip/set/name', t, c, name);
243
+ return `Created clip **${name}** (${length} beats / ${length / 4} bars) on track ${args.track}, slot ${(c + 1)}`;
244
+ }
245
+ case 'delete':
246
+ osc.send('/live/clip_slot/delete_clip', t, c);
247
+ return `Deleted clip in slot ${c + 1} on track ${args.track}`;
248
+ case 'duplicate':
249
+ osc.send('/live/clip_slot/duplicate_clip_to', t, c, t, c + 1);
250
+ return `Duplicated clip to slot ${c + 2} on track ${args.track}`;
251
+ case 'info': {
252
+ const name = await osc.query('/live/clip/get/name', t, c);
253
+ const length = await osc.query('/live/clip/get/length', t, c);
254
+ const looping = await osc.query('/live/clip/get/looping', t, c);
255
+ return [
256
+ `## Clip Info — Track ${args.track}, Slot ${c + 1}`,
257
+ `- Name: ${extractArgs(name)[2] || '?'}`,
258
+ `- Length: ${extractArgs(length)[2] || '?'} beats`,
259
+ `- Looping: ${extractArgs(looping)[2] ? 'Yes' : 'No'}`,
260
+ ].join('\n');
261
+ }
262
+ case 'list': {
263
+ const lines = [`## Clips on Track ${args.track}`, ''];
264
+ for (let i = 0; i < 16; i++) {
265
+ try {
266
+ const hasClip = await osc.query('/live/clip_slot/get/has_clip', t, i);
267
+ if (extractArgs(hasClip)[2]) {
268
+ const name = await osc.query('/live/clip/get/name', t, i);
269
+ lines.push(`- Slot ${i + 1}: **${extractArgs(name)[2] || 'Unnamed'}**`);
270
+ }
271
+ }
272
+ catch {
273
+ break;
274
+ }
275
+ }
276
+ if (lines.length === 2)
277
+ lines.push('No clips found');
278
+ return lines.join('\n');
279
+ }
280
+ default:
281
+ return `Unknown action "${action}". Options: fire, stop, create, delete, duplicate, info, list`;
282
+ }
283
+ }
284
+ catch (err) {
285
+ return `Ableton connection failed: ${err.message}\n\n${formatAbletonError()}`;
286
+ }
287
+ },
288
+ });
289
+ // ─── 4. Scene Control ─────────────────────────────────────────────────
290
+ registerTool({
291
+ name: 'ableton_scene',
292
+ description: 'Control Ableton Live scenes — fire (launch all clips in a row), list scenes, create, duplicate.',
293
+ parameters: {
294
+ action: { type: 'string', description: '"fire", "list", "create", "duplicate", "rename"', required: true },
295
+ scene: { type: 'number', description: 'Scene number (1-based)' },
296
+ name: { type: 'string', description: 'Scene name (for create/rename)' },
297
+ },
298
+ tier: 'free',
299
+ timeout: 10_000,
300
+ async execute(args) {
301
+ const action = String(args.action).toLowerCase();
302
+ try {
303
+ const osc = await ensureAbleton();
304
+ switch (action) {
305
+ case 'fire':
306
+ case 'launch': {
307
+ const s = Math.max(0, (Number(args.scene) || 1) - 1);
308
+ osc.send('/live/scene/fire', s);
309
+ return `Fired scene ${(s + 1)} ▶`;
310
+ }
311
+ case 'list': {
312
+ const countResult = await osc.query('/live/song/get/num_scenes');
313
+ const count = Number(extractArgs(countResult)[0]) || 0;
314
+ const lines = ['## Scenes', ''];
315
+ for (let i = 0; i < Math.min(count, 32); i++) {
316
+ try {
317
+ const name = await osc.query('/live/scene/get/name', i);
318
+ lines.push(`- Scene ${i + 1}: **${extractArgs(name)[1] || 'Unnamed'}**`);
319
+ }
320
+ catch {
321
+ break;
322
+ }
323
+ }
324
+ return lines.join('\n');
325
+ }
326
+ case 'create':
327
+ osc.send('/live/song/create_scene', -1);
328
+ return `Created new scene`;
329
+ case 'rename': {
330
+ const s = Math.max(0, (Number(args.scene) || 1) - 1);
331
+ osc.send('/live/scene/set/name', s, String(args.name || 'Scene'));
332
+ return `Renamed scene ${s + 1} to **${args.name}**`;
333
+ }
334
+ default:
335
+ return `Unknown action "${action}". Options: fire, list, create, rename`;
336
+ }
337
+ }
338
+ catch (err) {
339
+ return `Ableton connection failed: ${err.message}\n\n${formatAbletonError()}`;
340
+ }
341
+ },
342
+ });
343
+ // ─── 5. MIDI Note Writing ─────────────────────────────────────────────
344
+ registerTool({
345
+ name: 'ableton_midi',
346
+ description: 'Write, read, or clear MIDI notes in Ableton Live clips. Can write individual notes, chords, or full patterns. Track and clip numbers are 1-based.',
347
+ parameters: {
348
+ action: { type: 'string', description: '"write", "read", "clear". Default: write', },
349
+ track: { type: 'number', description: 'Track number (1-based)', required: true },
350
+ clip: { type: 'number', description: 'Clip slot (1-based, default: 1)' },
351
+ notes: { type: 'string', description: 'JSON array of notes: [{"pitch":60,"start":0,"duration":1,"velocity":100}] or shorthand: "C4 E4 G4" for chord at beat 0' },
352
+ },
353
+ tier: 'free',
354
+ timeout: 15_000,
355
+ async execute(args) {
356
+ const action = String(args.action || 'write').toLowerCase();
357
+ const t = userTrack(args.track);
358
+ const c = Math.max(0, (Number(args.clip) || 1) - 1);
359
+ try {
360
+ const osc = await ensureAbleton();
361
+ if (action === 'clear') {
362
+ osc.send('/live/clip/remove/notes', t, c);
363
+ return `Cleared all notes from track ${args.track}, clip ${(c + 1)}`;
364
+ }
365
+ if (action === 'read') {
366
+ const notes = await osc.query('/live/clip/get/notes', t, c);
367
+ const rawArgs = extractArgs(notes);
368
+ if (rawArgs.length < 5)
369
+ return 'No notes in this clip';
370
+ const lines = ['## MIDI Notes', '', '| Pitch | Note | Start | Duration | Velocity |', '|-------|------|-------|----------|----------|'];
371
+ // Notes come as: count, then groups of 5 (pitch, start, duration, velocity, mute)
372
+ for (let i = 1; i + 4 < rawArgs.length; i += 5) {
373
+ const pitch = Number(rawArgs[i]);
374
+ const start = Number(rawArgs[i + 1]);
375
+ const dur = Number(rawArgs[i + 2]);
376
+ const vel = Number(rawArgs[i + 3]);
377
+ lines.push(`| ${pitch} | ${midiToNoteName(pitch)} | ${start.toFixed(2)} | ${dur.toFixed(2)} | ${vel} |`);
378
+ }
379
+ return lines.join('\n');
380
+ }
381
+ // Write mode
382
+ const notesStr = String(args.notes || '');
383
+ let midiNotes = [];
384
+ // Try JSON parse first
385
+ try {
386
+ const parsed = JSON.parse(notesStr);
387
+ if (Array.isArray(parsed)) {
388
+ midiNotes = parsed.map((n) => ({
389
+ pitch: Number(n.pitch) || 60,
390
+ start: Number(n.start) || 0,
391
+ duration: Number(n.duration) || 1,
392
+ velocity: Number(n.velocity) || 80,
393
+ }));
394
+ }
395
+ }
396
+ catch {
397
+ // Try note name shorthand: "C4 E4 G4" → chord at beat 0
398
+ const noteNames = notesStr.split(/[\s,]+/).filter(Boolean);
399
+ let beat = 0;
400
+ for (const name of noteNames) {
401
+ try {
402
+ const pitch = noteNameToMidi(name);
403
+ midiNotes.push({ pitch, start: beat, duration: 1, velocity: 80 });
404
+ }
405
+ catch {
406
+ // Maybe it's a beat marker like "b2:"
407
+ const beatMatch = name.match(/^b(\d+):?$/);
408
+ if (beatMatch)
409
+ beat = Number(beatMatch[1]);
410
+ }
411
+ }
412
+ }
413
+ if (midiNotes.length === 0)
414
+ return 'No valid notes to write. Use JSON array or note names (e.g. "C4 E4 G4")';
415
+ // Send notes via OSC — batch in groups of 50
416
+ for (let i = 0; i < midiNotes.length; i += 50) {
417
+ const batch = midiNotes.slice(i, i + 50);
418
+ for (const note of batch) {
419
+ osc.send('/live/clip/add/notes', t, c, note.pitch, note.start, note.duration, note.velocity, 0);
420
+ }
421
+ }
422
+ const noteList = midiNotes.slice(0, 10).map(n => `${midiToNoteName(n.pitch)} at beat ${n.start}`).join(', ');
423
+ const extra = midiNotes.length > 10 ? ` + ${midiNotes.length - 10} more` : '';
424
+ return `Wrote **${midiNotes.length} notes** to track ${args.track}, clip ${c + 1}: ${noteList}${extra}`;
425
+ }
426
+ catch (err) {
427
+ return `Ableton connection failed: ${err.message}\n\n${formatAbletonError()}`;
428
+ }
429
+ },
430
+ });
431
+ // ─── 6. Device Control ────────────────────────────────────────────────
432
+ registerTool({
433
+ name: 'ableton_device',
434
+ description: 'Control devices (instruments and effects) on Ableton Live tracks. List devices, get/set parameters, enable/disable. Track numbers are 1-based, device/param indices are 0-based.',
435
+ parameters: {
436
+ action: { type: 'string', description: '"list", "params", "set", "enable", "disable", "info"', required: true },
437
+ track: { type: 'number', description: 'Track number (1-based)', required: true },
438
+ device: { type: 'number', description: 'Device index (0-based) in the device chain' },
439
+ param: { type: 'number', description: 'Parameter index (0-based) for set action' },
440
+ value: { type: 'number', description: 'Parameter value (usually 0-1 normalized) for set action' },
441
+ },
442
+ tier: 'free',
443
+ timeout: 15_000,
444
+ async execute(args) {
445
+ const action = String(args.action).toLowerCase();
446
+ const t = userTrack(args.track);
447
+ const d = Number(args.device) || 0;
448
+ try {
449
+ const osc = await ensureAbleton();
450
+ switch (action) {
451
+ case 'list': {
452
+ const devices = await osc.query('/live/track/get/devices/name', t);
453
+ const names = extractArgs(devices).slice(1); // first arg is track index
454
+ if (names.length === 0)
455
+ return `No devices on track ${args.track}`;
456
+ const lines = [`## Devices on Track ${args.track}`, ''];
457
+ names.forEach((name, i) => {
458
+ lines.push(`- [${i}] **${name}**`);
459
+ });
460
+ return lines.join('\n');
461
+ }
462
+ case 'params':
463
+ case 'parameters': {
464
+ const paramNames = await osc.query('/live/device/get/parameters/name', t, d);
465
+ const paramValues = await osc.query('/live/device/get/parameters/value', t, d);
466
+ const names = extractArgs(paramNames).slice(2); // skip track + device idx
467
+ const values = extractArgs(paramValues).slice(2);
468
+ const lines = [`## Device ${d} Parameters (Track ${args.track})`, ''];
469
+ lines.push('| # | Parameter | Value |');
470
+ lines.push('|---|-----------|-------|');
471
+ for (let i = 0; i < Math.min(names.length, values.length, 50); i++) {
472
+ lines.push(`| ${i} | ${names[i]} | ${typeof values[i] === 'number' ? Number(values[i]).toFixed(3) : values[i]} |`);
473
+ }
474
+ return lines.join('\n');
475
+ }
476
+ case 'set': {
477
+ const p = Number(args.param);
478
+ const v = Number(args.value);
479
+ if (isNaN(p) || isNaN(v))
480
+ return 'Error: param and value required for set action';
481
+ osc.send('/live/device/set/parameter/value', t, d, p, v);
482
+ return `Set device ${d} param ${p} to **${v}** on track ${args.track}`;
483
+ }
484
+ case 'enable':
485
+ osc.send('/live/device/set/enabled', t, d, 1);
486
+ return `Enabled device ${d} on track ${args.track}`;
487
+ case 'disable':
488
+ osc.send('/live/device/set/enabled', t, d, 0);
489
+ return `Disabled device ${d} on track ${args.track}`;
490
+ default:
491
+ return `Unknown action "${action}". Options: list, params, set, enable, disable`;
492
+ }
493
+ }
494
+ catch (err) {
495
+ return `Ableton connection failed: ${err.message}\n\n${formatAbletonError()}`;
496
+ }
497
+ },
498
+ });
499
+ // ─── 7. Mixer ─────────────────────────────────────────────────────────
500
+ registerTool({
501
+ name: 'ableton_mixer',
502
+ description: 'Batch mixer operations — snapshot all track levels, set multiple tracks at once, adjust sends.',
503
+ parameters: {
504
+ action: { type: 'string', description: '"snapshot" to see all levels, "set" to batch-set volumes, "send" to set send level', required: true },
505
+ levels: { type: 'string', description: 'For "set": JSON object mapping track numbers to volumes, e.g. {"1": 0.8, "3": 0.5}' },
506
+ track: { type: 'number', description: 'Track number (for send action)' },
507
+ send: { type: 'number', description: 'Send index (0-based, for send action)' },
508
+ value: { type: 'number', description: 'Send level 0-1 (for send action)' },
509
+ },
510
+ tier: 'free',
511
+ timeout: 20_000,
512
+ async execute(args) {
513
+ const action = String(args.action).toLowerCase();
514
+ try {
515
+ const osc = await ensureAbleton();
516
+ if (action === 'snapshot') {
517
+ const countResult = await osc.query('/live/song/get/num_tracks');
518
+ const count = Number(extractArgs(countResult)[0]) || 0;
519
+ const lines = ['## Mixer Snapshot', ''];
520
+ lines.push('| Track | Name | Volume | Pan |');
521
+ lines.push('|-------|------|--------|-----|');
522
+ for (let i = 0; i < Math.min(count, 32); i++) {
523
+ try {
524
+ const name = await osc.query('/live/track/get/name', i);
525
+ const vol = await osc.query('/live/track/get/volume', i);
526
+ const pan = await osc.query('/live/track/get/panning', i);
527
+ const volPct = (Number(extractArgs(vol)[1]) * 100).toFixed(0);
528
+ const panVal = Number(extractArgs(pan)[1]);
529
+ const panStr = panVal === 0 ? 'C' : panVal < 0 ? `L${Math.abs(panVal * 100).toFixed(0)}` : `R${(panVal * 100).toFixed(0)}`;
530
+ lines.push(`| ${i + 1} | ${extractArgs(name)[1]} | ${volPct}% | ${panStr} |`);
531
+ }
532
+ catch {
533
+ break;
534
+ }
535
+ }
536
+ return lines.join('\n');
537
+ }
538
+ if (action === 'set') {
539
+ try {
540
+ const levels = JSON.parse(String(args.levels));
541
+ const changes = [];
542
+ for (const [trackNum, vol] of Object.entries(levels)) {
543
+ const t = Math.max(0, Number(trackNum) - 1);
544
+ const v = Math.max(0, Math.min(1, Number(vol)));
545
+ osc.send('/live/track/set/volume', t, v);
546
+ changes.push(`Track ${trackNum} → ${(v * 100).toFixed(0)}%`);
547
+ }
548
+ return `Set volumes:\n${changes.map(c => `- ${c}`).join('\n')}`;
549
+ }
550
+ catch {
551
+ return 'Error: levels must be valid JSON, e.g. {"1": 0.8, "3": 0.5}';
552
+ }
553
+ }
554
+ if (action === 'send') {
555
+ const t = userTrack(args.track);
556
+ const s = Number(args.send) || 0;
557
+ const v = Math.max(0, Math.min(1, Number(args.value)));
558
+ osc.send('/live/track/set/send', t, s, v);
559
+ return `Track ${args.track} send ${s} → **${(v * 100).toFixed(0)}%**`;
560
+ }
561
+ return `Unknown action "${action}". Options: snapshot, set, send`;
562
+ }
563
+ catch (err) {
564
+ return `Ableton connection failed: ${err.message}\n\n${formatAbletonError()}`;
565
+ }
566
+ },
567
+ });
568
+ // ─── 8. Chord Progression Writer ──────────────────────────────────────
569
+ registerTool({
570
+ name: 'ableton_create_progression',
571
+ description: 'Generate a chord progression and write it as MIDI into an Ableton clip. Supports Roman numerals (ii V I), chord symbols (Cmaj7 Am7), named progressions (Andalusian, 12-bar blues, Coltrane changes), 6 voicing styles, and rhythm patterns. Creates the clip if it doesn\'t exist.',
572
+ parameters: {
573
+ track: { type: 'number', description: 'Track number (1-based)', required: true },
574
+ clip: { type: 'number', description: 'Clip slot (1-based, default: 1)' },
575
+ key: { type: 'string', description: 'Musical key: C, F#, Bb, etc. (default: C)', },
576
+ scale: { type: 'string', description: 'Scale type: major, minor, dorian, etc. (default: major)' },
577
+ progression: { type: 'string', description: 'Chord progression — Roman numerals ("ii V I"), chord symbols ("Cmaj7 Am7 Fmaj7 G7"), or named ("andalusian", "coltrane", "12_bar_blues"). Use progression="list" to see all named progressions.', required: true },
578
+ bars: { type: 'number', description: 'Number of bars (default: 4)' },
579
+ voicing: { type: 'string', description: 'Voicing style: close, open, drop2, drop3, spread, shell (default: close)' },
580
+ rhythm: { type: 'string', description: 'Rhythm pattern: whole, half, quarter, eighth, arpeggio_up, arpeggio_down (default: whole)' },
581
+ octave: { type: 'number', description: 'Base octave (default: 4)' },
582
+ },
583
+ tier: 'free',
584
+ timeout: 20_000,
585
+ async execute(args) {
586
+ // List named progressions
587
+ if (String(args.progression) === 'list') {
588
+ const lines = ['## Named Progressions', ''];
589
+ lines.push('| Name | Numerals | Description |');
590
+ lines.push('|------|----------|-------------|');
591
+ for (const [key, prog] of Object.entries(NAMED_PROGRESSIONS)) {
592
+ lines.push(`| ${key} | ${prog.numerals} | ${prog.description} |`);
593
+ }
594
+ return lines.join('\n');
595
+ }
596
+ const key = String(args.key || 'C');
597
+ const scale = String(args.scale || 'major');
598
+ const voicingStyle = String(args.voicing || 'close');
599
+ const rhythm = String(args.rhythm || 'whole');
600
+ const bars = Number(args.bars) || 4;
601
+ const octave = Number(args.octave) || 4;
602
+ const t = userTrack(args.track);
603
+ const c = Math.max(0, (Number(args.clip) || 1) - 1);
604
+ // Resolve named progression
605
+ let progressionStr = String(args.progression);
606
+ const named = NAMED_PROGRESSIONS[progressionStr.toLowerCase().replace(/[\s-]/g, '_')];
607
+ if (named) {
608
+ progressionStr = named.numerals;
609
+ }
610
+ // Parse progression into note arrays
611
+ let chordNotes;
612
+ try {
613
+ chordNotes = parseProgression(progressionStr, key, scale, octave);
614
+ }
615
+ catch (err) {
616
+ return `Error parsing progression: ${err.message}`;
617
+ }
618
+ if (chordNotes.length === 0)
619
+ return 'No chords parsed from progression';
620
+ // Apply voicing
621
+ chordNotes = chordNotes.map(notes => voiceChord(notes, voicingStyle));
622
+ // Generate MIDI notes with rhythm
623
+ const beatsPerBar = 4;
624
+ const totalBeats = bars * beatsPerBar;
625
+ const beatsPerChord = totalBeats / chordNotes.length;
626
+ const midiNotes = [];
627
+ for (let i = 0; i < chordNotes.length; i++) {
628
+ const chordStart = i * beatsPerChord;
629
+ const notes = chordNotes[i];
630
+ if (rhythm.startsWith('arpeggio')) {
631
+ const pattern = rhythm.includes('down') ? 'down' : 'up';
632
+ const arpNotes = arpeggiate(notes, pattern, 8, beatsPerChord);
633
+ for (const n of arpNotes) {
634
+ midiNotes.push({ ...n, start: n.start + chordStart });
635
+ }
636
+ }
637
+ else {
638
+ // Block chord with rhythm pattern
639
+ const divisions = RHYTHM_PATTERNS[rhythm] || RHYTHM_PATTERNS['whole'] || [0];
640
+ for (const div of divisions) {
641
+ if (div >= beatsPerChord)
642
+ break;
643
+ const noteDur = rhythm === 'whole' ? beatsPerChord :
644
+ rhythm === 'half' ? 2 :
645
+ rhythm === 'quarter' ? 1 :
646
+ rhythm === 'eighth' ? 0.5 : beatsPerChord;
647
+ for (const pitch of notes) {
648
+ midiNotes.push({
649
+ pitch,
650
+ start: chordStart + div,
651
+ duration: Math.min(noteDur, beatsPerChord - div),
652
+ velocity: 80,
653
+ });
654
+ }
655
+ }
656
+ }
657
+ }
658
+ // Send to Ableton
659
+ try {
660
+ const osc = await ensureAbleton();
661
+ // Create clip if needed
662
+ osc.send('/live/clip_slot/create_clip', t, c, totalBeats);
663
+ await new Promise(r => setTimeout(r, 200));
664
+ // Write notes
665
+ for (const note of midiNotes) {
666
+ osc.send('/live/clip/add/notes', t, c, note.pitch, note.start, note.duration, note.velocity, 0);
667
+ }
668
+ // Name the clip
669
+ const chordNames = chordNotes.map((notes, i) => {
670
+ const tokens = progressionStr.split(/[\s,|]+/);
671
+ return tokens[i % tokens.length] || '?';
672
+ }).join(' | ');
673
+ osc.send('/live/clip/set/name', t, c, `${key} ${chordNames}`);
674
+ return [
675
+ `## Chord Progression Written`,
676
+ `**Key**: ${key} ${scale}`,
677
+ `**Progression**: ${chordNames}`,
678
+ `**Voicing**: ${voicingStyle}`,
679
+ `**Rhythm**: ${rhythm}`,
680
+ `**Location**: Track ${args.track}, Clip ${c + 1} (${totalBeats} beats / ${bars} bars)`,
681
+ `**Notes written**: ${midiNotes.length}`,
682
+ named ? `**Named**: ${named.name} — ${named.description}` : '',
683
+ ].filter(Boolean).join('\n');
684
+ }
685
+ catch (err) {
686
+ return `Ableton connection failed: ${err.message}\n\n${formatAbletonError()}`;
687
+ }
688
+ },
689
+ });
690
+ // ─── 9. Session Info ──────────────────────────────────────────────────
691
+ registerTool({
692
+ name: 'ableton_session_info',
693
+ description: 'Get a full snapshot of the current Ableton Live session — tracks, clips, tempo, time signature, playing state, armed tracks, and devices. The producer agent should call this first to understand the session.',
694
+ parameters: {
695
+ detail: { type: 'string', description: '"summary" (default), "full", "tracks", "devices"' },
696
+ },
697
+ tier: 'free',
698
+ timeout: 30_000,
699
+ async execute(args) {
700
+ const detail = String(args.detail || 'summary').toLowerCase();
701
+ try {
702
+ const osc = await ensureAbleton();
703
+ const lines = [];
704
+ // Transport
705
+ const tempo = await osc.query('/live/song/get/tempo');
706
+ const playing = await osc.query('/live/song/get/is_playing');
707
+ const recording = await osc.query('/live/song/get/record_mode');
708
+ lines.push('## Ableton Live Session', '');
709
+ lines.push(`- **Tempo**: ${extractArgs(tempo)[0]} BPM`);
710
+ lines.push(`- **Playing**: ${extractArgs(playing)[0] ? '▶ Yes' : '⏹ No'}`);
711
+ lines.push(`- **Recording**: ${extractArgs(recording)[0] ? '⏺ Yes' : 'No'}`);
712
+ lines.push('');
713
+ // Tracks
714
+ const countResult = await osc.query('/live/song/get/num_tracks');
715
+ const trackCount = Number(extractArgs(countResult)[0]) || 0;
716
+ lines.push(`### Tracks (${trackCount})`, '');
717
+ if (detail === 'summary') {
718
+ lines.push('| # | Name | Vol | Armed | Muted | Soloed |');
719
+ lines.push('|---|------|-----|-------|-------|--------|');
720
+ }
721
+ else {
722
+ lines.push('| # | Name | Vol | Pan | Armed | Muted | Soloed |');
723
+ lines.push('|---|------|-----|-----|-------|-------|--------|');
724
+ }
725
+ for (let i = 0; i < Math.min(trackCount, 32); i++) {
726
+ try {
727
+ const name = await osc.query('/live/track/get/name', i);
728
+ const vol = await osc.query('/live/track/get/volume', i);
729
+ const mute = await osc.query('/live/track/get/mute', i);
730
+ const solo = await osc.query('/live/track/get/solo', i);
731
+ const arm = await osc.query('/live/track/get/arm', i);
732
+ const volPct = (Number(extractArgs(vol)[1]) * 100).toFixed(0) + '%';
733
+ const armStr = extractArgs(arm)[1] ? '⏺' : '';
734
+ const muteStr = extractArgs(mute)[1] ? '🔇' : '';
735
+ const soloStr = extractArgs(solo)[1] ? '🔊' : '';
736
+ if (detail === 'summary') {
737
+ lines.push(`| ${i + 1} | ${extractArgs(name)[1]} | ${volPct} | ${armStr} | ${muteStr} | ${soloStr} |`);
738
+ }
739
+ else {
740
+ const pan = await osc.query('/live/track/get/panning', i);
741
+ const panVal = Number(extractArgs(pan)[1]);
742
+ const panStr = panVal === 0 ? 'C' : panVal < 0 ? `L${Math.abs(panVal * 100).toFixed(0)}` : `R${(panVal * 100).toFixed(0)}`;
743
+ lines.push(`| ${i + 1} | ${extractArgs(name)[1]} | ${volPct} | ${panStr} | ${armStr} | ${muteStr} | ${soloStr} |`);
744
+ }
745
+ // Show devices in full/devices mode
746
+ if (detail === 'full' || detail === 'devices') {
747
+ try {
748
+ const devices = await osc.query('/live/track/get/devices/name', i);
749
+ const deviceNames = extractArgs(devices).slice(1);
750
+ if (deviceNames.length > 0) {
751
+ lines.push(`| | *Devices: ${deviceNames.join(' → ')}* | | | | |`);
752
+ }
753
+ }
754
+ catch { /* no devices */ }
755
+ }
756
+ }
757
+ catch {
758
+ break;
759
+ }
760
+ }
761
+ return lines.join('\n');
762
+ }
763
+ catch (err) {
764
+ return `Ableton connection failed: ${err.message}\n\n${formatAbletonError()}`;
765
+ }
766
+ },
767
+ });
768
+ }
769
+ //# sourceMappingURL=ableton.js.map