@kernel.chat/kbot 3.99.20 → 3.99.22

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 (36) hide show
  1. package/README.md +11 -0
  2. package/dist/agent.js +23 -0
  3. package/dist/agents/producer.js +65 -23
  4. package/dist/auth.d.ts +2 -0
  5. package/dist/cli.js +7 -4
  6. package/dist/critic-gate.d.ts +29 -0
  7. package/dist/critic-gate.js +223 -0
  8. package/dist/critic-retrospect.d.ts +64 -0
  9. package/dist/critic-retrospect.js +279 -0
  10. package/dist/critic-taxonomy.d.ts +40 -0
  11. package/dist/critic-taxonomy.js +146 -0
  12. package/dist/growth.d.ts +37 -0
  13. package/dist/growth.js +272 -0
  14. package/dist/integrations/ableton.d.ts +30 -0
  15. package/dist/integrations/ableton.js +66 -0
  16. package/dist/integrations/kbot-control-client.d.ts +66 -0
  17. package/dist/integrations/kbot-control-client.js +224 -0
  18. package/dist/observer.d.ts +13 -0
  19. package/dist/observer.js +5 -1
  20. package/dist/planner/hierarchical/dag.d.ts +71 -0
  21. package/dist/planner/hierarchical/dag.js +97 -0
  22. package/dist/planner/hierarchical/persistence.d.ts +26 -0
  23. package/dist/planner/hierarchical/persistence.js +113 -0
  24. package/dist/planner/hierarchical/session-planner.d.ts +68 -0
  25. package/dist/planner/hierarchical/session-planner.js +141 -0
  26. package/dist/planner/hierarchical/types.d.ts +116 -0
  27. package/dist/planner/hierarchical/types.js +18 -0
  28. package/dist/tool-pipeline.d.ts +39 -1
  29. package/dist/tool-pipeline.js +109 -1
  30. package/dist/tools/ableton-listen.d.ts +2 -0
  31. package/dist/tools/ableton-listen.js +126 -0
  32. package/dist/tools/ableton.js +477 -12
  33. package/dist/tools/index.js +2 -0
  34. package/dist/tools/kbot-control.d.ts +2 -0
  35. package/dist/tools/kbot-control.js +63 -0
  36. package/package.json +1 -1
@@ -3,21 +3,26 @@
3
3
  //
4
4
  // Tools:
5
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
6
+ // ableton_track — list/mute/solo/arm/volume/pan/rename/info + set color/routing/monitoring
7
+ // ableton_clip — fire/stop/create/delete/duplicate/info + set gain/warp/pitch/loop/color/launch
8
8
  // ableton_scene — fire, list, create, duplicate
9
+ // ableton_song — undo/redo, tap_tempo, metronome, punch, cue points, loop, back_to_arranger
10
+ // ableton_view — selected scene/track/clip/device (get + set)
9
11
  // ableton_midi — write/read/clear MIDI notes in clips
10
12
  // ableton_device — list, get/set params, enable/disable devices
11
13
  // ableton_mixer — snapshot levels, batch set, sends
12
14
  // ableton_create_progression — chord progressions → MIDI in clips
15
+ // ableton_create_track — create midi/audio/return track, optionally load instrument
13
16
  // ableton_session_info — full session state snapshot
14
17
  // ableton_audio_analysis — real-time audio level meters (track + master RMS)
15
18
  // ableton_knowledge — deep Ableton knowledge base queries (registered in ableton-knowledge.ts)
16
19
  //
17
20
  // Requires: AbletonOSC loaded in Ableton Live (Preferences → Link/Tempo/MIDI → Control Surface)
21
+ // For UI-only operations OSC doesn't expose, fall back to Claude's computer-use MCP.
18
22
  import { registerTool } from './index.js';
19
23
  import { execSync } from 'node:child_process';
20
24
  import { ensureAbleton, formatAbletonError } from '../integrations/ableton-osc.js';
25
+ import { tryKc } from '../integrations/ableton.js';
21
26
  import { parseProgression, voiceChord, arpeggiate, NAMED_PROGRESSIONS, RHYTHM_PATTERNS, GENRE_DRUM_PATTERNS, noteNameToMidi, midiToNoteName, } from './music-theory.js';
22
27
  // ── Helpers ─────────────────────────────────────────────────────────────────
