@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.
- package/README.md +11 -0
- package/dist/agent.js +23 -0
- package/dist/agents/producer.js +65 -23
- package/dist/auth.d.ts +2 -0
- package/dist/cli.js +7 -4
- package/dist/critic-gate.d.ts +29 -0
- package/dist/critic-gate.js +223 -0
- package/dist/critic-retrospect.d.ts +64 -0
- package/dist/critic-retrospect.js +279 -0
- package/dist/critic-taxonomy.d.ts +40 -0
- package/dist/critic-taxonomy.js +146 -0
- package/dist/growth.d.ts +37 -0
- package/dist/growth.js +272 -0
- package/dist/integrations/ableton.d.ts +30 -0
- package/dist/integrations/ableton.js +66 -0
- package/dist/integrations/kbot-control-client.d.ts +66 -0
- package/dist/integrations/kbot-control-client.js +224 -0
- package/dist/observer.d.ts +13 -0
- package/dist/observer.js +5 -1
- package/dist/planner/hierarchical/dag.d.ts +71 -0
- package/dist/planner/hierarchical/dag.js +97 -0
- package/dist/planner/hierarchical/persistence.d.ts +26 -0
- package/dist/planner/hierarchical/persistence.js +113 -0
- package/dist/planner/hierarchical/session-planner.d.ts +68 -0
- package/dist/planner/hierarchical/session-planner.js +141 -0
- package/dist/planner/hierarchical/types.d.ts +116 -0
- package/dist/planner/hierarchical/types.js +18 -0
- package/dist/tool-pipeline.d.ts +39 -1
- package/dist/tool-pipeline.js +109 -1
- package/dist/tools/ableton-listen.d.ts +2 -0
- package/dist/tools/ableton-listen.js +126 -0
- package/dist/tools/ableton.js +477 -12
- package/dist/tools/index.js +2 -0
- package/dist/tools/kbot-control.d.ts +2 -0
- package/dist/tools/kbot-control.js +63 -0
- package/package.json +1 -1
package/dist/tools/ableton.js
CHANGED
|
@@ -3,21 +3,26 @@
|
|
|
3
3
|
//
|
|
4
4
|
// Tools:
|
|
5
5
|
// ableton_transport — play, stop, record, tempo, time sig, seek
|
|
6
|
-
// ableton_track — list
|
|
7
|
-
// ableton_clip — fire
|
|
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,
|
|
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: '
|
|
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
|
|
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,
|
|
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
|
|
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 "
|
|
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',
|
package/dist/tools/index.js
CHANGED
|
@@ -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' },
|