@kernel.chat/kbot 3.69.1 → 3.71.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.
- package/dist/a2a.d.ts +50 -5
- package/dist/a2a.js +305 -44
- package/dist/auth.d.ts +4 -0
- package/dist/auth.js +43 -4
- package/dist/doctor.js +53 -7
- package/dist/integrations/ableton-bridge.d.ts +158 -0
- package/dist/integrations/ableton-bridge.js +486 -0
- package/dist/integrations/ableton-m4l.d.ts +94 -0
- package/dist/integrations/ableton-m4l.js +252 -1
- package/dist/integrations/install-remote-script.d.ts +23 -0
- package/dist/integrations/install-remote-script.js +121 -0
- package/dist/machine.d.ts +1 -0
- package/dist/machine.js +17 -1
- package/dist/serve.js +3 -2
- package/dist/tools/a2a.d.ts +2 -0
- package/dist/tools/a2a.js +233 -0
- package/dist/tools/ableton-bridge-tools.d.ts +14 -0
- package/dist/tools/ableton-bridge-tools.js +327 -0
- package/dist/tools/ai-analysis.d.ts +2 -0
- package/dist/tools/ai-analysis.js +677 -0
- package/dist/tools/financial-analysis.d.ts +2 -0
- package/dist/tools/financial-analysis.js +945 -0
- package/dist/tools/index.js +6 -0
- package/dist/tools/music-gen.d.ts +2 -0
- package/dist/tools/music-gen.js +1006 -0
- package/dist/tools/threat-intel.d.ts +2 -0
- package/dist/tools/threat-intel.js +1619 -0
- package/package.json +2 -2
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
// kbot A2A Tools — Agent-to-Agent protocol status and management
|
|
2
|
+
//
|
|
3
|
+
// Provides the a2a_status tool for inspecting the A2A server state,
|
|
4
|
+
// registered capabilities, active tasks, and discovered remote agents.
|
|
5
|
+
//
|
|
6
|
+
// Tools: a2a_status, a2a_discover, a2a_send, a2a_card
|
|
7
|
+
import { registerTool } from './index.js';
|
|
8
|
+
import { getA2AStatus, buildAgentCard, discoverAgent, delegateTask, listRemoteAgents, removeRemoteAgent, } from '../a2a.js';
|
|
9
|
+
export function registerA2ATools() {
|
|
10
|
+
// ── a2a_status ──
|
|
11
|
+
registerTool({
|
|
12
|
+
name: 'a2a_status',
|
|
13
|
+
description: 'Show A2A (Agent-to-Agent) protocol server status: whether the server is running, ' +
|
|
14
|
+
'registered agent capabilities (all 35 kbot specialists), task statistics, ' +
|
|
15
|
+
'active connections, and discovered remote agents.',
|
|
16
|
+
parameters: {
|
|
17
|
+
verbose: {
|
|
18
|
+
type: 'boolean',
|
|
19
|
+
description: 'Show full skill descriptions and all tags. Defaults to false (summary only).',
|
|
20
|
+
required: false,
|
|
21
|
+
default: false,
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
tier: 'free',
|
|
25
|
+
async execute(args) {
|
|
26
|
+
const verbose = args.verbose === true || args.verbose === 'true';
|
|
27
|
+
const status = getA2AStatus();
|
|
28
|
+
const lines = [];
|
|
29
|
+
// Server section
|
|
30
|
+
lines.push('=== A2A Server Status ===');
|
|
31
|
+
lines.push(`Running: ${status.server.running ? 'YES' : 'NO'}`);
|
|
32
|
+
if (status.server.running) {
|
|
33
|
+
lines.push(`Endpoint: ${status.server.endpointUrl}`);
|
|
34
|
+
lines.push(`Started: ${status.server.startedAt}`);
|
|
35
|
+
lines.push(`Uptime: ${status.server.uptime}`);
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
lines.push('(Server not started. Run `kbot serve` to start the A2A endpoint.)');
|
|
39
|
+
}
|
|
40
|
+
// Tasks section
|
|
41
|
+
lines.push('');
|
|
42
|
+
lines.push('=== Task Statistics ===');
|
|
43
|
+
lines.push(`Received: ${status.tasks.received}`);
|
|
44
|
+
lines.push(`Completed: ${status.tasks.completed}`);
|
|
45
|
+
lines.push(`Failed: ${status.tasks.failed}`);
|
|
46
|
+
lines.push(`Active: ${status.tasks.active}`);
|
|
47
|
+
lines.push(`In Store: ${status.tasks.stored}`);
|
|
48
|
+
// Capabilities section
|
|
49
|
+
lines.push('');
|
|
50
|
+
lines.push(`=== Registered Capabilities (${status.capabilities.totalSkills} agents) ===`);
|
|
51
|
+
if (verbose) {
|
|
52
|
+
for (const skill of status.capabilities.skills) {
|
|
53
|
+
lines.push(` ${skill.id}: ${skill.name}`);
|
|
54
|
+
lines.push(` Tags: ${skill.tags.join(', ')}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
// Compact: group by category
|
|
59
|
+
const ids = status.capabilities.skills.map(s => s.id);
|
|
60
|
+
lines.push(`Agents: ${ids.join(', ')}`);
|
|
61
|
+
}
|
|
62
|
+
// Connections section
|
|
63
|
+
lines.push('');
|
|
64
|
+
lines.push(`=== Active Connections (${status.connections.uniqueClients} unique clients) ===`);
|
|
65
|
+
if (status.connections.clients.length === 0) {
|
|
66
|
+
lines.push(' (no connections yet)');
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
for (const client of status.connections.clients) {
|
|
70
|
+
lines.push(` - ${client}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// Remote agents section
|
|
74
|
+
lines.push('');
|
|
75
|
+
lines.push(`=== Discovered Remote Agents (${status.registry.remoteAgents}) ===`);
|
|
76
|
+
if (status.registry.agents.length === 0) {
|
|
77
|
+
lines.push(' (none discovered — use a2a_discover to find remote agents)');
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
for (const agent of status.registry.agents) {
|
|
81
|
+
lines.push(` - ${agent.name} @ ${agent.url} (${agent.skills} skills)`);
|
|
82
|
+
if (agent.lastContact) {
|
|
83
|
+
lines.push(` Last contact: ${agent.lastContact}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return lines.join('\n');
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
// ── a2a_discover ──
|
|
91
|
+
registerTool({
|
|
92
|
+
name: 'a2a_discover',
|
|
93
|
+
description: 'Discover a remote A2A agent by URL. Fetches its Agent Card from ' +
|
|
94
|
+
'<url>/.well-known/agent.json and registers it in the local registry ' +
|
|
95
|
+
'for future task delegation.',
|
|
96
|
+
parameters: {
|
|
97
|
+
url: {
|
|
98
|
+
type: 'string',
|
|
99
|
+
description: 'Base URL of the remote agent (e.g. "http://other-agent:8080")',
|
|
100
|
+
required: true,
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
tier: 'free',
|
|
104
|
+
async execute(args) {
|
|
105
|
+
const url = String(args.url).trim();
|
|
106
|
+
if (!url)
|
|
107
|
+
return 'Error: url is required';
|
|
108
|
+
const card = await discoverAgent(url);
|
|
109
|
+
if (!card) {
|
|
110
|
+
return `Failed to discover agent at ${url}. Ensure the agent is running and exposes /.well-known/agent.json.`;
|
|
111
|
+
}
|
|
112
|
+
const lines = [
|
|
113
|
+
`Discovered: ${card.name} v${card.version}`,
|
|
114
|
+
`URL: ${card.url}`,
|
|
115
|
+
`Provider: ${card.provider.organization}`,
|
|
116
|
+
`Skills (${card.skills.length}):`,
|
|
117
|
+
...card.skills.map(s => ` - ${s.id}: ${s.name} [${s.tags.join(', ')}]`),
|
|
118
|
+
'',
|
|
119
|
+
'Agent registered in local registry. Use a2a_send to delegate tasks.',
|
|
120
|
+
];
|
|
121
|
+
return lines.join('\n');
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
// ── a2a_send ──
|
|
125
|
+
registerTool({
|
|
126
|
+
name: 'a2a_send',
|
|
127
|
+
description: 'Send a task to a remote A2A agent. Delegates work to another agent\'s ' +
|
|
128
|
+
'specialist via the A2A protocol. Optionally specify which agent skill to use.',
|
|
129
|
+
parameters: {
|
|
130
|
+
url: {
|
|
131
|
+
type: 'string',
|
|
132
|
+
description: 'Base URL of the remote agent',
|
|
133
|
+
required: true,
|
|
134
|
+
},
|
|
135
|
+
task: {
|
|
136
|
+
type: 'string',
|
|
137
|
+
description: 'The task prompt to send to the remote agent',
|
|
138
|
+
required: true,
|
|
139
|
+
},
|
|
140
|
+
agent: {
|
|
141
|
+
type: 'string',
|
|
142
|
+
description: 'Hint which specialist agent should handle the task (optional)',
|
|
143
|
+
required: false,
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
tier: 'free',
|
|
147
|
+
timeout: 180_000, // 3 minutes for remote task execution
|
|
148
|
+
async execute(args) {
|
|
149
|
+
const url = String(args.url).trim();
|
|
150
|
+
const task = String(args.task).trim();
|
|
151
|
+
const agent = args.agent ? String(args.agent).trim() : undefined;
|
|
152
|
+
if (!url || !task)
|
|
153
|
+
return 'Error: url and task are required';
|
|
154
|
+
const result = await delegateTask(url, task, { agent });
|
|
155
|
+
if (!result) {
|
|
156
|
+
return `Task delegation to ${url} failed. The remote agent may be down or the task execution failed.`;
|
|
157
|
+
}
|
|
158
|
+
return [
|
|
159
|
+
`=== Task Result from ${url} ===`,
|
|
160
|
+
result.text,
|
|
161
|
+
'',
|
|
162
|
+
result.metadata.agentUsed ? `Agent used: ${result.metadata.agentUsed}` : '',
|
|
163
|
+
result.metadata.model ? `Model: ${result.metadata.model}` : '',
|
|
164
|
+
].filter(Boolean).join('\n');
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
// ── a2a_card ──
|
|
168
|
+
registerTool({
|
|
169
|
+
name: 'a2a_card',
|
|
170
|
+
description: 'Generate and display kbot\'s A2A Agent Card — the JSON descriptor that ' +
|
|
171
|
+
'other agents use to discover kbot\'s capabilities. Shows all 35 specialist ' +
|
|
172
|
+
'agents as skills with tags and descriptions.',
|
|
173
|
+
parameters: {
|
|
174
|
+
url: {
|
|
175
|
+
type: 'string',
|
|
176
|
+
description: 'Override the endpoint URL in the card (defaults to the current server URL)',
|
|
177
|
+
required: false,
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
tier: 'free',
|
|
181
|
+
async execute(args) {
|
|
182
|
+
const url = args.url ? String(args.url).trim() : undefined;
|
|
183
|
+
const card = buildAgentCard(url);
|
|
184
|
+
return JSON.stringify(card, null, 2);
|
|
185
|
+
},
|
|
186
|
+
});
|
|
187
|
+
// ── a2a_remove ──
|
|
188
|
+
registerTool({
|
|
189
|
+
name: 'a2a_remove',
|
|
190
|
+
description: 'Remove a remote A2A agent from the local discovery registry.',
|
|
191
|
+
parameters: {
|
|
192
|
+
url: {
|
|
193
|
+
type: 'string',
|
|
194
|
+
description: 'Base URL of the remote agent to remove',
|
|
195
|
+
required: true,
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
tier: 'free',
|
|
199
|
+
async execute(args) {
|
|
200
|
+
const url = String(args.url).trim();
|
|
201
|
+
if (!url)
|
|
202
|
+
return 'Error: url is required';
|
|
203
|
+
const removed = removeRemoteAgent(url);
|
|
204
|
+
return removed
|
|
205
|
+
? `Removed ${url} from the A2A registry.`
|
|
206
|
+
: `Agent ${url} not found in the registry.`;
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
// ── a2a_list ──
|
|
210
|
+
registerTool({
|
|
211
|
+
name: 'a2a_list',
|
|
212
|
+
description: 'List all discovered remote A2A agents from the local registry.',
|
|
213
|
+
parameters: {},
|
|
214
|
+
tier: 'free',
|
|
215
|
+
async execute() {
|
|
216
|
+
const agents = listRemoteAgents();
|
|
217
|
+
if (agents.length === 0) {
|
|
218
|
+
return 'No remote agents discovered. Use a2a_discover to find agents.';
|
|
219
|
+
}
|
|
220
|
+
const lines = agents.map(a => {
|
|
221
|
+
const skills = a.card.skills.map(s => s.id).join(', ');
|
|
222
|
+
return [
|
|
223
|
+
`${a.card.name} (${a.url})`,
|
|
224
|
+
` Skills: ${skills}`,
|
|
225
|
+
` Discovered: ${a.discoveredAt}`,
|
|
226
|
+
a.lastContactedAt ? ` Last contact: ${a.lastContactedAt}` : '',
|
|
227
|
+
].filter(Boolean).join('\n');
|
|
228
|
+
});
|
|
229
|
+
return `=== Discovered Remote Agents (${agents.length}) ===\n\n${lines.join('\n\n')}`;
|
|
230
|
+
},
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
//# sourceMappingURL=a2a.js.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ableton-bridge-tools.ts — Ableton Browser & Device Loading Tools
|
|
3
|
+
*
|
|
4
|
+
* Uses AbletonBridge (port 9001) for full Ableton Browser API access.
|
|
5
|
+
* Falls back to KBotBridge (port 9998) if AbletonBridge is unavailable.
|
|
6
|
+
*
|
|
7
|
+
* Tools:
|
|
8
|
+
* ableton_load_effect — Load any Ableton native effect by name onto a track
|
|
9
|
+
* ableton_browse — Search Ableton's browser (instruments, effects, presets, samples)
|
|
10
|
+
* ableton_load_preset — Load a preset onto a device
|
|
11
|
+
* ableton_effect_chain — Apply a full chain of effects to a track in sequence
|
|
12
|
+
*/
|
|
13
|
+
export declare function registerAbletonBridgeTools(): void;
|
|
14
|
+
//# sourceMappingURL=ableton-bridge-tools.d.ts.map
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ableton-bridge-tools.ts — Ableton Browser & Device Loading Tools
|
|
3
|
+
*
|
|
4
|
+
* Uses AbletonBridge (port 9001) for full Ableton Browser API access.
|
|
5
|
+
* Falls back to KBotBridge (port 9998) if AbletonBridge is unavailable.
|
|
6
|
+
*
|
|
7
|
+
* Tools:
|
|
8
|
+
* ableton_load_effect — Load any Ableton native effect by name onto a track
|
|
9
|
+
* ableton_browse — Search Ableton's browser (instruments, effects, presets, samples)
|
|
10
|
+
* ableton_load_preset — Load a preset onto a device
|
|
11
|
+
* ableton_effect_chain — Apply a full chain of effects to a track in sequence
|
|
12
|
+
*/
|
|
13
|
+
import { registerTool } from './index.js';
|
|
14
|
+
import { tryAbletonBridge, tryKBotRemote, formatBridgeError, } from '../integrations/ableton-bridge.js';
|
|
15
|
+
// ── Helpers ─────────────────────────────────────────────────────────────
|
|
16
|
+
/** Convert 1-based user track to 0-based internal index. */
|
|
17
|
+
function userTrack(track) {
|
|
18
|
+
const n = Number(track);
|
|
19
|
+
return Math.max(0, n - 1);
|
|
20
|
+
}
|
|
21
|
+
/** Category aliases for user convenience. */
|
|
22
|
+
const CATEGORY_ALIASES = {
|
|
23
|
+
effects: 'audio_effects',
|
|
24
|
+
effect: 'audio_effects',
|
|
25
|
+
fx: 'audio_effects',
|
|
26
|
+
audio_fx: 'audio_effects',
|
|
27
|
+
midi_fx: 'midi_effects',
|
|
28
|
+
midi: 'midi_effects',
|
|
29
|
+
inst: 'instruments',
|
|
30
|
+
instrument: 'instruments',
|
|
31
|
+
drum: 'drums',
|
|
32
|
+
kit: 'drums',
|
|
33
|
+
sound: 'sounds',
|
|
34
|
+
pack: 'packs',
|
|
35
|
+
plugin: 'plugins',
|
|
36
|
+
vst: 'plugins',
|
|
37
|
+
au: 'plugins',
|
|
38
|
+
sample: 'samples',
|
|
39
|
+
preset: 'presets',
|
|
40
|
+
};
|
|
41
|
+
function resolveCategory(raw) {
|
|
42
|
+
if (!raw)
|
|
43
|
+
return undefined;
|
|
44
|
+
const lower = raw.toLowerCase().trim();
|
|
45
|
+
return CATEGORY_ALIASES[lower] ?? lower;
|
|
46
|
+
}
|
|
47
|
+
/** Format browser items for display. */
|
|
48
|
+
function formatBrowserItems(items, limit = 20) {
|
|
49
|
+
if (items.length === 0)
|
|
50
|
+
return 'No results found.';
|
|
51
|
+
const shown = items.slice(0, limit);
|
|
52
|
+
const lines = shown.map((item) => {
|
|
53
|
+
const tags = [];
|
|
54
|
+
if (item.isDevice)
|
|
55
|
+
tags.push('device');
|
|
56
|
+
if (item.isFolder)
|
|
57
|
+
tags.push('folder');
|
|
58
|
+
if (item.isLoadable)
|
|
59
|
+
tags.push('loadable');
|
|
60
|
+
const tagStr = tags.length > 0 ? ` [${tags.join(', ')}]` : '';
|
|
61
|
+
return `- **${item.name}**${tagStr}\n URI: \`${item.uri}\``;
|
|
62
|
+
});
|
|
63
|
+
let result = lines.join('\n');
|
|
64
|
+
if (items.length > limit) {
|
|
65
|
+
result += `\n\n_...and ${items.length - limit} more results_`;
|
|
66
|
+
}
|
|
67
|
+
return result;
|
|
68
|
+
}
|
|
69
|
+
// ── Tool Registration ───────────────────────────────────────────────────
|
|
70
|
+
export function registerAbletonBridgeTools() {
|
|
71
|
+
// ─── 1. Load Effect ───────────────────────────────────────────────────
|
|
72
|
+
registerTool({
|
|
73
|
+
name: 'ableton_load_effect',
|
|
74
|
+
description: 'Load any Ableton native audio effect by name onto a track. ' +
|
|
75
|
+
'This is the primary tool for adding effects like Saturator, Reverb, Compressor, EQ Eight, Auto Filter, etc. ' +
|
|
76
|
+
'Searches Ableton\'s browser via AbletonBridge and loads the device directly. ' +
|
|
77
|
+
'Supports position control to place the effect before or after existing devices.',
|
|
78
|
+
parameters: {
|
|
79
|
+
track: { type: 'number', description: 'Track number (1-based)', required: true },
|
|
80
|
+
name: { type: 'string', description: 'Effect name (e.g. "Saturator", "Reverb", "Compressor", "EQ Eight", "Auto Filter", "Chorus-Ensemble")', required: true },
|
|
81
|
+
position: {
|
|
82
|
+
type: 'string',
|
|
83
|
+
description: 'Where to place the effect: "before" (start of chain), "after" (after last device), "end" (same as after). Default: "end"',
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
tier: 'free',
|
|
87
|
+
timeout: 15_000,
|
|
88
|
+
async execute(args) {
|
|
89
|
+
const t = userTrack(args.track);
|
|
90
|
+
const name = String(args.name).trim();
|
|
91
|
+
const position = String(args.position ?? 'end').toLowerCase();
|
|
92
|
+
try {
|
|
93
|
+
// Try AbletonBridge first (full browser API)
|
|
94
|
+
const ab = await tryAbletonBridge();
|
|
95
|
+
if (ab) {
|
|
96
|
+
// Search specifically in audio_effects category
|
|
97
|
+
const items = await ab.searchBrowser(name, 'audio_effects');
|
|
98
|
+
// Find exact name match first, then partial match
|
|
99
|
+
const exactMatch = items.find((item) => item.isLoadable && item.name.toLowerCase() === name.toLowerCase());
|
|
100
|
+
const partialMatch = items.find((item) => item.isLoadable && item.name.toLowerCase().includes(name.toLowerCase()));
|
|
101
|
+
const target = exactMatch ?? partialMatch;
|
|
102
|
+
if (!target) {
|
|
103
|
+
// Try broader search without category filter
|
|
104
|
+
const broadItems = await ab.searchBrowser(name);
|
|
105
|
+
const broadMatch = broadItems.find((item) => item.isLoadable && (item.isDevice || item.name.toLowerCase().includes(name.toLowerCase())));
|
|
106
|
+
if (broadMatch) {
|
|
107
|
+
await ab.loadDevice(t, broadMatch.uri);
|
|
108
|
+
return `Loaded **${broadMatch.name}** on track ${args.track} (via browser search)`;
|
|
109
|
+
}
|
|
110
|
+
return `Effect "${name}" not found in Ableton's browser. Check the exact name (e.g. "EQ Eight" not "EQ8").`;
|
|
111
|
+
}
|
|
112
|
+
await ab.loadDevice(t, target.uri);
|
|
113
|
+
// Handle position if not "end" — move device within chain
|
|
114
|
+
if (position === 'before') {
|
|
115
|
+
const chain = await ab.getEffectChain(t);
|
|
116
|
+
if (chain.length > 1) {
|
|
117
|
+
// The newly loaded device is at the end — note this for the user
|
|
118
|
+
return `Loaded **${target.name}** on track ${args.track} (at end of chain — ${chain.length} devices total). Note: position reordering requires manual adjustment in Ableton.`;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return `Loaded **${target.name}** on track ${args.track}`;
|
|
122
|
+
}
|
|
123
|
+
// Fallback to KBotBridge Remote Script
|
|
124
|
+
const kb = await tryKBotRemote();
|
|
125
|
+
if (kb) {
|
|
126
|
+
const ok = await kb.loadDevice(t, name);
|
|
127
|
+
if (ok) {
|
|
128
|
+
return `Loaded **${name}** on track ${args.track} (via KBotBridge)`;
|
|
129
|
+
}
|
|
130
|
+
return `KBotBridge could not load "${name}". The device may not be found in the browser.`;
|
|
131
|
+
}
|
|
132
|
+
// Neither bridge available
|
|
133
|
+
return formatBridgeError();
|
|
134
|
+
}
|
|
135
|
+
catch (err) {
|
|
136
|
+
return `Failed to load effect: ${err.message}\n\n${formatBridgeError()}`;
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
// ─── 2. Browse ────────────────────────────────────────────────────────
|
|
141
|
+
registerTool({
|
|
142
|
+
name: 'ableton_browse',
|
|
143
|
+
description: 'Search Ableton\'s browser for instruments, effects, presets, samples, packs, and plugins. ' +
|
|
144
|
+
'Returns matching items with their URIs for loading. ' +
|
|
145
|
+
'Use category to narrow results: "instruments", "audio_effects", "midi_effects", "drums", "sounds", "packs", "plugins", "samples", "presets".',
|
|
146
|
+
parameters: {
|
|
147
|
+
query: { type: 'string', description: 'Search query (e.g. "reverb", "piano", "808")', required: true },
|
|
148
|
+
category: {
|
|
149
|
+
type: 'string',
|
|
150
|
+
description: 'Category filter: instruments, audio_effects (or "fx"), midi_effects (or "midi_fx"), drums, sounds, packs, plugins (or "vst"), samples, presets',
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
tier: 'free',
|
|
154
|
+
timeout: 15_000,
|
|
155
|
+
async execute(args) {
|
|
156
|
+
const query = String(args.query).trim();
|
|
157
|
+
const category = resolveCategory(args.category);
|
|
158
|
+
try {
|
|
159
|
+
// Try AbletonBridge first
|
|
160
|
+
const ab = await tryAbletonBridge();
|
|
161
|
+
if (ab) {
|
|
162
|
+
const items = await ab.searchBrowser(query, category);
|
|
163
|
+
const header = category
|
|
164
|
+
? `## Browser Search: "${query}" in ${category}`
|
|
165
|
+
: `## Browser Search: "${query}"`;
|
|
166
|
+
return `${header}\n\n${formatBrowserItems(items)}`;
|
|
167
|
+
}
|
|
168
|
+
// Fallback to KBotBridge
|
|
169
|
+
const kb = await tryKBotRemote();
|
|
170
|
+
if (kb) {
|
|
171
|
+
const items = await kb.searchBrowser(query);
|
|
172
|
+
return `## Browser Search: "${query}" (via KBotBridge)\n\n${formatBrowserItems(items)}`;
|
|
173
|
+
}
|
|
174
|
+
return formatBridgeError();
|
|
175
|
+
}
|
|
176
|
+
catch (err) {
|
|
177
|
+
return `Browse failed: ${err.message}\n\n${formatBridgeError()}`;
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
});
|
|
181
|
+
// ─── 3. Load Preset ───────────────────────────────────────────────────
|
|
182
|
+
registerTool({
|
|
183
|
+
name: 'ableton_load_preset',
|
|
184
|
+
description: 'Load a preset onto a device on a track. Searches available presets for the device and loads the best match. ' +
|
|
185
|
+
'Use ableton_browse first to find the device URI if needed.',
|
|
186
|
+
parameters: {
|
|
187
|
+
track: { type: 'number', description: 'Track number (1-based)', required: true },
|
|
188
|
+
device: { type: 'number', description: 'Device index on the track (0-based, first device = 0)', required: true },
|
|
189
|
+
preset_name: { type: 'string', description: 'Preset name to search for (e.g. "Warm Pad", "Clean Lead")', required: true },
|
|
190
|
+
},
|
|
191
|
+
tier: 'free',
|
|
192
|
+
timeout: 15_000,
|
|
193
|
+
async execute(args) {
|
|
194
|
+
const t = userTrack(args.track);
|
|
195
|
+
const deviceIdx = Number(args.device);
|
|
196
|
+
const presetName = String(args.preset_name).trim();
|
|
197
|
+
try {
|
|
198
|
+
const ab = await tryAbletonBridge();
|
|
199
|
+
if (!ab) {
|
|
200
|
+
return 'Preset loading requires AbletonBridge (port 9001). KBotBridge does not support preset browsing.\n\n' + formatBridgeError();
|
|
201
|
+
}
|
|
202
|
+
// Get the device chain to find the device URI
|
|
203
|
+
const chain = await ab.getEffectChain(t);
|
|
204
|
+
if (chain.length === 0) {
|
|
205
|
+
return `No devices on track ${args.track}. Load a device first with ableton_load_effect.`;
|
|
206
|
+
}
|
|
207
|
+
if (deviceIdx >= chain.length) {
|
|
208
|
+
return `Track ${args.track} has ${chain.length} device(s) (indices 0-${chain.length - 1}). Device index ${deviceIdx} is out of range.`;
|
|
209
|
+
}
|
|
210
|
+
const device = chain[deviceIdx];
|
|
211
|
+
// Search for presets matching the device + preset name
|
|
212
|
+
const presetItems = await ab.searchBrowser(`${device.name} ${presetName}`, 'presets');
|
|
213
|
+
// Try to find a matching preset
|
|
214
|
+
const exactMatch = presetItems.find((p) => p.isLoadable && p.name.toLowerCase() === presetName.toLowerCase());
|
|
215
|
+
const partialMatch = presetItems.find((p) => p.isLoadable && p.name.toLowerCase().includes(presetName.toLowerCase()));
|
|
216
|
+
const target = exactMatch ?? partialMatch ?? presetItems.find((p) => p.isLoadable);
|
|
217
|
+
if (!target) {
|
|
218
|
+
// List available presets for the device
|
|
219
|
+
const devicePresets = await ab.searchBrowser(device.name, 'presets');
|
|
220
|
+
if (devicePresets.length > 0) {
|
|
221
|
+
const presetList = devicePresets
|
|
222
|
+
.filter((p) => p.isLoadable)
|
|
223
|
+
.slice(0, 10)
|
|
224
|
+
.map((p) => ` - ${p.name}`)
|
|
225
|
+
.join('\n');
|
|
226
|
+
return `No preset matching "${presetName}" for ${device.name}.\n\nAvailable presets:\n${presetList}`;
|
|
227
|
+
}
|
|
228
|
+
return `No presets found for "${presetName}" on ${device.name} (device ${deviceIdx}).`;
|
|
229
|
+
}
|
|
230
|
+
await ab.loadPreset(t, deviceIdx, target.uri);
|
|
231
|
+
return `Loaded preset **${target.name}** onto **${device.name}** (track ${args.track}, device ${deviceIdx})`;
|
|
232
|
+
}
|
|
233
|
+
catch (err) {
|
|
234
|
+
return `Failed to load preset: ${err.message}`;
|
|
235
|
+
}
|
|
236
|
+
},
|
|
237
|
+
});
|
|
238
|
+
// ─── 4. Effect Chain ──────────────────────────────────────────────────
|
|
239
|
+
registerTool({
|
|
240
|
+
name: 'ableton_effect_chain',
|
|
241
|
+
description: 'Apply a full chain of audio effects to a track in sequence. ' +
|
|
242
|
+
'Loads each effect one by one from Ableton\'s browser. ' +
|
|
243
|
+
'Great for setting up standard chains like "Compressor → EQ Eight → Saturator → Reverb".',
|
|
244
|
+
parameters: {
|
|
245
|
+
track: { type: 'number', description: 'Track number (1-based)', required: true },
|
|
246
|
+
chain: {
|
|
247
|
+
type: 'array',
|
|
248
|
+
description: 'Array of effect names to load in order (e.g. ["Compressor", "EQ Eight", "Saturator", "Reverb"])',
|
|
249
|
+
required: true,
|
|
250
|
+
items: { type: 'string' },
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
tier: 'free',
|
|
254
|
+
timeout: 60_000,
|
|
255
|
+
async execute(args) {
|
|
256
|
+
const t = userTrack(args.track);
|
|
257
|
+
const chain = args.chain;
|
|
258
|
+
if (!Array.isArray(chain) || chain.length === 0) {
|
|
259
|
+
return 'Error: `chain` must be an array of effect names (e.g. ["Compressor", "EQ Eight", "Reverb"]).';
|
|
260
|
+
}
|
|
261
|
+
const results = [`## Effect Chain → Track ${args.track}`, ''];
|
|
262
|
+
let loaded = 0;
|
|
263
|
+
let failed = 0;
|
|
264
|
+
try {
|
|
265
|
+
// Try AbletonBridge first
|
|
266
|
+
const ab = await tryAbletonBridge();
|
|
267
|
+
if (ab) {
|
|
268
|
+
for (const effectName of chain) {
|
|
269
|
+
const name = String(effectName).trim();
|
|
270
|
+
try {
|
|
271
|
+
const items = await ab.searchBrowser(name, 'audio_effects');
|
|
272
|
+
const exactMatch = items.find((item) => item.isLoadable && item.name.toLowerCase() === name.toLowerCase());
|
|
273
|
+
const partialMatch = items.find((item) => item.isLoadable && item.name.toLowerCase().includes(name.toLowerCase()));
|
|
274
|
+
const target = exactMatch ?? partialMatch;
|
|
275
|
+
if (target) {
|
|
276
|
+
await ab.loadDevice(t, target.uri);
|
|
277
|
+
results.push(`- **${target.name}** loaded`);
|
|
278
|
+
loaded++;
|
|
279
|
+
}
|
|
280
|
+
else {
|
|
281
|
+
results.push(`- **${name}** — not found in browser`);
|
|
282
|
+
failed++;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
catch (err) {
|
|
286
|
+
results.push(`- **${name}** — error: ${err.message}`);
|
|
287
|
+
failed++;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
results.push('');
|
|
291
|
+
results.push(`**${loaded}** loaded, **${failed}** failed out of ${chain.length} effects.`);
|
|
292
|
+
return results.join('\n');
|
|
293
|
+
}
|
|
294
|
+
// Fallback to KBotBridge — try loading each effect
|
|
295
|
+
const kb = await tryKBotRemote();
|
|
296
|
+
if (kb) {
|
|
297
|
+
for (const effectName of chain) {
|
|
298
|
+
const name = String(effectName).trim();
|
|
299
|
+
try {
|
|
300
|
+
const ok = await kb.loadDevice(t, name);
|
|
301
|
+
if (ok) {
|
|
302
|
+
results.push(`- **${name}** loaded`);
|
|
303
|
+
loaded++;
|
|
304
|
+
}
|
|
305
|
+
else {
|
|
306
|
+
results.push(`- **${name}** — not found`);
|
|
307
|
+
failed++;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
catch (err) {
|
|
311
|
+
results.push(`- **${name}** — error: ${err.message}`);
|
|
312
|
+
failed++;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
results.push('');
|
|
316
|
+
results.push(`**${loaded}** loaded, **${failed}** failed out of ${chain.length} effects (via KBotBridge).`);
|
|
317
|
+
return results.join('\n');
|
|
318
|
+
}
|
|
319
|
+
return formatBridgeError();
|
|
320
|
+
}
|
|
321
|
+
catch (err) {
|
|
322
|
+
return `Effect chain failed: ${err.message}\n\n${formatBridgeError()}`;
|
|
323
|
+
}
|
|
324
|
+
},
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
//# sourceMappingURL=ableton-bridge-tools.js.map
|