23
28
  function extractArgs(args) {
@@ -51,6 +56,46 @@ export function registerAbletonTools() {
51
56
  async execute(args) {
52
57
  const action = String(args.action).toLowerCase();
53
58
  try {
59
+ // Try kbot-control first for supported methods
60
+ switch (action) {
61
+ case 'play':
62
+ case 'start': {
63
+ const kc = await tryKc('song.play');
64
+ if (kc !== undefined)
65
+ return '▶ Playing (via kbot-control)';
66
+ break;
67
+ }
68
+ case 'stop': {
69
+ const kc = await tryKc('song.stop');
70
+ if (kc !== undefined)
71
+ return '⏹ Stopped (via kbot-control)';
72
+ break;
73
+ }
74
+ case 'tempo':
75
+ case 'bpm': {
76
+ const bpm = Number(args.value);
77
+ if (!bpm || bpm < 20 || bpm > 999)
78
+ return 'Error: BPM must be between 20 and 999';
79
+ const kc = await tryKc('song.tempo', { value: bpm });
80
+ if (kc !== undefined)
81
+ return `Tempo set to **${bpm} BPM** (via kbot-control)`;
82
+ break;
83
+ }
84
+ case 'status': {
85
+ const kc = await tryKc('song.get_state');
86
+ if (kc !== undefined) {
87
+ return [
88
+ '## Transport Status (via kbot-control)',
89
+ `- Tempo: **${Number(kc.tempo).toFixed(1)} BPM**`,
90
+ `- Playing: ${kc.is_playing ? '▶ Yes' : '⏹ No'}`,
91
+ `- Recording: ${kc.record_mode ? '⏺ Yes' : 'No'}`,
92
+ `- Loop: ${kc.loop ? 'on' : 'off'}`,
93
+ `- Metronome: ${kc.metronome ? 'on' : 'off'}`,
94
+ ].join('\n');
95
+ }
96
+ break;
97
+ }
98
+ }
54
99
  const osc = await ensureAbleton();
55
100
  switch (action) {
56
101
  case 'play':
@@ -112,16 +157,51 @@ export function registerAbletonTools() {
112
157
  // ─── 2. Track Control ─────────────────────────────────────────────────
113
158
  registerTool({
114
159
  name: 'ableton_track',
115
- 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).',
160
+ description: 'Control Ableton Live tracks — list all tracks, mute, solo, arm, set volume/pan/color/routing/monitoring, rename, info. Track numbers are 1-based.',
116
161
  parameters: {
117
- action: { type: 'string', description: 'Action: "list", "mute", "unmute", "solo", "unsolo", "arm", "disarm", "volume", "pan", "rename", "info"', required: true },
162
+ action: { type: 'string', description: '"list", "mute", "unmute", "solo", "unsolo", "arm", "disarm", "volume", "pan", "rename", "info", "color", "monitoring", "input_routing", "output_routing", "set"', required: true },
118
163
  track: { type: 'number', description: 'Track number (1-based). Required for all actions except "list"' },
119
- value: { type: 'string', description: 'Volume (0-1), pan (-1 to 1), or new name for rename' },
164
+ value: { type: 'string', description: 'Volume (0-1), pan (-1 to 1), name, color index (0-69), monitoring ("in"/"auto"/"off"), or routing type/channel string.' },
165
+ property: { type: 'string', description: 'For "set" action: AbletonOSC property path (e.g. "color_index", "input_routing_type").' },
120
166
  },
121
167
  tier: 'free',
122
168
  timeout: 15_000,
123
169
  async execute(args) {
124
170
  const action = String(args.action).toLowerCase();
171
+ // kbot-control fast path
172
+ if (action === 'list') {
173
+ const kc = await tryKc('track.list');
174
+ if (kc !== undefined) {
175
+ const lines = ['## Tracks (via kbot-control)', ''];
176
+ lines.push('| # | Name | Volume | Pan | Mute | Solo | Armed |');
177
+ lines.push('|---|------|--------|-----|------|------|-------|');
178
+ for (const t of kc) {
179
+ lines.push(`| ${Number(t.index) + 1} | ${t.name} | ${(Number(t.volume) * 100).toFixed(0)}% | ${Number(t.panning).toFixed(2)} | ${t.mute ? '🔇' : ''} | ${t.solo ? '🔊' : ''} | ${t.arm ? '⏺' : ''} |`);
180
+ }
181
+ return lines.join('\n');
182
+ }
183
+ }
184
+ if (args.track !== undefined && ['mute', 'unmute', 'solo', 'unsolo', 'arm', 'disarm', 'volume', 'vol', 'pan', 'color', 'rename'].includes(action)) {
185
+ const idx = userTrack(args.track);
186
+ const kcMap = {
187
+ mute: { method: 'track.mute', params: { index: idx, value: 1 }, label: `Track ${args.track} muted 🔇` },
188
+ unmute: { method: 'track.mute', params: { index: idx, value: 0 }, label: `Track ${args.track} unmuted` },
189
+ solo: { method: 'track.solo', params: { index: idx, value: 1 }, label: `Track ${args.track} soloed 🔊` },
190
+ unsolo: { method: 'track.solo', params: { index: idx, value: 0 }, label: `Track ${args.track} unsoloed` },
191
+ arm: { method: 'track.arm', params: { index: idx, value: 1 }, label: `Track ${args.track} armed ⏺` },
192
+ disarm: { method: 'track.arm', params: { index: idx, value: 0 }, label: `Track ${args.track} disarmed` },
193
+ volume: { method: 'track.volume', params: { index: idx, value: Math.max(0, Math.min(1, Number(args.value))) }, label: `Track ${args.track} volume → ${(Math.max(0, Math.min(1, Number(args.value))) * 100).toFixed(0)}%` },
194
+ vol: { method: 'track.volume', params: { index: idx, value: Math.max(0, Math.min(1, Number(args.value))) }, label: `Track ${args.track} volume → ${(Math.max(0, Math.min(1, Number(args.value))) * 100).toFixed(0)}%` },
195
+ color: { method: 'track.color', params: { index: idx, color_index: Number(args.value) }, label: `Track ${args.track} color → ${args.value}` },
196
+ rename: { method: 'track.rename', params: { index: idx, name: String(args.value || 'Track') }, label: `Track ${args.track} renamed to ${args.value}` },
197
+ };
198
+ const entry = kcMap[action];
199
+ if (entry) {
200
+ const kc = await tryKc(entry.method, entry.params);
201
+ if (kc !== undefined)
202
+ return entry.label + ' (via kbot-control)';
203
+ }
204
+ }
125
205
  try {
126
206
  const osc = await ensureAbleton();
127
207
  if (action === 'list') {
@@ -183,6 +263,43 @@ export function registerAbletonTools() {
183
263
  osc.send('/live/track/set/name', t, name);
184
264
  return `Track ${args.track} renamed to **${name}**`;
185
265
  }
266
+ case 'color': {
267
+ const idx = Math.max(0, Math.min(69, Number(args.value)));
268
+ osc.send('/live/track/set/color_index', t, idx);
269
+ return `Track ${args.track} color → ${idx}`;
270
+ }
271
+ case 'monitoring': {
272
+ // 0 = In, 1 = Auto, 2 = Off
273
+ const mode = String(args.value || '').toLowerCase();
274
+ const mapped = mode === 'in' ? 0 : mode === 'off' ? 2 : 1;
275
+ osc.send('/live/track/set/current_monitoring_state', t, mapped);
276
+ return `Track ${args.track} monitoring → **${['In', 'Auto', 'Off'][mapped]}**`;
277
+ }
278
+ case 'input_routing':
279
+ case 'inroute': {
280
+ osc.send('/live/track/set/input_routing_type', t, String(args.value));
281
+ return `Track ${args.track} input routing → ${args.value}`;
282
+ }
283
+ case 'output_routing':
284
+ case 'outroute': {
285
+ osc.send('/live/track/set/output_routing_type', t, String(args.value));
286
+ return `Track ${args.track} output routing → ${args.value}`;
287
+ }
288
+ case 'set': {
289
+ // Escape hatch for any AbletonOSC track setter not covered above.
290
+ // e.g. property="color_index", value=5 → /live/track/set/color_index
291
+ const prop = String(args.property || '').trim();
292
+ if (!prop)
293
+ return 'Error: "set" requires a property parameter.';
294
+ const address = `/live/track/set/${prop}`;
295
+ const raw = args.value;
296
+ const numeric = typeof raw === 'number' ? raw : (raw !== undefined && !isNaN(Number(raw)) ? Number(raw) : null);
297
+ if (numeric !== null)
298
+ osc.send(address, t, numeric);
299
+ else
300
+ osc.send(address, t, String(raw));
301
+ return `Set track ${args.track} ${prop} = ${raw}`;
302
+ }
186
303
  case 'info': {
187
304
  const name = await osc.query('/live/track/get/name', t);
188
305
  const vol = await osc.query('/live/track/get/volume', t);
@@ -200,7 +317,7 @@ export function registerAbletonTools() {
200
317
  ].join('\n');
201
318
  }
202
319
  default:
203
- return `Unknown action "${action}". Options: list, mute, unmute, solo, unsolo, arm, disarm, volume, pan, rename, info`;
320
+ return `Unknown action "${action}". Options: list, mute, unmute, solo, unsolo, arm, disarm, volume, pan, rename, info, color, monitoring, input_routing, output_routing, set`;
204
321
  }
205
322
  }
206
323
  catch (err) {
@@ -211,13 +328,15 @@ export function registerAbletonTools() {
211
328
  // ─── 3. Clip Control ──────────────────────────────────────────────────
212
329
  registerTool({
213
330
  name: 'ableton_clip',
214
- description: 'Control Ableton Live clips in Session View — fire, stop, create, delete, duplicate, get info. Track and clip numbers are 1-based.',
331
+ description: 'Control Ableton Live clips in Session View — fire/stop/create/delete/duplicate/info/list, plus set gain, warp, pitch, loop_start/end, color, launch_mode, start_marker, end_marker, velocity_amount. Track and clip numbers are 1-based.',
215
332
  parameters: {
216
- action: { type: 'string', description: '"fire", "stop", "create", "delete", "duplicate", "info", "list"', required: true },
333
+ action: { type: 'string', description: '"fire", "stop", "create", "delete", "duplicate", "info", "list", "set"', required: true },
217
334
  track: { type: 'number', description: 'Track number (1-based)', required: true },
218
335
  clip: { type: 'number', description: 'Clip slot number (1-based). Default: 1' },
219
336
  name: { type: 'string', description: 'Clip name (for create)' },
220
337
  length: { type: 'number', description: 'Clip length in beats (for create, default: 16 = 4 bars)' },
338
+ property: { type: 'string', description: 'For "set" action: clip property. One of: name, gain, pitch_coarse, pitch_fine, loop_start, loop_end, start_marker, end_marker, velocity_amount, color_index, launch_mode, launch_quantization, warp_mode, warping, legato, muted, ram_mode.' },
339
+ value: { type: 'string', description: 'For "set" action: the value to assign.' },
221
340
  },
222
341
  tier: 'free',
223
342
  timeout: 10_000,
@@ -225,6 +344,46 @@ export function registerAbletonTools() {
225
344
  const action = String(args.action).toLowerCase();
226
345
  const t = userTrack(args.track);
227
346
  const c = Math.max(0, (Number(args.clip) || 1) - 1);
347
+ // kbot-control fast path for common clip ops
348
+ const kcMap = {
349
+ fire: { method: 'clip.fire', params: { track: t, slot: c }, label: `Fired clip ${args.clip || 1} on track ${args.track} ▶ (via kbot-control)` },
350
+ launch: { method: 'clip.fire', params: { track: t, slot: c }, label: `Fired clip ${args.clip || 1} on track ${args.track} ▶ (via kbot-control)` },
351
+ stop: { method: 'clip.stop', params: { track: t, slot: c }, label: `Stopped clip ${args.clip || 1} on track ${args.track} ⏹ (via kbot-control)` },
352
+ delete: { method: 'clip.delete', params: { track: t, slot: c }, label: `Deleted clip in slot ${c + 1} on track ${args.track} (via kbot-control)` },
353
+ duplicate: { method: 'clip.duplicate', params: { track: t, slot: c }, label: `Duplicated clip to slot ${c + 2} on track ${args.track} (via kbot-control)` },
354
+ };
355
+ if (kcMap[action]) {
356
+ const kc = await tryKc(kcMap[action].method, kcMap[action].params);
357
+ if (kc !== undefined)
358
+ return kcMap[action].label;
359
+ }
360
+ if (action === 'create') {
361
+ const length = Number(args.length) || 16;
362
+ const name = String(args.name || `Clip ${c + 1}`);
363
+ const kc = await tryKc('clip.create', { track: t, slot: c, length, name });
364
+ if (kc !== undefined)
365
+ return `Created clip **${name}** (${length} beats / ${length / 4} bars) on track ${args.track}, slot ${c + 1} (via kbot-control)`;
366
+ }
367
+ if (action === 'info') {
368
+ const kc = await tryKc('clip.get_state', { track: t, slot: c });
369
+ if (kc !== undefined) {
370
+ return [
371
+ `## Clip Info — Track ${args.track}, Slot ${c + 1} (via kbot-control)`,
372
+ `- Name: ${kc.name || '?'}`,
373
+ `- Length: ${kc.length} beats`,
374
+ `- Type: ${kc.is_midi ? 'MIDI' : kc.is_audio ? 'Audio' : '?'}`,
375
+ `- Playing: ${kc.is_playing ? 'Yes' : 'No'}`,
376
+ `- Looping: ${kc.looping ? 'Yes' : 'No'}`,
377
+ kc.is_audio ? `- Gain: ${kc.gain}` : null,
378
+ kc.is_audio ? `- Warp: ${kc.warping ? 'on' : 'off'} (mode ${kc.warp_mode})` : null,
379
+ ].filter(Boolean).join('\n');
380
+ }
381
+ }
382
+ if (action === 'set' && args.property) {
383
+ const kc = await tryKc('clip.set_property', { track: t, slot: c, property: String(args.property), value: args.value });
384
+ if (kc !== undefined)
385
+ return `Set clip on track ${args.track} slot ${c + 1}: ${args.property} = ${args.value} (via kbot-control)`;
386
+ }
228
387
  try {
229
388
  const osc = await ensureAbleton();
230
389
  switch (action) {
@@ -280,8 +439,28 @@ export function registerAbletonTools() {
280
439
  lines.push('No clips found');
281
440
  return lines.join('\n');
282
441
  }
442
+ case 'set': {
443
+ // Generic setter for any clip property AbletonOSC exposes.
444
+ // See /live/clip/set/* — name, gain, pitch_coarse, pitch_fine,
445
+ // loop_start, loop_end, start_marker, end_marker, velocity_amount,
446
+ // color_index, launch_mode, launch_quantization, warp_mode, warping,
447
+ // legato, muted, ram_mode.
448
+ const prop = String(args.property || '').trim();
449
+ if (!prop)
450
+ return 'Error: "set" requires a property parameter.';
451
+ const address = `/live/clip/set/${prop}`;
452
+ const raw = args.value;
453
+ const numeric = typeof raw === 'number' ? raw : (raw !== undefined && !isNaN(Number(raw)) ? Number(raw) : null);
454
+ if (prop === 'name' || numeric === null) {
455
+ osc.send(address, t, c, String(raw));
456
+ }
457
+ else {
458
+ osc.send(address, t, c, numeric);
459
+ }
460
+ return `Set clip on track ${args.track} slot ${c + 1}: ${prop} = ${raw}`;
461
+ }
283
462
  default:
284
- return `Unknown action "${action}". Options: fire, stop, create, delete, duplicate, info, list`;
463
+ return `Unknown action "${action}". Options: fire, stop, create, delete, duplicate, info, list, set`;
285
464
  }
286
465
  }
287
466
  catch (err) {
@@ -448,6 +627,20 @@ export function registerAbletonTools() {
448
627
  const action = String(args.action).toLowerCase();
449
628
  const t = userTrack(args.track);
450
629
  const d = Number(args.device) || 0;
630
+ // kbot-control fast path
631
+ if (action === 'list') {
632
+ const kc = await tryKc('device.list', { track: t });
633
+ if (kc !== undefined) {
634
+ if (kc.length === 0)
635
+ return `No devices on track ${args.track}`;
636
+ const lines = [`## Devices on Track ${args.track} (via kbot-control)`, ''];
637
+ for (const dev of kc) {
638
+ const marker = dev.is_active ? '' : ' *(bypassed)*';
639
+ lines.push(`- [${dev.index}] **${dev.name}**${marker}`);
640
+ }
641
+ return lines.join('\n');
642
+ }
643
+ }
451
644
  try {
452
645
  const osc = await ensureAbleton();
453
646
  switch (action) {
@@ -975,11 +1168,11 @@ return "ok"
975
1168
  // ─── 13. Create Track ─────────────────────────────────────────────────
976
1169
  registerTool({
977
1170
  name: 'ableton_create_track',
978
- description: 'Create a new MIDI or audio track in Ableton and optionally load an instrument on it.',
1171
+ description: 'Create a new MIDI, audio, or return track in Ableton and optionally load an instrument on it.',
979
1172
  parameters: {
980
- type: { type: 'string', description: '"midi" or "audio" (default: midi)', },
1173
+ type: { type: 'string', description: '"midi", "audio", or "return" (default: midi)', },
981
1174
  name: { type: 'string', description: 'Track name' },
982
- instrument: { type: 'string', description: 'Optional: instrument to load (e.g. "Serum 2", "Operator")' },
1175
+ instrument: { type: 'string', description: 'Optional: instrument to load (midi tracks only — e.g. "Serum 2", "Operator")' },
983
1176
  manufacturer: { type: 'string', description: 'Optional: plugin manufacturer (e.g. "Xfer Records")' },
984
1177
  },
985
1178
  tier: 'free',
@@ -992,6 +1185,11 @@ return "ok"
992
1185
  if (trackType === 'audio') {
993
1186
  osc.send('/live/kbot/create_audio_track', -1);
994
1187
  }
1188
+ else if (trackType === 'return') {
1189
+ // AbletonOSC native path for return tracks. kbot bridge may not
1190
+ // wrap this, so we use the standard OSC address directly.
1191
+ osc.send('/live/song/create_return_track');
1192
+ }
995
1193
  else {
996
1194
  osc.send('/live/kbot/create_midi_track', -1);
997
1195
  }
@@ -1024,6 +1222,273 @@ return "ok"
1024
1222
  }
1025
1223
  },
1026
1224
  });
1225
+ // ─── 13b. Song-level control ──────────────────────────────────────────
1226
+ registerTool({
1227
+ name: 'ableton_song',
1228
+ description: 'Song-level Ableton controls — undo, redo, tap tempo, metronome, punch in/out, cue points, loop, back to arranger, capture MIDI, stop all clips, groove, record_mode.',
1229
+ parameters: {
1230
+ action: { type: 'string', description: '"undo", "redo", "tap_tempo", "metronome", "punch_in", "punch_out", "cue_add", "cue_delete", "cue_next", "cue_prev", "cue_jump", "loop", "loop_start", "loop_length", "back_to_arranger", "capture_midi", "stop_all_clips", "groove", "record_mode", "jump_by", "status"', required: true },
1231
+ value: { type: 'string', description: 'Value for the action: 1/0 for toggles (metronome/loop/punch/record_mode), beats for loop_start/length/jump_by/groove, name for cue_add.' },
1232
+ },
1233
+ tier: 'free',
1234
+ timeout: 10_000,
1235
+ async execute(args) {
1236
+ const action = String(args.action).toLowerCase();
1237
+ // kbot-control fast path: most song.* methods map 1:1 to dispatcher.
1238
+ const kcMap = {
1239
+ undo: { method: 'song.undo', label: '↶ Undo' },
1240
+ redo: { method: 'song.redo', label: '↷ Redo' },
1241
+ tap_tempo: { method: 'song.tap_tempo', label: '👆 Tap tempo' },
1242
+ tap: { method: 'song.tap_tempo', label: '👆 Tap tempo' },
1243
+ capture_midi: { method: 'song.capture_midi', label: '🎹 Captured MIDI' },
1244
+ stop_all_clips: { method: 'song.stop_all_clips', label: '⏹ Stopped all clips' },
1245
+ back_to_arranger: { method: 'song.back_to_arranger', label: 'Switched to Arrangement view' },
1246
+ metronome: { method: 'song.metronome', buildParams: () => ({ value: String(args.value).match(/^(1|true|on|yes)$/i) ? 1 : 0 }) },
1247
+ loop: { method: 'song.loop', buildParams: () => ({ value: String(args.value).match(/^(1|true|on|yes)$/i) ? 1 : 0 }) },
1248
+ record_mode: { method: 'song.record_mode', buildParams: () => ({ value: String(args.value).match(/^(1|true|on|yes)$/i) ? 1 : 0 }) },
1249
+ jump_by: { method: 'song.jump_by', buildParams: () => ({ beats: Number(args.value) }) },
1250
+ status: { method: 'song.get_state' },
1251
+ };
1252
+ const entry = kcMap[action];
1253
+ if (entry) {
1254
+ const kc = await tryKc(entry.method, entry.buildParams?.());
1255
+ if (kc !== undefined) {
1256
+ if (action === 'status' && kc && typeof kc === 'object') {
1257
+ const s = kc;
1258
+ return [
1259
+ '## Song status (via kbot-control)',
1260
+ `- Tempo: ${Number(s.tempo).toFixed(1)} BPM`,
1261
+ `- Playing: ${s.is_playing ? 'Yes' : 'No'}`,
1262
+ `- Loop: ${s.loop ? 'on' : 'off'}`,
1263
+ `- Metronome: ${s.metronome ? 'on' : 'off'}`,
1264
+ `- Punch in/out: ${s.punch_in ? 'on' : 'off'} / ${s.punch_out ? 'on' : 'off'}`,
1265
+ `- Record mode: ${s.record_mode ? 'on' : 'off'}`,
1266
+ ].join('\n');
1267
+ }
1268
+ return entry.label ?? `${entry.method} → ${typeof kc === 'string' ? kc : JSON.stringify(kc)}`;
1269
+ }
1270
+ }
1271
+ try {
1272
+ const osc = await ensureAbleton();
1273
+ const asBool = (v) => {
1274
+ const s = String(v).toLowerCase();
1275
+ if (s === '1' || s === 'true' || s === 'on' || s === 'yes')
1276
+ return 1;
1277
+ if (s === '0' || s === 'false' || s === 'off' || s === 'no')
1278
+ return 0;
1279
+ return Number(v) ? 1 : 0;
1280
+ };
1281
+ switch (action) {
1282
+ case 'undo':
1283
+ osc.send('/live/song/undo');
1284
+ return '↶ Undo';
1285
+ case 'redo':
1286
+ osc.send('/live/song/redo');
1287
+ return '↷ Redo';
1288
+ case 'tap_tempo':
1289
+ case 'tap':
1290
+ osc.send('/live/song/tap_tempo');
1291
+ return '👆 Tap tempo';
1292
+ case 'metronome': {
1293
+ const v = args.value === undefined ? 1 : asBool(args.value);
1294
+ osc.send('/live/song/set/metronome', v);
1295
+ return `Metronome → ${v ? 'on' : 'off'}`;
1296
+ }
1297
+ case 'punch_in': {
1298
+ const v = args.value === undefined ? 1 : asBool(args.value);
1299
+ osc.send('/live/song/set/punch_in', v);
1300
+ return `Punch-in → ${v ? 'on' : 'off'}`;
1301
+ }
1302
+ case 'punch_out': {
1303
+ const v = args.value === undefined ? 1 : asBool(args.value);
1304
+ osc.send('/live/song/set/punch_out', v);
1305
+ return `Punch-out → ${v ? 'on' : 'off'}`;
1306
+ }
1307
+ case 'cue_add':
1308
+ osc.send('/live/song/cue_point/add_or_delete');
1309
+ if (args.value)
1310
+ osc.send('/live/song/cue_point/set/name', String(args.value));
1311
+ return `Cue point added${args.value ? ` "${args.value}"` : ''}`;
1312
+ case 'cue_delete':
1313
+ osc.send('/live/song/cue_point/add_or_delete');
1314
+ return 'Cue point at playhead deleted';
1315
+ case 'cue_next':
1316
+ osc.send('/live/song/jump_to_next_cue');
1317
+ return '→ Next cue';
1318
+ case 'cue_prev':
1319
+ osc.send('/live/song/jump_to_prev_cue');
1320
+ return '← Previous cue';
1321
+ case 'cue_jump': {
1322
+ const v = Number(args.value);
1323
+ if (isNaN(v))
1324
+ return 'Error: cue_jump needs a cue index in value.';
1325
+ osc.send('/live/song/cue_point/jump', v);
1326
+ return `Jumped to cue ${v}`;
1327
+ }
1328
+ case 'loop': {
1329
+ const v = args.value === undefined ? 1 : asBool(args.value);
1330
+ osc.send('/live/song/set/loop', v);
1331
+ return `Loop → ${v ? 'on' : 'off'}`;
1332
+ }
1333
+ case 'loop_start': {
1334
+ const v = Number(args.value);
1335
+ if (isNaN(v))
1336
+ return 'Error: loop_start needs a beat position in value.';
1337
+ osc.send('/live/song/set/loop_start', v);
1338
+ return `Loop start → beat ${v}`;
1339
+ }
1340
+ case 'loop_length': {
1341
+ const v = Number(args.value);
1342
+ if (isNaN(v))
1343
+ return 'Error: loop_length needs a beat length in value.';
1344
+ osc.send('/live/song/set/loop_length', v);
1345
+ return `Loop length → ${v} beats`;
1346
+ }
1347
+ case 'back_to_arranger':
1348
+ osc.send('/live/song/set/back_to_arranger', 1);
1349
+ return 'Switched to Arrangement view (back_to_arranger)';
1350
+ case 'capture_midi':
1351
+ osc.send('/live/song/capture_midi');
1352
+ return '🎹 Captured MIDI from armed tracks';
1353
+ case 'stop_all_clips':
1354
+ osc.send('/live/song/stop_all_clips');
1355
+ return '⏹ Stopped all clips';
1356
+ case 'groove': {
1357
+ const v = Math.max(0, Math.min(1, Number(args.value)));
1358
+ osc.send('/live/song/set/groove_amount', v);
1359
+ return `Groove amount → ${(v * 100).toFixed(0)}%`;
1360
+ }
1361
+ case 'record_mode': {
1362
+ const v = args.value === undefined ? 1 : asBool(args.value);
1363
+ osc.send('/live/song/set/record_mode', v);
1364
+ return `Arrangement record → ${v ? 'on' : 'off'}`;
1365
+ }
1366
+ case 'jump_by': {
1367
+ const v = Number(args.value);
1368
+ if (isNaN(v))
1369
+ return 'Error: jump_by needs a beat delta in value.';
1370
+ osc.send('/live/song/jump_by', v);
1371
+ return `Jumped by ${v} beats`;
1372
+ }
1373
+ case 'status': {
1374
+ const tempo = await osc.query('/live/song/get/tempo');
1375
+ const playing = await osc.query('/live/song/get/is_playing');
1376
+ const loop = await osc.query('/live/song/get/loop');
1377
+ const metro = await osc.query('/live/song/get/metronome');
1378
+ const punchIn = await osc.query('/live/song/get/punch_in');
1379
+ const punchOut = await osc.query('/live/song/get/punch_out');
1380
+ return [
1381
+ `## Song status`,
1382
+ `- Tempo: ${Number(extractArgs(tempo)[0]).toFixed(1)} BPM`,
1383
+ `- Playing: ${extractArgs(playing)[0] ? 'Yes' : 'No'}`,
1384
+ `- Loop: ${extractArgs(loop)[0] ? 'on' : 'off'}`,
1385
+ `- Metronome: ${extractArgs(metro)[0] ? 'on' : 'off'}`,
1386
+ `- Punch in/out: ${extractArgs(punchIn)[0] ? 'on' : 'off'} / ${extractArgs(punchOut)[0] ? 'on' : 'off'}`,
1387
+ ].join('\n');
1388
+ }
1389
+ default:
1390
+ return `Unknown action "${action}". Options: undo, redo, tap_tempo, metronome, punch_in, punch_out, cue_add, cue_delete, cue_next, cue_prev, cue_jump, loop, loop_start, loop_length, back_to_arranger, capture_midi, stop_all_clips, groove, record_mode, jump_by, status`;
1391
+ }
1392
+ }
1393
+ catch (err) {
1394
+ return `Ableton connection failed: ${err.message}\n\n${formatAbletonError()}`;
1395
+ }
1396
+ },
1397
+ });
1398
+ // ─── 13c. View / selection control ────────────────────────────────────
1399
+ registerTool({
1400
+ name: 'ableton_view',
1401
+ description: 'Drive the Ableton UI cursor — get or set the currently selected scene, track, clip, or device. Use this to focus the user\'s view before opening a device, or to read what the user is looking at.',
1402
+ parameters: {
1403
+ action: { type: 'string', description: '"get" or "set"', required: true },
1404
+ target: { type: 'string', description: '"scene", "track", "clip", or "device"', required: true },
1405
+ track: { type: 'number', description: 'Track number (1-based) — required for set target=clip or set target=device' },
1406
+ clip: { type: 'number', description: 'Clip slot (1-based) — required for set target=clip' },
1407
+ device: { type: 'number', description: 'Device index (0-based) — required for set target=device' },
1408
+ value: { type: 'number', description: 'Scene number (1-based) or track number (1-based) when setting scene/track selection.' },
1409
+ },
1410
+ tier: 'free',
1411
+ timeout: 10_000,
1412
+ async execute(args) {
1413
+ const action = String(args.action).toLowerCase();
1414
+ const target = String(args.target).toLowerCase();
1415
+ try {
1416
+ const osc = await ensureAbleton();
1417
+ if (action === 'get') {
1418
+ switch (target) {
1419
+ case 'scene': {
1420
+ const r = await osc.query('/live/view/get/selected_scene');
1421
+ const idx = Number(extractArgs(r)[0]);
1422
+ return `Selected scene: ${idx + 1} (0-based ${idx})`;
1423
+ }
1424
+ case 'track': {
1425
+ const r = await osc.query('/live/view/get/selected_track');
1426
+ const idx = Number(extractArgs(r)[0]);
1427
+ return `Selected track: ${idx + 1} (0-based ${idx})`;
1428
+ }
1429
+ case 'clip': {
1430
+ const r = await osc.query('/live/view/get/selected_clip');
1431
+ return `Selected clip: [${extractArgs(r).join(', ')}]`;
1432
+ }
1433
+ case 'device': {
1434
+ const r = await osc.query('/live/view/get/selected_device');
1435
+ return `Selected device: [${extractArgs(r).join(', ')}]`;
1436
+ }
1437
+ default:
1438
+ return `Unknown target "${target}". Options: scene, track, clip, device`;
1439
+ }
1440
+ }
1441
+ if (action === 'set') {
1442
+ switch (target) {
1443
+ case 'scene': {
1444
+ const s = Math.max(0, (Number(args.value) || 1) - 1);
1445
+ // Prefer kbot-control (actually scrolls + highlights)
1446
+ const kc = await tryKc('view.focus_scene', { index: s });
1447
+ if (kc !== undefined)
1448
+ return `Focused scene ${s + 1} (via kbot-control)`;
1449
+ osc.send('/live/view/set/selected_scene', s);
1450
+ return `Focused scene ${s + 1}`;
1451
+ }
1452
+ case 'track': {
1453
+ const t = Math.max(0, (Number(args.value) || 1) - 1);
1454
+ // kbot-control's view.focus_track also scrolls + highlights,
1455
+ // fixing the bug that made OSC-based device loads hit the wrong track.
1456
+ const kc = await tryKc('view.focus_track', { index: t });
1457
+ if (kc !== undefined)
1458
+ return `Focused track ${t + 1} (via kbot-control — scrolled + highlighted)`;
1459
+ osc.send('/live/view/set/selected_track', t);
1460
+ return `Focused track ${t + 1}`;
1461
+ }
1462
+ case 'clip': {
1463
+ if (!args.track || !args.clip)
1464
+ return 'Error: set target=clip requires track and clip.';
1465
+ const t = userTrack(args.track);
1466
+ const c = Math.max(0, Number(args.clip) - 1);
1467
+ const kc = await tryKc('view.focus_clip', { track: t, slot: c });
1468
+ if (kc !== undefined)
1469
+ return `Focused clip on track ${args.track} slot ${args.clip} (via kbot-control)`;
1470
+ osc.send('/live/view/set/selected_clip', t, c);
1471
+ return `Focused clip on track ${args.track} slot ${args.clip}`;
1472
+ }
1473
+ case 'device': {
1474
+ if (!args.track || args.device === undefined)
1475
+ return 'Error: set target=device requires track and device index.';
1476
+ const t = userTrack(args.track);
1477
+ const d = Number(args.device);
1478
+ osc.send('/live/view/set/selected_device', t, d);
1479
+ return `Focused device ${d} on track ${args.track}`;
1480
+ }
1481
+ default:
1482
+ return `Unknown target "${target}". Options: scene, track, clip, device`;
1483
+ }
1484
+ }
1485
+ return `Unknown action "${action}". Options: get, set`;
1486
+ }
1487
+ catch (err) {
1488
+ return `Ableton connection failed: ${err.message}\n\n${formatAbletonError()}`;
1489
+ }
1490
+ },
1491
+ });
1027
1492
  // ─── 14. Splice Search & Download ─────────────────────────────────────
1028
1493
  registerTool({
1029
1494
  name: 'splice_search',
@@ -280,6 +280,8 @@ const LAZY_MODULE_IMPORTS = [
280
280
  { path: './ableton.js', registerFn: 'registerAbletonTools' },
281
281
  { path: './ableton-knowledge.js', registerFn: 'registerAbletonKnowledgeTools' },
282
282
  { path: './ableton-bridge-tools.js', registerFn: 'registerAbletonBridgeTools' },
283
+ { path: './kbot-control.js', registerFn: 'registerKbotControlTools' },
284
+ { path: './ableton-listen.js', registerFn: 'registerAbletonListenTool' },
283
285
  { path: './producer-engine.js', registerFn: 'registerProducerEngine' },
284
286
  { path: './sound-designer.js', registerFn: 'registerSoundDesignerTools' },
285
287
  { path: './arrangement-engine.js', registerFn: 'registerArrangementEngine' },
@@ -0,0 +1,2 @@
1
+ export declare function registerKbotControlTools(): void;
2
+ //# sourceMappingURL=kbot-control.d.ts.map