@kernel.chat/kbot 3.70.0 → 3.72.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/cli.js +7 -1
- 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/integrations/mobile-mcp-client.d.ts +111 -0
- package/dist/integrations/mobile-mcp-client.js +343 -0
- package/dist/serve.d.ts +3 -0
- package/dist/serve.js +51 -7
- package/dist/tools/ableton-bridge-tools.d.ts +14 -0
- package/dist/tools/ableton-bridge-tools.js +327 -0
- package/dist/tools/index.js +3 -0
- package/dist/tools/iphone.d.ts +2 -0
- package/dist/tools/iphone.js +800 -0
- package/dist/tools/mobile-automation.d.ts +2 -0
- package/dist/tools/mobile-automation.js +612 -0
- package/dist/tools/serum2-preset.js +27 -0
- package/package.json +2 -2
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mobile-mcp-client.ts — kbot <-> mobile-mcp integration
|
|
3
|
+
*
|
|
4
|
+
* Singleton client that manages the mobile-mcp server process lifecycle.
|
|
5
|
+
* Communicates via MCP protocol over stdio transport.
|
|
6
|
+
* Auto-installs @mobilenext/mobile-mcp via npm if not present.
|
|
7
|
+
*
|
|
8
|
+
* mobile-mcp provides native accessibility-tree-based automation for
|
|
9
|
+
* iOS and Android devices connected via USB or WiFi.
|
|
10
|
+
*
|
|
11
|
+
* @see https://github.com/mobile-next/mobile-mcp
|
|
12
|
+
*/
|
|
13
|
+
import { spawn, execSync } from 'node:child_process';
|
|
14
|
+
import { Buffer } from 'node:buffer';
|
|
15
|
+
function encodeJsonRpc(msg) {
|
|
16
|
+
const body = JSON.stringify(msg);
|
|
17
|
+
return `Content-Length: ${Buffer.byteLength(body)}\r\n\r\n${body}`;
|
|
18
|
+
}
|
|
19
|
+
// ── MobileMCPClient ────────────────────────────────────────────────────
|
|
20
|
+
export class MobileMCPClient {
|
|
21
|
+
static instance = null;
|
|
22
|
+
process = null;
|
|
23
|
+
messageId = 0;
|
|
24
|
+
pending = new Map();
|
|
25
|
+
buffer = '';
|
|
26
|
+
initialized = false;
|
|
27
|
+
activeDeviceId = null;
|
|
28
|
+
static getInstance() {
|
|
29
|
+
if (!MobileMCPClient.instance) {
|
|
30
|
+
MobileMCPClient.instance = new MobileMCPClient();
|
|
31
|
+
}
|
|
32
|
+
return MobileMCPClient.instance;
|
|
33
|
+
}
|
|
34
|
+
/** Whether the MCP server process is running and initialized */
|
|
35
|
+
get isConnected() {
|
|
36
|
+
return this.initialized && this.process !== null && !this.process.killed;
|
|
37
|
+
}
|
|
38
|
+
/** The device ID currently being controlled */
|
|
39
|
+
get currentDeviceId() {
|
|
40
|
+
return this.activeDeviceId;
|
|
41
|
+
}
|
|
42
|
+
// ── Process lifecycle ──────────────────────────────────────────────
|
|
43
|
+
/** Start the mobile-mcp server process and perform MCP handshake */
|
|
44
|
+
async start() {
|
|
45
|
+
if (this.isConnected)
|
|
46
|
+
return;
|
|
47
|
+
// Ensure npx is available
|
|
48
|
+
try {
|
|
49
|
+
execSync('which npx', { stdio: 'pipe' });
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
throw new Error('npx not found. Ensure Node.js >= 22 is installed.');
|
|
53
|
+
}
|
|
54
|
+
// Spawn the mobile-mcp server via npx (auto-installs if needed)
|
|
55
|
+
this.process = spawn('npx', ['-y', '@mobilenext/mobile-mcp@latest'], {
|
|
56
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
57
|
+
env: { ...process.env },
|
|
58
|
+
});
|
|
59
|
+
this.buffer = '';
|
|
60
|
+
this.messageId = 0;
|
|
61
|
+
this.pending.clear();
|
|
62
|
+
this.process.stdout?.on('data', (chunk) => {
|
|
63
|
+
this.buffer += chunk.toString();
|
|
64
|
+
this.parseMessages();
|
|
65
|
+
});
|
|
66
|
+
// Log stderr for debugging but don't crash
|
|
67
|
+
this.process.stderr?.on('data', (chunk) => {
|
|
68
|
+
const msg = chunk.toString().trim();
|
|
69
|
+
if (msg && process.env.KBOT_DEBUG) {
|
|
70
|
+
console.error(`[mobile-mcp stderr] ${msg}`);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
this.process.on('error', (err) => {
|
|
74
|
+
this.initialized = false;
|
|
75
|
+
this.process = null;
|
|
76
|
+
if (process.env.KBOT_DEBUG) {
|
|
77
|
+
console.error(`[mobile-mcp] Process error: ${err.message}`);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
this.process.on('exit', (code) => {
|
|
81
|
+
this.initialized = false;
|
|
82
|
+
this.process = null;
|
|
83
|
+
// Reject any pending requests
|
|
84
|
+
const pendingEntries = Array.from(this.pending.entries());
|
|
85
|
+
for (const [id, { reject }] of pendingEntries) {
|
|
86
|
+
reject(new Error(`mobile-mcp process exited with code ${code}`));
|
|
87
|
+
this.pending.delete(id);
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
// MCP initialize handshake
|
|
91
|
+
try {
|
|
92
|
+
await this.sendRequest('initialize', {
|
|
93
|
+
protocolVersion: '2024-11-05',
|
|
94
|
+
capabilities: {},
|
|
95
|
+
clientInfo: { name: 'kbot', version: '3.61.0' },
|
|
96
|
+
});
|
|
97
|
+
this.sendNotification('initialized', {});
|
|
98
|
+
this.initialized = true;
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
this.stop();
|
|
102
|
+
throw new Error(`mobile-mcp handshake failed: ${err instanceof Error ? err.message : String(err)}\n` +
|
|
103
|
+
'Ensure @mobilenext/mobile-mcp is installed: npm install -g @mobilenext/mobile-mcp');
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/** Stop the mobile-mcp server process */
|
|
107
|
+
stop() {
|
|
108
|
+
if (this.process) {
|
|
109
|
+
try {
|
|
110
|
+
// Graceful shutdown
|
|
111
|
+
this.sendNotification('exit', null);
|
|
112
|
+
}
|
|
113
|
+
catch { /* best effort */ }
|
|
114
|
+
this.process.kill();
|
|
115
|
+
this.process = null;
|
|
116
|
+
}
|
|
117
|
+
this.initialized = false;
|
|
118
|
+
this.activeDeviceId = null;
|
|
119
|
+
this.buffer = '';
|
|
120
|
+
this.pending.clear();
|
|
121
|
+
}
|
|
122
|
+
// ── MCP protocol ───────────────────────────────────────────────────
|
|
123
|
+
parseMessages() {
|
|
124
|
+
while (true) {
|
|
125
|
+
const headerEnd = this.buffer.indexOf('\r\n\r\n');
|
|
126
|
+
if (headerEnd === -1)
|
|
127
|
+
break;
|
|
128
|
+
const header = this.buffer.slice(0, headerEnd);
|
|
129
|
+
const lengthMatch = header.match(/Content-Length:\s*(\d+)/i);
|
|
130
|
+
if (!lengthMatch) {
|
|
131
|
+
this.buffer = this.buffer.slice(headerEnd + 4);
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
const contentLength = parseInt(lengthMatch[1], 10);
|
|
135
|
+
const bodyStart = headerEnd + 4;
|
|
136
|
+
if (this.buffer.length < bodyStart + contentLength)
|
|
137
|
+
break;
|
|
138
|
+
const body = this.buffer.slice(bodyStart, bodyStart + contentLength);
|
|
139
|
+
this.buffer = this.buffer.slice(bodyStart + contentLength);
|
|
140
|
+
try {
|
|
141
|
+
const msg = JSON.parse(body);
|
|
142
|
+
if (msg.id !== undefined && this.pending.has(msg.id)) {
|
|
143
|
+
const { resolve, reject } = this.pending.get(msg.id);
|
|
144
|
+
this.pending.delete(msg.id);
|
|
145
|
+
if (msg.error) {
|
|
146
|
+
reject(new Error(msg.error.message));
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
resolve(msg.result);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
// Skip malformed messages
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
sendRequest(method, params, timeout = 30_000) {
|
|
159
|
+
return new Promise((resolve, reject) => {
|
|
160
|
+
if (!this.process?.stdin?.writable) {
|
|
161
|
+
reject(new Error('mobile-mcp process is not running. Call mobile_connect first.'));
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
const id = ++this.messageId;
|
|
165
|
+
this.pending.set(id, { resolve, reject });
|
|
166
|
+
const msg = { jsonrpc: '2.0', id, method, params };
|
|
167
|
+
this.process.stdin.write(encodeJsonRpc(msg));
|
|
168
|
+
setTimeout(() => {
|
|
169
|
+
if (this.pending.has(id)) {
|
|
170
|
+
this.pending.delete(id);
|
|
171
|
+
reject(new Error(`mobile-mcp request timeout after ${timeout / 1000}s: ${method}`));
|
|
172
|
+
}
|
|
173
|
+
}, timeout);
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
sendNotification(method, params) {
|
|
177
|
+
if (!this.process?.stdin?.writable)
|
|
178
|
+
return;
|
|
179
|
+
const msg = { jsonrpc: '2.0', method, params };
|
|
180
|
+
this.process.stdin.write(encodeJsonRpc(msg));
|
|
181
|
+
}
|
|
182
|
+
/** Call a tool on the mobile-mcp server */
|
|
183
|
+
async callTool(toolName, args) {
|
|
184
|
+
if (!this.isConnected) {
|
|
185
|
+
throw new Error('Not connected to mobile-mcp. Call mobile_connect first.');
|
|
186
|
+
}
|
|
187
|
+
const result = await this.sendRequest('tools/call', {
|
|
188
|
+
name: toolName,
|
|
189
|
+
arguments: args,
|
|
190
|
+
}, 60_000);
|
|
191
|
+
return result;
|
|
192
|
+
}
|
|
193
|
+
/** Extract text content from an MCP tool result */
|
|
194
|
+
extractText(result) {
|
|
195
|
+
const r = result;
|
|
196
|
+
if (r?.content) {
|
|
197
|
+
return r.content
|
|
198
|
+
.filter(c => c.type === 'text' && c.text)
|
|
199
|
+
.map(c => c.text)
|
|
200
|
+
.join('\n');
|
|
201
|
+
}
|
|
202
|
+
return JSON.stringify(result, null, 2);
|
|
203
|
+
}
|
|
204
|
+
/** Extract image content (base64) from an MCP tool result */
|
|
205
|
+
extractImage(result) {
|
|
206
|
+
const r = result;
|
|
207
|
+
if (r?.content) {
|
|
208
|
+
const img = r.content.find(c => c.type === 'image' && c.data);
|
|
209
|
+
if (img)
|
|
210
|
+
return { data: img.data, mimeType: img.mimeType || 'image/png' };
|
|
211
|
+
}
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
// ── High-level device operations ───────────────────────────────────
|
|
215
|
+
/** List all available devices */
|
|
216
|
+
async listDevices() {
|
|
217
|
+
const result = await this.callTool('mobile_list_available_devices', {});
|
|
218
|
+
const text = this.extractText(result);
|
|
219
|
+
try {
|
|
220
|
+
return JSON.parse(text);
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
// Try to parse from structured output
|
|
224
|
+
return [];
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
/** Set the active device for subsequent operations */
|
|
228
|
+
setActiveDevice(deviceId) {
|
|
229
|
+
this.activeDeviceId = deviceId;
|
|
230
|
+
}
|
|
231
|
+
/** Get the active device ID, throwing if none set */
|
|
232
|
+
requireDevice(deviceId) {
|
|
233
|
+
const id = deviceId || this.activeDeviceId;
|
|
234
|
+
if (!id) {
|
|
235
|
+
throw new Error('No device selected. Use mobile_connect to connect to a device, or pass a device ID.');
|
|
236
|
+
}
|
|
237
|
+
return id;
|
|
238
|
+
}
|
|
239
|
+
/** List apps on the active device */
|
|
240
|
+
async listApps(deviceId) {
|
|
241
|
+
const device = this.requireDevice(deviceId);
|
|
242
|
+
const result = await this.callTool('mobile_list_apps', { device });
|
|
243
|
+
return this.extractText(result);
|
|
244
|
+
}
|
|
245
|
+
/** Launch an app by bundle ID */
|
|
246
|
+
async launchApp(packageName, deviceId) {
|
|
247
|
+
const device = this.requireDevice(deviceId);
|
|
248
|
+
const result = await this.callTool('mobile_launch_app', { device, packageName });
|
|
249
|
+
return this.extractText(result);
|
|
250
|
+
}
|
|
251
|
+
/** Take a screenshot, returns base64 image data */
|
|
252
|
+
async takeScreenshot(deviceId) {
|
|
253
|
+
const device = this.requireDevice(deviceId);
|
|
254
|
+
const result = await this.callTool('mobile_take_screenshot', { device });
|
|
255
|
+
const img = this.extractImage(result);
|
|
256
|
+
if (img)
|
|
257
|
+
return img;
|
|
258
|
+
return this.extractText(result);
|
|
259
|
+
}
|
|
260
|
+
/** Save screenshot to a file */
|
|
261
|
+
async saveScreenshot(saveTo, deviceId) {
|
|
262
|
+
const device = this.requireDevice(deviceId);
|
|
263
|
+
const result = await this.callTool('mobile_save_screenshot', { device, saveTo });
|
|
264
|
+
return this.extractText(result);
|
|
265
|
+
}
|
|
266
|
+
/** List UI elements on screen via accessibility tree */
|
|
267
|
+
async listElements(deviceId) {
|
|
268
|
+
const device = this.requireDevice(deviceId);
|
|
269
|
+
const result = await this.callTool('mobile_list_elements_on_screen', { device });
|
|
270
|
+
return this.extractText(result);
|
|
271
|
+
}
|
|
272
|
+
/** Tap at coordinates */
|
|
273
|
+
async tap(x, y, deviceId) {
|
|
274
|
+
const device = this.requireDevice(deviceId);
|
|
275
|
+
const result = await this.callTool('mobile_click_on_screen_at_coordinates', { device, x, y });
|
|
276
|
+
return this.extractText(result);
|
|
277
|
+
}
|
|
278
|
+
/** Swipe on screen */
|
|
279
|
+
async swipe(direction, opts) {
|
|
280
|
+
const device = this.requireDevice(opts?.deviceId);
|
|
281
|
+
const args = { device, direction };
|
|
282
|
+
if (opts?.x !== undefined)
|
|
283
|
+
args.x = opts.x;
|
|
284
|
+
if (opts?.y !== undefined)
|
|
285
|
+
args.y = opts.y;
|
|
286
|
+
if (opts?.distance !== undefined)
|
|
287
|
+
args.distance = opts.distance;
|
|
288
|
+
const result = await this.callTool('mobile_swipe_on_screen', args);
|
|
289
|
+
return this.extractText(result);
|
|
290
|
+
}
|
|
291
|
+
/** Type text */
|
|
292
|
+
async typeText(text, submit = false, deviceId) {
|
|
293
|
+
const device = this.requireDevice(deviceId);
|
|
294
|
+
const result = await this.callTool('mobile_type_keys', { device, text, submit });
|
|
295
|
+
return this.extractText(result);
|
|
296
|
+
}
|
|
297
|
+
/** Press a device button */
|
|
298
|
+
async pressButton(button, deviceId) {
|
|
299
|
+
const device = this.requireDevice(deviceId);
|
|
300
|
+
const result = await this.callTool('mobile_press_button', { device, button });
|
|
301
|
+
return this.extractText(result);
|
|
302
|
+
}
|
|
303
|
+
/** Get screen size */
|
|
304
|
+
async getScreenSize(deviceId) {
|
|
305
|
+
const device = this.requireDevice(deviceId);
|
|
306
|
+
const result = await this.callTool('mobile_get_screen_size', { device });
|
|
307
|
+
return this.extractText(result);
|
|
308
|
+
}
|
|
309
|
+
/** Open a URL in the device browser */
|
|
310
|
+
async openUrl(url, deviceId) {
|
|
311
|
+
const device = this.requireDevice(deviceId);
|
|
312
|
+
const result = await this.callTool('mobile_open_url', { device, url });
|
|
313
|
+
return this.extractText(result);
|
|
314
|
+
}
|
|
315
|
+
/** Get device orientation */
|
|
316
|
+
async getOrientation(deviceId) {
|
|
317
|
+
const device = this.requireDevice(deviceId);
|
|
318
|
+
const result = await this.callTool('mobile_get_orientation', { device });
|
|
319
|
+
return this.extractText(result);
|
|
320
|
+
}
|
|
321
|
+
/** Terminate an app */
|
|
322
|
+
async terminateApp(packageName, deviceId) {
|
|
323
|
+
const device = this.requireDevice(deviceId);
|
|
324
|
+
const result = await this.callTool('mobile_terminate_app', { device, packageName });
|
|
325
|
+
return this.extractText(result);
|
|
326
|
+
}
|
|
327
|
+
/** Double tap at coordinates */
|
|
328
|
+
async doubleTap(x, y, deviceId) {
|
|
329
|
+
const device = this.requireDevice(deviceId);
|
|
330
|
+
const result = await this.callTool('mobile_double_tap_on_screen', { device, x, y });
|
|
331
|
+
return this.extractText(result);
|
|
332
|
+
}
|
|
333
|
+
/** Long press at coordinates */
|
|
334
|
+
async longPress(x, y, duration, deviceId) {
|
|
335
|
+
const device = this.requireDevice(deviceId);
|
|
336
|
+
const args = { device, x, y };
|
|
337
|
+
if (duration !== undefined)
|
|
338
|
+
args.duration = duration;
|
|
339
|
+
const result = await this.callTool('mobile_long_press_on_screen_at_coordinates', args);
|
|
340
|
+
return this.extractText(result);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
//# sourceMappingURL=mobile-mcp-client.js.map
|
package/dist/serve.d.ts
CHANGED
package/dist/serve.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
// kbot Serve — HTTP server that exposes all tools over REST
|
|
1
|
+
// kbot Serve — HTTP/HTTPS server that exposes all tools over REST
|
|
2
2
|
//
|
|
3
3
|
// Usage:
|
|
4
4
|
// kbot serve # Start on default port 7437
|
|
5
5
|
// kbot serve --port 3000 # Custom port
|
|
6
6
|
// kbot serve --token mysecret # Require auth token
|
|
7
|
+
// kbot serve --https # HTTPS with auto-generated self-signed cert
|
|
8
|
+
// kbot serve --cert x.pem --key x.key # HTTPS with custom cert
|
|
7
9
|
//
|
|
8
10
|
// Endpoints:
|
|
9
11
|
// GET /health — Health check
|
|
@@ -13,6 +15,7 @@
|
|
|
13
15
|
// POST /stream — SSE streaming agent execution
|
|
14
16
|
// GET /metrics — Tool execution metrics
|
|
15
17
|
import { createServer } from 'node:http';
|
|
18
|
+
import { createServer as createHttpsServer } from 'node:https';
|
|
16
19
|
import { createRequire } from 'node:module';
|
|
17
20
|
import { registerAllTools, getAllTools, executeTool, getToolDefinitionsForApi, getToolMetrics } from './tools/index.js';
|
|
18
21
|
import { extractMcpAppFromText, renderMcpApp, listAppCapableTools } from './mcp-apps.js';
|
|
@@ -22,6 +25,10 @@ import { runAgent } from './agent.js';
|
|
|
22
25
|
import { destroySession } from './memory.js';
|
|
23
26
|
import { randomUUID } from 'node:crypto';
|
|
24
27
|
import { mountA2ARoutes } from './a2a.js';
|
|
28
|
+
import { execSync } from 'node:child_process';
|
|
29
|
+
import { existsSync, readFileSync, mkdirSync } from 'node:fs';
|
|
30
|
+
import { join } from 'node:path';
|
|
31
|
+
import { homedir } from 'node:os';
|
|
25
32
|
const __require = createRequire(import.meta.url);
|
|
26
33
|
const VERSION = __require('../package.json').version;
|
|
27
34
|
function cors(res) {
|
|
@@ -42,13 +49,42 @@ function readBody(req) {
|
|
|
42
49
|
req.on('error', reject);
|
|
43
50
|
});
|
|
44
51
|
}
|
|
52
|
+
/** Generate or load a self-signed TLS certificate for localhost */
|
|
53
|
+
function ensureSelfSignedCert() {
|
|
54
|
+
const certDir = join(homedir(), '.kbot', 'certs');
|
|
55
|
+
const certPath = join(certDir, 'localhost.crt');
|
|
56
|
+
const keyPath = join(certDir, 'localhost.key');
|
|
57
|
+
if (existsSync(certPath) && existsSync(keyPath)) {
|
|
58
|
+
return { cert: readFileSync(certPath, 'utf-8'), key: readFileSync(keyPath, 'utf-8') };
|
|
59
|
+
}
|
|
60
|
+
mkdirSync(certDir, { recursive: true });
|
|
61
|
+
printInfo('Generating self-signed TLS certificate for localhost...');
|
|
62
|
+
execSync(`openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 ` +
|
|
63
|
+
`-nodes -days 365 -subj "/CN=localhost" ` +
|
|
64
|
+
`-addext "subjectAltName=DNS:localhost,IP:127.0.0.1" ` +
|
|
65
|
+
`-keyout "${keyPath}" -out "${certPath}"`, { stdio: 'pipe' });
|
|
66
|
+
printSuccess('Certificate generated at ~/.kbot/certs/');
|
|
67
|
+
return { cert: readFileSync(certPath, 'utf-8'), key: readFileSync(keyPath, 'utf-8') };
|
|
68
|
+
}
|
|
45
69
|
export async function startServe(options) {
|
|
46
70
|
// Register all tools before starting
|
|
47
71
|
printInfo('Registering tools...');
|
|
48
72
|
await registerAllTools({ computerUse: options.computerUse });
|
|
49
73
|
const tools = getAllTools();
|
|
50
74
|
printSuccess(`${tools.length} tools registered`);
|
|
51
|
-
|
|
75
|
+
// Determine TLS config
|
|
76
|
+
const useTls = !!(options.https || options.cert || options.key);
|
|
77
|
+
let tlsOpts;
|
|
78
|
+
if (useTls) {
|
|
79
|
+
if (options.cert && options.key) {
|
|
80
|
+
tlsOpts = { cert: readFileSync(options.cert, 'utf-8'), key: readFileSync(options.key, 'utf-8') };
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
tlsOpts = ensureSelfSignedCert();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
const protocol = useTls ? 'https' : 'http';
|
|
87
|
+
const handler = async (req, res) => {
|
|
52
88
|
// CORS preflight
|
|
53
89
|
if (req.method === 'OPTIONS') {
|
|
54
90
|
cors(res);
|
|
@@ -66,7 +102,7 @@ export async function startServe(options) {
|
|
|
66
102
|
return;
|
|
67
103
|
}
|
|
68
104
|
}
|
|
69
|
-
const url = new URL(req.url || '/',
|
|
105
|
+
const url = new URL(req.url || '/', `${protocol}://localhost:${options.port}`);
|
|
70
106
|
const path = url.pathname;
|
|
71
107
|
try {
|
|
72
108
|
// GET /health
|
|
@@ -218,15 +254,23 @@ export async function startServe(options) {
|
|
|
218
254
|
catch (err) {
|
|
219
255
|
json(res, 500, { error: err instanceof Error ? err.message : 'Internal error' });
|
|
220
256
|
}
|
|
221
|
-
}
|
|
257
|
+
};
|
|
258
|
+
const server = useTls
|
|
259
|
+
? createHttpsServer(tlsOpts, handler)
|
|
260
|
+
: createServer(handler);
|
|
222
261
|
// Mount A2A protocol routes (Agent Card + task endpoints)
|
|
262
|
+
// Cast needed: https.Server and http.Server share the same request event API
|
|
223
263
|
mountA2ARoutes(server, {
|
|
224
264
|
port: options.port,
|
|
225
|
-
endpointUrl:
|
|
265
|
+
endpointUrl: `${protocol}://localhost:${options.port}`,
|
|
226
266
|
token: options.token,
|
|
227
267
|
});
|
|
268
|
+
const baseUrl = `${protocol}://localhost:${options.port}`;
|
|
228
269
|
server.listen(options.port, () => {
|
|
229
|
-
printSuccess(`kbot serve running on
|
|
270
|
+
printSuccess(`kbot serve running on ${baseUrl}`);
|
|
271
|
+
if (useTls) {
|
|
272
|
+
printInfo(' TLS: self-signed cert (browsers will warn — safe for local connectors)');
|
|
273
|
+
}
|
|
230
274
|
printInfo(` GET /health — Health check`);
|
|
231
275
|
printInfo(` GET /tools — List ${tools.length} tools`);
|
|
232
276
|
printInfo(` POST /execute — Execute a tool`);
|
|
@@ -243,7 +287,7 @@ export async function startServe(options) {
|
|
|
243
287
|
}
|
|
244
288
|
printInfo('');
|
|
245
289
|
printInfo('Connect from kernel.chat:');
|
|
246
|
-
printInfo(` connectKbot('
|
|
290
|
+
printInfo(` connectKbot('${baseUrl}')`);
|
|
247
291
|
});
|
|
248
292
|
// Graceful shutdown
|
|
249
293
|
const shutdown = () => {
|
|
@@ -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
|