@kernel.chat/kbot 3.97.1 → 3.98.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.
@@ -11,10 +11,12 @@
11
11
  // ableton_mixer — snapshot levels, batch set, sends
12
12
  // ableton_create_progression — chord progressions → MIDI in clips
13
13
  // ableton_session_info — full session state snapshot
14
+ // ableton_audio_analysis — real-time audio level meters (track + master RMS)
14
15
  // ableton_knowledge — deep Ableton knowledge base queries (registered in ableton-knowledge.ts)
15
16
  //
16
17
  // Requires: AbletonOSC loaded in Ableton Live (Preferences → Link/Tempo/MIDI → Control Surface)
17
18
  import { registerTool } from './index.js';
19
+ import { execSync } from 'node:child_process';
18
20
  import { ensureAbleton, formatAbletonError } from '../integrations/ableton-osc.js';
19
21
  import { parseProgression, voiceChord, arpeggiate, NAMED_PROGRESSIONS, RHYTHM_PATTERNS, GENRE_DRUM_PATTERNS, noteNameToMidi, midiToNoteName, } from './music-theory.js';
20
22
  // ── Helpers ─────────────────────────────────────────────────────────────────
@@ -769,52 +771,89 @@ export function registerAbletonTools() {
769
771
  // ─── 10. Load Plugin ──────────────────────────────────────────────────
770
772
  registerTool({
771
773
  name: 'ableton_load_plugin',
772
- description: 'Load any instrument or plugin onto a track by name — native (Operator, Wavetable, Drift) or third-party VST/AU (Serum 2, Vital, Kontakt). Searches Ableton\'s browser and loads onto the selected track.',
774
+ description: 'Load any instrument or plugin onto a track by name — native (Operator, Wavetable, Drift) or third-party VST/AU (Serum 2, Vital, Kontakt). Tries OSC first, then falls back to AppleScript browser automation on macOS.',
773
775
  parameters: {
774
776
  track: { type: 'number', description: 'Track number (1-based)', required: true },
775
777
  plugin: { type: 'string', description: 'Plugin name to search for (e.g. "Serum 2", "Operator", "Wavetable")', required: true },
776
778
  manufacturer: { type: 'string', description: 'Manufacturer name for VST/AU (e.g. "Xfer Records", "Native Instruments"). Optional — helps narrow search.' },
779
+ skip_results: { type: 'number', description: 'Number of Down arrow presses before selecting (to skip past FX/presets). Default: 1' },
777
780
  },
778
781
  tier: 'free',
779
- timeout: 15_000,
782
+ timeout: 20_000,
780
783
  async execute(args) {
781
784
  const t = userTrack(args.track);
782
785
  const plugin = String(args.plugin);
783
786
  const manufacturer = args.manufacturer ? String(args.manufacturer) : '';
787
+ const skipResults = Number(args.skip_results) || 1;
788
+ // ── Attempt 1: OSC (fast, reliable for native instruments) ──
784
789
  try {
785
790
  const osc = await ensureAbleton();
786
- if (manufacturer) {
787
- const result = await osc.query('/live/kbot/load_plugin', t, manufacturer, plugin);
788
- const status = extractArgs(result);
789
- if (status[0] === 'ok') {
790
- return `Loaded **${status[1]}** on track ${args.track}`;
791
- }
792
- return `Could not find "${manufacturer} ${plugin}": ${status.join(', ')}`;
793
- }
794
- // Try load_plugin first — _deep_plugin_search correctly handles native
795
- // instruments by returning the first loadable child of matching browser folders.
796
- // This works for both native (Drum Rack, Operator, Wavetable) and third-party plugins.
797
- let result = await osc.query('/live/kbot/load_plugin', t, plugin, '');
798
- let status = extractArgs(result);
799
- if (status[0] === 'ok') {
800
- return `Loaded **${status[1]}** on track ${args.track}`;
801
- }
802
- // Fallback: try load_device (uses _search_tree, less reliable for native instruments)
803
- result = await osc.query('/live/kbot/load_device', t, plugin);
804
- status = extractArgs(result);
805
- if (status[0] === 'ok') {
806
- return `Loaded **${status[1]}** on track ${args.track}`;
791
+ // Select the target track first so the plugin loads there
792
+ osc.send('/live/song/set/current_track', t);
793
+ await new Promise(r => setTimeout(r, 200));
794
+ // Try native load_device endpoint (works for Ableton built-in instruments)
795
+ const nativeResult = await osc.query('/live/track/load/device', t, plugin);
796
+ const nativeStatus = extractArgs(nativeResult);
797
+ if (nativeStatus.length > 0 && String(nativeStatus[0]) !== 'error') {
798
+ return `Loaded **${plugin}** on track ${args.track} (via OSC)`;
807
799
  }
808
- // Final fallback: try plugins category with name as both manufacturer and plugin
809
- result = await osc.query('/live/kbot/load_plugin', t, plugin, plugin);
810
- status = extractArgs(result);
811
- if (status[0] === 'ok') {
812
- return `Loaded **${status[1]}** on track ${args.track}`;
800
+ }
801
+ catch {
802
+ // OSC failed — fall through to AppleScript
803
+ }
804
+ // ── Attempt 2: AppleScript browser automation (macOS only) ──
805
+ // Uses Ableton's built-in browser search: Cmd+F → type name → arrow down → Return
806
+ // Proven approach from ZENOLOGY loading session
807
+ if (process.platform !== 'darwin') {
808
+ return `Plugin "${plugin}" could not be loaded via OSC. AppleScript fallback is macOS-only.`;
809
+ }
810
+ try {
811
+ const searchTerm = manufacturer ? `${manufacturer} ${plugin}` : plugin;
812
+ // Build AppleScript: activate Ableton, open browser search, type plugin name, select, load
813
+ const downArrows = Array(skipResults).fill('key code 125').join('\ndelay 0.3\n'); // 125 = Down arrow
814
+ const script = `
815
+ tell application "Ableton Live 12"
816
+ activate
817
+ end tell
818
+ delay 0.5
819
+ tell application "System Events"
820
+ tell process "Ableton Live 12"
821
+ -- Open browser search: Cmd+F (View > Search in Browser)
822
+ keystroke "f" using command down
823
+ delay 0.8
824
+ -- Clear any existing search text and type plugin name
825
+ keystroke "a" using command down
826
+ delay 0.1
827
+ keystroke "${searchTerm.replace(/"/g, '\\"')}"
828
+ delay 1.0
829
+ -- Navigate down to the result (skip past categories/folders)
830
+ ${downArrows}
831
+ delay 0.3
832
+ -- Press Return to load the selected item onto the current track
833
+ key code 36
834
+ delay 0.5
835
+ end tell
836
+ end tell
837
+ return "ok"
838
+ `;
839
+ const lines = script.split('\n').filter(l => l.trim());
840
+ const escapedArgs = lines.map(l => `-e '${l.replace(/'/g, "'\\''")}'`).join(' ');
841
+ const result = execSync(`osascript ${escapedArgs}`, {
842
+ encoding: 'utf-8',
843
+ timeout: 15_000,
844
+ stdio: ['pipe', 'pipe', 'pipe'],
845
+ }).trim();
846
+ if (result === 'ok') {
847
+ return `Loaded **${plugin}** on track ${args.track} (via AppleScript browser search)`;
813
848
  }
814
- return `Plugin "${plugin}" not found in Ableton's browser. Check the name and try again.`;
849
+ return `AppleScript returned unexpected result: ${result}`;
815
850
  }
816
851
  catch (err) {
817
- return `Ableton connection failed: ${err.message}\n\n${formatAbletonError()}`;
852
+ const msg = err.message;
853
+ if (msg.includes('not allowed') || msg.includes('assistive')) {
854
+ return `AppleScript failed — Accessibility permission required.\n\nGrant permission in System Settings > Privacy & Security > Accessibility for your terminal app.`;
855
+ }
856
+ return `Failed to load "${plugin}": OSC endpoint not available, AppleScript fallback failed.\n\nError: ${msg}`;
818
857
  }
819
858
  },
820
859
  });
@@ -964,21 +1003,19 @@ export function registerAbletonTools() {
964
1003
  if (args.name) {
965
1004
  osc.send('/live/track/set/name', newTrackIdx, String(args.name));
966
1005
  }
967
- // Load instrument if specified — use load_plugin first (handles native + third-party)
1006
+ // Load instrument if specified — try native OSC first, then load_plugin tool handles fallbacks
968
1007
  if (args.instrument) {
969
- const manufacturer = args.manufacturer ? String(args.manufacturer) : '';
970
- if (manufacturer) {
971
- await osc.query('/live/kbot/load_plugin', newTrackIdx, manufacturer, String(args.instrument));
972
- }
973
- else {
974
- // Try load_plugin first (works for native instruments via _deep_plugin_search fallback)
975
- const result = await osc.query('/live/kbot/load_plugin', newTrackIdx, String(args.instrument), '');
976
- const status = extractArgs(result);
977
- if (status[0] !== 'ok') {
978
- // Fallback to load_device
979
- await osc.query('/live/kbot/load_device', newTrackIdx, String(args.instrument));
1008
+ try {
1009
+ const loadResult = await osc.query('/live/track/load/device', newTrackIdx, String(args.instrument));
1010
+ const loadStatus = extractArgs(loadResult);
1011
+ if (loadStatus.length === 0 || String(loadStatus[0]) === 'error') {
1012
+ // Native load failed — OSC doesn't have this device. The user can use
1013
+ // ableton_load_plugin separately which has AppleScript fallback.
980
1014
  }
981
1015
  }
1016
+ catch {
1017
+ // Timeout or connection error — device may still have loaded, continue
1018
+ }
982
1019
  }
983
1020
  return `Created ${trackType} track **${args.name || 'Track ' + (newTrackIdx + 1)}**${args.instrument ? ' with ' + args.instrument : ''} (track ${newTrackIdx + 1})`;
984
1021
  }
@@ -1035,5 +1072,102 @@ export function registerAbletonTools() {
1035
1072
  }
1036
1073
  },
1037
1074
  });
1075
+ // ─── 15. Audio Analysis ──────────────────────────────────────────────
1076
+ registerTool({
1077
+ name: 'ableton_audio_analysis',
1078
+ description: 'Get real-time audio level meters from Ableton Live — track output levels (L/R RMS), master output, and peak detection. Use this to hear what is playing, check if a track has signal, or monitor the mix.',
1079
+ parameters: {
1080
+ track: { type: 'number', description: 'Track number to analyze (1-based). Omit to get master output only.' },
1081
+ all_tracks: { type: 'boolean', description: 'If true, read levels for all tracks plus master. Default: false' },
1082
+ },
1083
+ tier: 'free',
1084
+ timeout: 10_000,
1085
+ async execute(args) {
1086
+ try {
1087
+ const osc = await ensureAbleton();
1088
+ const lines = ['## Audio Levels', ''];
1089
+ // Helper to read a meter value with error handling
1090
+ async function readMeter(address, ...oscArgs) {
1091
+ try {
1092
+ const result = await osc.query(address, ...oscArgs);
1093
+ const vals = extractArgs(result);
1094
+ // Meter values are typically floats 0.0 - 1.0 (or higher for clipping)
1095
+ return typeof vals[vals.length - 1] === 'number' ? vals[vals.length - 1] : 0;
1096
+ }
1097
+ catch {
1098
+ return -1; // timeout = no response
1099
+ }
1100
+ }
1101
+ // Meter bar visualization
1102
+ function meterBar(level) {
1103
+ if (level < 0)
1104
+ return '[-no signal-]';
1105
+ const db = level > 0 ? 20 * Math.log10(level) : -Infinity;
1106
+ const dbStr = db === -Infinity ? '-inf' : db.toFixed(1);
1107
+ const barLen = Math.min(20, Math.max(0, Math.round(level * 20)));
1108
+ const bar = '\u2588'.repeat(barLen) + '\u2591'.repeat(20 - barLen);
1109
+ return `[${bar}] ${dbStr} dB`;
1110
+ }
1111
+ if (args.all_tracks) {
1112
+ // Read all track levels
1113
+ const countResult = await osc.query('/live/song/get/num_tracks');
1114
+ const numTracks = Number(extractArgs(countResult)[0]) || 0;
1115
+ for (let t = 0; t < numTracks - 1; t++) { // -1 to skip master return
1116
+ const nameResult = await osc.query('/live/track/get/name', t);
1117
+ const name = extractArgs(nameResult)[1] || `Track ${t + 1}`;
1118
+ const left = await readMeter('/live/track/get/output_meter_left', t);
1119
+ const right = await readMeter('/live/track/get/output_meter_right', t);
1120
+ const avg = left >= 0 && right >= 0 ? (left + right) / 2 : Math.max(left, right);
1121
+ lines.push(`**${displayTrack(t)}. ${name}**: ${meterBar(avg)}`);
1122
+ if (left >= 0 && right >= 0 && Math.abs(left - right) > 0.05) {
1123
+ lines.push(` L: ${meterBar(left)} | R: ${meterBar(right)}`);
1124
+ }
1125
+ }
1126
+ lines.push('');
1127
+ }
1128
+ else if (args.track) {
1129
+ // Read specific track
1130
+ const t = userTrack(args.track);
1131
+ const nameResult = await osc.query('/live/track/get/name', t);
1132
+ const name = extractArgs(nameResult)[1] || `Track ${args.track}`;
1133
+ const left = await readMeter('/live/track/get/output_meter_left', t);
1134
+ const right = await readMeter('/live/track/get/output_meter_right', t);
1135
+ lines.push(`**Track ${args.track} (${name})**`);
1136
+ lines.push(` Left: ${meterBar(left)}`);
1137
+ lines.push(` Right: ${meterBar(right)}`);
1138
+ // Check if signal is present
1139
+ const avg = left >= 0 && right >= 0 ? (left + right) / 2 : Math.max(left, right);
1140
+ if (avg <= 0) {
1141
+ lines.push('');
1142
+ lines.push('No signal detected. Check: is the track armed? Is transport playing? Does the track have clips?');
1143
+ }
1144
+ else if (avg > 1.0) {
1145
+ lines.push('');
1146
+ lines.push('**Warning**: Signal is clipping! Reduce track volume.');
1147
+ }
1148
+ lines.push('');
1149
+ }
1150
+ // Always show master output
1151
+ const masterLeft = await readMeter('/live/master/get/output_meter_left');
1152
+ const masterRight = await readMeter('/live/master/get/output_meter_right');
1153
+ lines.push('**Master Output**');
1154
+ lines.push(` Left: ${meterBar(masterLeft)}`);
1155
+ lines.push(` Right: ${meterBar(masterRight)}`);
1156
+ const masterAvg = masterLeft >= 0 && masterRight >= 0 ? (masterLeft + masterRight) / 2 : Math.max(masterLeft, masterRight);
1157
+ if (masterAvg <= 0) {
1158
+ lines.push('');
1159
+ lines.push('No audio on master output. Is transport playing?');
1160
+ }
1161
+ else if (masterAvg > 0.9) {
1162
+ lines.push('');
1163
+ lines.push('**Loud!** Master is near clipping. Consider reducing levels.');
1164
+ }
1165
+ return lines.join('\n');
1166
+ }
1167
+ catch (err) {
1168
+ return `Ableton connection failed: ${err.message}\n\n${formatAbletonError()}`;
1169
+ }
1170
+ },
1171
+ });
1038
1172
  }
1039
1173
  //# sourceMappingURL=ableton.js.map
@@ -0,0 +1,2 @@
1
+ export declare function registerEstimationTools(): void;
2
+ //# sourceMappingURL=estimation.d.ts.map
@@ -0,0 +1,21 @@
1
+ // kbot Estimation Tools — Project planning and resource allocation
2
+ import { registerTool } from './index.js';
3
+ export function registerEstimationTools() {
4
+ registerTool({
5
+ name: 'estimate_task',
6
+ description: 'Estimate the time or resources needed for a task. Provide as much detail as possible for a more accurate estimate.',
7
+ parameters: {
8
+ task_description: { type: 'string', description: 'Detailed description of the task', required: true },
9
+ units: { type: 'string', description: 'Units for the estimate (e.g., hours, days, USD)', required: false, default: 'hours' },
10
+ },
11
+ tier: 'free',
12
+ async execute(args) {
13
+ const taskDescription = String(args.task_description);
14
+ const units = String(args.units) || 'hours';
15
+ // Placeholder estimation logic — replace with a more sophisticated model
16
+ let estimate = Math.round(Math.random() * 10);
17
+ return `Based on the description "${taskDescription}", I estimate this will take approximately ${estimate} ${units}.`;
18
+ },
19
+ });
20
+ }
21
+ //# sourceMappingURL=estimation.js.map
@@ -0,0 +1,2 @@
1
+ export declare function registerIdempotencyTools(): void;
2
+ //# sourceMappingURL=idempotency-check.d.ts.map
@@ -0,0 +1,31 @@
1
+ // kbot Idempotency Check Tool — Prevent duplicate actions
2
+ // This tool verifies if a task has already been executed to avoid unintended side effects.
3
+ import { registerTool } from './index.js';
4
+ export function registerIdempotencyTools() {
5
+ registerTool({
6
+ name: 'idempotency_check',
7
+ description: 'Checks if an action has already been performed based on a unique identifier. Returns true if the action has been done, false otherwise.',
8
+ parameters: {
9
+ identifier: { type: 'string', description: 'Unique identifier for the action', required: true },
10
+ },
11
+ tier: 'free',
12
+ async execute(args) {
13
+ const identifier = String(args.identifier);
14
+ // Simulate a database check (replace with actual DB query)
15
+ const hasRun = await simulateDatabaseCheck(identifier);
16
+ if (hasRun) {
17
+ return 'true';
18
+ }
19
+ else {
20
+ return 'false';
21
+ }
22
+ },
23
+ });
24
+ }
25
+ async function simulateDatabaseCheck(identifier) {
26
+ // Replace with actual database query logic
27
+ // This is a placeholder for demonstration purposes
28
+ console.log(`Simulating database check for identifier: ${identifier}`);
29
+ return false; // Assume not run for now
30
+ }
31
+ //# sourceMappingURL=idempotency-check.js.map
@@ -0,0 +1,2 @@
1
+ export declare function registerIdempotencyCheckerTool(): void;
2
+ //# sourceMappingURL=idempotency-checker.d.ts.map
@@ -0,0 +1,23 @@
1
+ // kbot Idempotency Checker Tool — Ensures function calls are safe to repeat.
2
+ import { registerTool } from './index.js';
3
+ export function registerIdempotencyCheckerTool() {
4
+ registerTool({
5
+ name: 'is_idempotent',
6
+ description: 'Checks if a function is idempotent — meaning it produces the same output given the same input, regardless of how many times it is called.',
7
+ parameters: {
8
+ functionName: { type: 'string', description: 'Name of the function to check', required: true },
9
+ input: { type: 'string', description: 'Input to the function', required: true },
10
+ },
11
+ tier: 'free',
12
+ async execute(args) {
13
+ const functionName = String(args.functionName);
14
+ const input = String(args.input);
15
+ // Placeholder for actual idempotency check logic.
16
+ // In a real implementation, this would involve analyzing the function's code
17
+ // or executing it multiple times with the same input and comparing the results.
18
+ // For now, we simply return a canned response.
19
+ return `The idempotency of '${functionName}' with input '${input}' is currently unknown. Further analysis is required.`;
20
+ },
21
+ });
22
+ }
23
+ //# sourceMappingURL=idempotency-checker.js.map
@@ -0,0 +1,2 @@
1
+ export declare function registerImageVariationTools(): void;
2
+ //# sourceMappingURL=image-variation.d.ts.map
@@ -0,0 +1,31 @@
1
+ // kbot Image Variation Tool — Generates variations of an image.
2
+ import { registerTool } from './index.js';
3
+ export function registerImageVariationTools() {
4
+ registerTool({
5
+ name: 'image_variation',
6
+ description: 'Generate variations of an image using a URL. Supports different sizes and formats.',
7
+ parameters: {
8
+ image_url: { type: 'string', description: 'URL of the image to generate variations for', required: true },
9
+ width: { type: 'integer', description: 'Width of the generated image in pixels (optional)', default: 256 },
10
+ height: { type: 'integer', description: 'Height of the generated image in pixels (optional)', default: 256 },
11
+ format: { type: 'string', description: 'Format of the generated image (jpg, png, webp) (optional)', default: 'jpg' },
12
+ },
13
+ tier: 'pro',
14
+ async execute(args) {
15
+ const imageUrl = String(args.image_url);
16
+ const width = Number(args.width) || 256;
17
+ const height = Number(args.height) || 256;
18
+ const format = String(args.format).toLowerCase() || 'jpg';
19
+ try {
20
+ const url = `https://api.imgproxy.net/kbot/resize?width=${width}&height=${height}&format=${format}&url=${encodeURIComponent(imageUrl)}`;
21
+ const res = await fetch(url, { signal: AbortSignal.timeout(8000) });
22
+ const imageBuffer = await res.arrayBuffer();
23
+ return `data:image/${format};base64,${Buffer.from(imageBuffer).toString('base64')}`;
24
+ }
25
+ catch (error) {
26
+ return `Error generating image variation: ${error}`;
27
+ }
28
+ },
29
+ });
30
+ }
31
+ //# sourceMappingURL=image-variation.js.map
@@ -311,6 +311,7 @@ const LAZY_MODULE_IMPORTS = [
311
311
  { path: './financial-analysis.js', registerFn: 'registerFinancialAnalysisTools' },
312
312
  { path: './ai-analysis.js', registerFn: 'registerAIAnalysisTools' },
313
313
  { path: './music-gen.js', registerFn: 'registerMusicGenTools' },
314
+ { path: './one-prompt-producer.js', registerFn: 'registerOnePromptTools' },
314
315
  { path: './mobile-automation.js', registerFn: 'registerMobileAutomationTools' },
315
316
  { path: './iphone.js', registerFn: 'registerIPhoneTools' },
316
317
  { path: './ghost.js', registerFn: 'registerGhostTools' },
@@ -0,0 +1,2 @@
1
+ export declare function registerOnePromptTools(): void;
2
+ //# sourceMappingURL=one-prompt-producer.d.ts.map