@kernel.chat/kbot 3.71.0 → 3.73.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.
@@ -0,0 +1,2 @@
1
+ export declare function registerMobileAutomationTools(): void;
2
+ //# sourceMappingURL=mobile-automation.d.ts.map
@@ -0,0 +1,612 @@
1
+ // kbot Mobile Automation — iOS/Android device control via mobile-mcp
2
+ //
3
+ // Uses mobile-mcp (https://github.com/mobile-next/mobile-mcp) for native
4
+ // accessibility-tree-based automation. Connects to real iOS devices via USB
5
+ // or iOS/Android simulators.
6
+ //
7
+ // Key advantage over screenshot-based automation: deterministic element
8
+ // targeting via the native accessibility tree. Every UI element has a label,
9
+ // type, position, and identifier — no computer vision needed.
10
+ //
11
+ // Prerequisites:
12
+ // - Node.js >= 22
13
+ // - iOS device connected via USB and trusted, OR iOS simulator running
14
+ // - Xcode command-line tools installed (for iOS physical devices)
15
+ //
16
+ // The mobile-mcp package (@mobilenext/mobile-mcp) is auto-installed via npx
17
+ // on first use. No manual setup required.
18
+ import { registerTool } from './index.js';
19
+ import { MobileMCPClient } from '../integrations/mobile-mcp-client.js';
20
+ import { writeFileSync, existsSync, mkdirSync } from 'node:fs';
21
+ import { dirname, resolve } from 'node:path';
22
+ // ── Helpers ────────────────────────────────────────────────────────────
23
+ function getClient() {
24
+ return MobileMCPClient.getInstance();
25
+ }
26
+ function ensureConnected() {
27
+ const client = getClient();
28
+ if (!client.isConnected) {
29
+ return 'Error: Not connected to a mobile device. Use mobile_connect first.';
30
+ }
31
+ if (!client.currentDeviceId) {
32
+ return 'Error: No device selected. Use mobile_connect to connect to a device.';
33
+ }
34
+ return null;
35
+ }
36
+ // ── Tool Registration ──────────────────────────────────────────────────
37
+ export function registerMobileAutomationTools() {
38
+ // ── mobile_connect ─────────────────────────────────────────────────
39
+ registerTool({
40
+ name: 'mobile_connect',
41
+ description: 'Connect to an iOS or Android device for mobile automation. ' +
42
+ 'Starts the mobile-mcp server (auto-installs @mobilenext/mobile-mcp if needed), ' +
43
+ 'discovers connected devices, and selects one. Returns: device name, platform, ' +
44
+ 'version, screen size. For iOS physical devices, connect via USB and trust the computer first.',
45
+ parameters: {
46
+ device_id: {
47
+ type: 'string',
48
+ description: 'Specific device ID to connect to (optional). If omitted, connects to the first available device. ' +
49
+ 'Use mobile_connect with no args to see all devices, then reconnect with a specific ID.',
50
+ },
51
+ platform: {
52
+ type: 'string',
53
+ description: 'Filter by platform: "ios" or "android" (optional)',
54
+ },
55
+ },
56
+ tier: 'free',
57
+ timeout: 120_000, // npx install can take time on first run
58
+ async execute(args) {
59
+ const client = getClient();
60
+ try {
61
+ // Start the MCP server if not running
62
+ if (!client.isConnected) {
63
+ await client.start();
64
+ }
65
+ // List available devices
66
+ const devices = await client.listDevices();
67
+ if (devices.length === 0) {
68
+ client.stop();
69
+ return ('Error: No devices found.\n\n' +
70
+ 'For iOS physical devices:\n' +
71
+ ' 1. Connect your iPhone/iPad via USB\n' +
72
+ ' 2. Unlock the device and tap "Trust This Computer"\n' +
73
+ ' 3. Ensure Xcode command-line tools are installed: xcode-select --install\n\n' +
74
+ 'For iOS simulators:\n' +
75
+ ' 1. Open Xcode > Window > Devices and Simulators\n' +
76
+ ' 2. Boot a simulator, or: xcrun simctl boot "iPhone 16"\n\n' +
77
+ 'For Android:\n' +
78
+ ' 1. Enable USB debugging on the device\n' +
79
+ ' 2. Connect via USB and accept the debugging prompt\n' +
80
+ ' 3. Verify with: adb devices');
81
+ }
82
+ // Filter by platform if specified
83
+ let candidates = devices;
84
+ if (args.platform) {
85
+ const plat = String(args.platform).toLowerCase();
86
+ candidates = devices.filter(d => d.platform === plat);
87
+ if (candidates.length === 0) {
88
+ return (`No ${plat} devices found. Available devices:\n` +
89
+ devices.map(d => ` ${d.id} — ${d.name} (${d.platform} ${d.version}, ${d.type}, ${d.state})`).join('\n'));
90
+ }
91
+ }
92
+ // Select device
93
+ let selected = candidates[0];
94
+ if (args.device_id) {
95
+ const targetId = String(args.device_id);
96
+ const match = candidates.find(d => d.id === targetId);
97
+ if (!match) {
98
+ return (`Device "${targetId}" not found. Available devices:\n` +
99
+ candidates.map(d => ` ${d.id} — ${d.name} (${d.platform} ${d.version}, ${d.type}, ${d.state})`).join('\n'));
100
+ }
101
+ selected = match;
102
+ }
103
+ else {
104
+ // Prefer online devices, then real devices over simulators
105
+ const online = candidates.filter(d => d.state === 'online');
106
+ if (online.length > 0) {
107
+ const real = online.filter(d => d.type === 'real');
108
+ selected = real.length > 0 ? real[0] : online[0];
109
+ }
110
+ }
111
+ client.setActiveDevice(selected.id);
112
+ // Get screen size
113
+ let screenInfo = '';
114
+ try {
115
+ screenInfo = await client.getScreenSize(selected.id);
116
+ }
117
+ catch {
118
+ screenInfo = 'Screen size: unknown';
119
+ }
120
+ const lines = [
121
+ 'Connected to mobile device.',
122
+ ` Device: ${selected.name}`,
123
+ ` ID: ${selected.id}`,
124
+ ` Platform: ${selected.platform}`,
125
+ ` Type: ${selected.type}`,
126
+ ` Version: ${selected.version}`,
127
+ ` State: ${selected.state}`,
128
+ ` ${screenInfo}`,
129
+ ];
130
+ // Show other available devices if multiple
131
+ if (candidates.length > 1) {
132
+ lines.push('');
133
+ lines.push(`Other available devices (${candidates.length - 1}):`);
134
+ for (const d of candidates) {
135
+ if (d.id !== selected.id) {
136
+ lines.push(` ${d.id} — ${d.name} (${d.platform} ${d.version}, ${d.type})`);
137
+ }
138
+ }
139
+ lines.push('Use mobile_connect with device_id to switch.');
140
+ }
141
+ return lines.join('\n');
142
+ }
143
+ catch (err) {
144
+ return `Error connecting to mobile device: ${err instanceof Error ? err.message : String(err)}`;
145
+ }
146
+ },
147
+ });
148
+ // ── mobile_screenshot ──────────────────────────────────────────────
149
+ registerTool({
150
+ name: 'mobile_screenshot',
151
+ description: 'Capture the current screen of the connected mobile device. ' +
152
+ 'Saves the screenshot to the specified file path (PNG or JPEG). ' +
153
+ 'Returns the file path on success.',
154
+ parameters: {
155
+ save_to: {
156
+ type: 'string',
157
+ description: 'File path to save the screenshot (e.g., "/tmp/screen.png"). Must end in .png, .jpg, or .jpeg.',
158
+ required: true,
159
+ },
160
+ },
161
+ tier: 'free',
162
+ async execute(args) {
163
+ const err = ensureConnected();
164
+ if (err)
165
+ return err;
166
+ const saveTo = String(args.save_to);
167
+ if (!/\.(png|jpe?g)$/i.test(saveTo)) {
168
+ return 'Error: save_to must end in .png, .jpg, or .jpeg';
169
+ }
170
+ // Ensure directory exists
171
+ const dir = dirname(resolve(saveTo));
172
+ if (!existsSync(dir)) {
173
+ try {
174
+ mkdirSync(dir, { recursive: true });
175
+ }
176
+ catch {
177
+ return `Error: Cannot create directory ${dir}`;
178
+ }
179
+ }
180
+ try {
181
+ const client = getClient();
182
+ const result = await client.saveScreenshot(saveTo);
183
+ return result;
184
+ }
185
+ catch (err) {
186
+ // Fallback: try taking screenshot and saving manually
187
+ try {
188
+ const client = getClient();
189
+ const screenshotResult = await client.takeScreenshot();
190
+ if (typeof screenshotResult === 'object' && 'data' in screenshotResult) {
191
+ const buffer = Buffer.from(screenshotResult.data, 'base64');
192
+ writeFileSync(saveTo, buffer);
193
+ return `Screenshot saved to: ${saveTo} (${Math.round(buffer.length / 1024)}KB)`;
194
+ }
195
+ return `Error: ${typeof screenshotResult === 'string' ? screenshotResult : 'Could not capture screenshot'}`;
196
+ }
197
+ catch (innerErr) {
198
+ return `Error capturing screenshot: ${innerErr instanceof Error ? innerErr.message : String(innerErr)}`;
199
+ }
200
+ }
201
+ },
202
+ });
203
+ // ── mobile_tap ─────────────────────────────────────────────────────
204
+ registerTool({
205
+ name: 'mobile_tap',
206
+ description: 'Tap on the connected mobile device screen. Can tap at exact coordinates (x, y) ' +
207
+ 'or find and tap an element by its accessibility label. When using label, the tool ' +
208
+ 'queries the accessibility tree to find the element and taps its center coordinates.',
209
+ parameters: {
210
+ x: {
211
+ type: 'number',
212
+ description: 'X coordinate to tap (pixels). Use with y. Ignored if label is provided.',
213
+ },
214
+ y: {
215
+ type: 'number',
216
+ description: 'Y coordinate to tap (pixels). Use with x. Ignored if label is provided.',
217
+ },
218
+ label: {
219
+ type: 'string',
220
+ description: 'Accessibility label or identifier of the element to tap. ' +
221
+ 'The tool searches the accessibility tree for a matching element and taps its center. ' +
222
+ 'Preferred over coordinates for reliable automation.',
223
+ },
224
+ double_tap: {
225
+ type: 'boolean',
226
+ description: 'If true, performs a double-tap instead of a single tap (default: false)',
227
+ },
228
+ long_press: {
229
+ type: 'boolean',
230
+ description: 'If true, performs a long press instead of a tap (default: false)',
231
+ },
232
+ long_press_duration: {
233
+ type: 'number',
234
+ description: 'Duration of long press in milliseconds (default: 500, max: 10000). Only used with long_press.',
235
+ },
236
+ },
237
+ tier: 'free',
238
+ async execute(args) {
239
+ const err = ensureConnected();
240
+ if (err)
241
+ return err;
242
+ const client = getClient();
243
+ let tapX;
244
+ let tapY;
245
+ if (args.label) {
246
+ // Find element by accessibility label
247
+ const label = String(args.label).toLowerCase();
248
+ try {
249
+ const elementsText = await client.listElements();
250
+ let elements;
251
+ try {
252
+ elements = JSON.parse(elementsText);
253
+ }
254
+ catch {
255
+ // Elements may be in a non-JSON format; try to find coordinates from text
256
+ return (`Error: Could not parse element list to find "${args.label}". ` +
257
+ 'Use mobile_elements to inspect the screen, then use x/y coordinates.');
258
+ }
259
+ // Search by label, identifier, text, or name (case-insensitive partial match)
260
+ const match = elements.find(e => {
261
+ const fields = [e.label, e.identifier, e.text, e.name, e.value].filter(Boolean);
262
+ return fields.some(f => f.toLowerCase().includes(label));
263
+ });
264
+ if (!match) {
265
+ const available = elements
266
+ .filter(e => e.label || e.text || e.identifier)
267
+ .slice(0, 20)
268
+ .map(e => ` [${e.type}] "${e.label || e.text || e.identifier}" at (${e.x}, ${e.y})`)
269
+ .join('\n');
270
+ return (`Element with label "${args.label}" not found on screen.\n\n` +
271
+ `Visible elements (first 20):\n${available || ' (none with labels)'}\n\n` +
272
+ 'Use mobile_elements for the full list.');
273
+ }
274
+ // Tap center of the element
275
+ tapX = Math.round(match.x + match.width / 2);
276
+ tapY = Math.round(match.y + match.height / 2);
277
+ }
278
+ catch (innerErr) {
279
+ return `Error finding element: ${innerErr instanceof Error ? innerErr.message : String(innerErr)}`;
280
+ }
281
+ }
282
+ else if (args.x !== undefined && args.y !== undefined) {
283
+ tapX = Number(args.x);
284
+ tapY = Number(args.y);
285
+ if (isNaN(tapX) || isNaN(tapY)) {
286
+ return 'Error: x and y must be numbers';
287
+ }
288
+ }
289
+ else {
290
+ return 'Error: Provide either label (accessibility label) or x + y (coordinates)';
291
+ }
292
+ try {
293
+ if (args.long_press) {
294
+ const duration = args.long_press_duration ? Number(args.long_press_duration) : undefined;
295
+ return await client.longPress(tapX, tapY, duration);
296
+ }
297
+ if (args.double_tap) {
298
+ return await client.doubleTap(tapX, tapY);
299
+ }
300
+ return await client.tap(tapX, tapY);
301
+ }
302
+ catch (tapErr) {
303
+ return `Error tapping: ${tapErr instanceof Error ? tapErr.message : String(tapErr)}`;
304
+ }
305
+ },
306
+ });
307
+ // ── mobile_swipe ───────────────────────────────────────────────────
308
+ registerTool({
309
+ name: 'mobile_swipe',
310
+ description: 'Perform a swipe gesture on the connected mobile device. ' +
311
+ 'Specify direction and optionally starting coordinates and distance.',
312
+ parameters: {
313
+ direction: {
314
+ type: 'string',
315
+ description: 'Swipe direction: "up", "down", "left", or "right"',
316
+ required: true,
317
+ },
318
+ x: {
319
+ type: 'number',
320
+ description: 'Starting X coordinate (optional — defaults to screen center)',
321
+ },
322
+ y: {
323
+ type: 'number',
324
+ description: 'Starting Y coordinate (optional — defaults to screen center)',
325
+ },
326
+ distance: {
327
+ type: 'number',
328
+ description: 'Swipe distance in pixels (optional — defaults to reasonable default)',
329
+ },
330
+ },
331
+ tier: 'free',
332
+ async execute(args) {
333
+ const err = ensureConnected();
334
+ if (err)
335
+ return err;
336
+ const direction = String(args.direction).toLowerCase();
337
+ if (!['up', 'down', 'left', 'right'].includes(direction)) {
338
+ return 'Error: direction must be "up", "down", "left", or "right"';
339
+ }
340
+ try {
341
+ const client = getClient();
342
+ return await client.swipe(direction, {
343
+ x: args.x !== undefined ? Number(args.x) : undefined,
344
+ y: args.y !== undefined ? Number(args.y) : undefined,
345
+ distance: args.distance !== undefined ? Number(args.distance) : undefined,
346
+ });
347
+ }
348
+ catch (swipeErr) {
349
+ return `Error swiping: ${swipeErr instanceof Error ? swipeErr.message : String(swipeErr)}`;
350
+ }
351
+ },
352
+ });
353
+ // ── mobile_type ────────────────────────────────────────────────────
354
+ registerTool({
355
+ name: 'mobile_type',
356
+ description: 'Type text into the currently focused input field on the connected mobile device. ' +
357
+ 'The field must already be focused (tap on it first with mobile_tap). ' +
358
+ 'Optionally submit the input (press Enter/Return after typing).',
359
+ parameters: {
360
+ text: {
361
+ type: 'string',
362
+ description: 'The text to type',
363
+ required: true,
364
+ },
365
+ submit: {
366
+ type: 'boolean',
367
+ description: 'If true, press Enter/Return after typing (default: false)',
368
+ },
369
+ },
370
+ tier: 'free',
371
+ async execute(args) {
372
+ const err = ensureConnected();
373
+ if (err)
374
+ return err;
375
+ const text = String(args.text);
376
+ if (!text)
377
+ return 'Error: text is required';
378
+ try {
379
+ const client = getClient();
380
+ return await client.typeText(text, Boolean(args.submit));
381
+ }
382
+ catch (typeErr) {
383
+ return `Error typing: ${typeErr instanceof Error ? typeErr.message : String(typeErr)}`;
384
+ }
385
+ },
386
+ });
387
+ // ── mobile_launch ──────────────────────────────────────────────────
388
+ registerTool({
389
+ name: 'mobile_launch',
390
+ description: 'Launch an app on the connected mobile device by bundle ID or package name. ' +
391
+ 'Examples: "com.apple.mobilesafari" (Safari), "com.apple.Preferences" (Settings), ' +
392
+ '"com.apple.MobileSMS" (Messages). Use mobile_app_list to find bundle IDs.',
393
+ parameters: {
394
+ app: {
395
+ type: 'string',
396
+ description: 'App bundle ID (iOS, e.g., "com.apple.mobilesafari") or ' +
397
+ 'package name (Android, e.g., "com.android.chrome")',
398
+ required: true,
399
+ },
400
+ },
401
+ tier: 'free',
402
+ async execute(args) {
403
+ const err = ensureConnected();
404
+ if (err)
405
+ return err;
406
+ const app = String(args.app);
407
+ if (!app)
408
+ return 'Error: app (bundle ID or package name) is required';
409
+ try {
410
+ const client = getClient();
411
+ return await client.launchApp(app);
412
+ }
413
+ catch (launchErr) {
414
+ return `Error launching app: ${launchErr instanceof Error ? launchErr.message : String(launchErr)}`;
415
+ }
416
+ },
417
+ });
418
+ // ── mobile_elements ────────────────────────────────────────────────
419
+ registerTool({
420
+ name: 'mobile_elements',
421
+ description: 'List all visible UI elements on the connected mobile device screen. ' +
422
+ 'Returns each element\'s accessibility label, type (Button, TextField, StaticText, etc.), ' +
423
+ 'identifier, text content, and screen coordinates. ' +
424
+ 'This is the key advantage over screenshot-based automation — deterministic element targeting. ' +
425
+ 'Use this to find elements before tapping them with mobile_tap.',
426
+ parameters: {
427
+ filter: {
428
+ type: 'string',
429
+ description: 'Optional text filter — only return elements matching this text in their label, text, identifier, or type (case-insensitive)',
430
+ },
431
+ },
432
+ tier: 'free',
433
+ maxResultSize: 100_000, // Element trees can be large
434
+ async execute(args) {
435
+ const err = ensureConnected();
436
+ if (err)
437
+ return err;
438
+ try {
439
+ const client = getClient();
440
+ const elementsText = await client.listElements();
441
+ if (!args.filter)
442
+ return elementsText;
443
+ // Apply filter
444
+ const filter = String(args.filter).toLowerCase();
445
+ try {
446
+ const elements = JSON.parse(elementsText);
447
+ const filtered = elements.filter(e => {
448
+ const fields = [e.type, e.label, e.text, e.identifier, e.name, e.value].filter(Boolean);
449
+ return fields.some(f => f.toLowerCase().includes(filter));
450
+ });
451
+ if (filtered.length === 0) {
452
+ return `No elements matching "${args.filter}" found on screen. Use mobile_elements without filter to see all elements.`;
453
+ }
454
+ return JSON.stringify(filtered, null, 2);
455
+ }
456
+ catch {
457
+ // If not JSON, do text-based filtering
458
+ return elementsText;
459
+ }
460
+ }
461
+ catch (elemErr) {
462
+ return `Error listing elements: ${elemErr instanceof Error ? elemErr.message : String(elemErr)}`;
463
+ }
464
+ },
465
+ });
466
+ // ── mobile_back ────────────────────────────────────────────────────
467
+ registerTool({
468
+ name: 'mobile_back',
469
+ description: 'Press the back button / navigate back on the connected mobile device. ' +
470
+ 'On Android this presses the BACK button. On iOS this performs a back swipe gesture ' +
471
+ '(swipe from left edge to right).',
472
+ parameters: {},
473
+ tier: 'free',
474
+ async execute() {
475
+ const err = ensureConnected();
476
+ if (err)
477
+ return err;
478
+ try {
479
+ const client = getClient();
480
+ // Try BACK button first (works on Android, may work on iOS via mobile-mcp)
481
+ try {
482
+ return await client.pressButton('BACK');
483
+ }
484
+ catch {
485
+ // Fallback for iOS: swipe from left edge to go back
486
+ return await client.swipe('right', { x: 10, y: 400 });
487
+ }
488
+ }
489
+ catch (backErr) {
490
+ return `Error pressing back: ${backErr instanceof Error ? backErr.message : String(backErr)}`;
491
+ }
492
+ },
493
+ });
494
+ // ── mobile_home ────────────────────────────────────────────────────
495
+ registerTool({
496
+ name: 'mobile_home',
497
+ description: 'Press the home button / go to home screen on the connected mobile device. ' +
498
+ 'On Android this presses the HOME button. On iOS this presses HOME ' +
499
+ '(simulates the home gesture on Face ID devices).',
500
+ parameters: {},
501
+ tier: 'free',
502
+ async execute() {
503
+ const err = ensureConnected();
504
+ if (err)
505
+ return err;
506
+ try {
507
+ const client = getClient();
508
+ return await client.pressButton('HOME');
509
+ }
510
+ catch (homeErr) {
511
+ return `Error pressing home: ${homeErr instanceof Error ? homeErr.message : String(homeErr)}`;
512
+ }
513
+ },
514
+ });
515
+ // ── mobile_app_list ────────────────────────────────────────────────
516
+ registerTool({
517
+ name: 'mobile_app_list',
518
+ description: 'List all installed apps on the connected mobile device. ' +
519
+ 'Returns app names and bundle IDs (iOS) or package names (Android). ' +
520
+ 'Use the bundle ID with mobile_launch to open an app.',
521
+ parameters: {},
522
+ tier: 'free',
523
+ maxResultSize: 100_000, // App lists can be long
524
+ async execute() {
525
+ const err = ensureConnected();
526
+ if (err)
527
+ return err;
528
+ try {
529
+ const client = getClient();
530
+ return await client.listApps();
531
+ }
532
+ catch (listErr) {
533
+ return `Error listing apps: ${listErr instanceof Error ? listErr.message : String(listErr)}`;
534
+ }
535
+ },
536
+ });
537
+ // ── mobile_open_url ────────────────────────────────────────────────
538
+ registerTool({
539
+ name: 'mobile_open_url',
540
+ description: 'Open a URL in the default browser on the connected mobile device. ' +
541
+ 'The URL must start with http:// or https://.',
542
+ parameters: {
543
+ url: {
544
+ type: 'string',
545
+ description: 'The URL to open (must start with http:// or https://)',
546
+ required: true,
547
+ },
548
+ },
549
+ tier: 'free',
550
+ async execute(args) {
551
+ const err = ensureConnected();
552
+ if (err)
553
+ return err;
554
+ const url = String(args.url);
555
+ if (!/^https?:\/\//i.test(url)) {
556
+ return 'Error: URL must start with http:// or https://';
557
+ }
558
+ try {
559
+ const client = getClient();
560
+ return await client.openUrl(url);
561
+ }
562
+ catch (urlErr) {
563
+ return `Error opening URL: ${urlErr instanceof Error ? urlErr.message : String(urlErr)}`;
564
+ }
565
+ },
566
+ });
567
+ // ── mobile_disconnect ──────────────────────────────────────────────
568
+ registerTool({
569
+ name: 'mobile_disconnect',
570
+ description: 'Disconnect from the mobile device and shut down the mobile-mcp server. ' +
571
+ 'Call when done with mobile automation to free resources.',
572
+ parameters: {},
573
+ tier: 'free',
574
+ async execute() {
575
+ const client = getClient();
576
+ if (!client.isConnected) {
577
+ return 'Not connected to any mobile device.';
578
+ }
579
+ client.stop();
580
+ return 'Disconnected from mobile device. mobile-mcp server stopped.';
581
+ },
582
+ });
583
+ // ── mobile_terminate_app ───────────────────────────────────────────
584
+ registerTool({
585
+ name: 'mobile_terminate_app',
586
+ description: 'Force-stop a running app on the connected mobile device.',
587
+ parameters: {
588
+ app: {
589
+ type: 'string',
590
+ description: 'App bundle ID (iOS) or package name (Android) to terminate',
591
+ required: true,
592
+ },
593
+ },
594
+ tier: 'free',
595
+ async execute(args) {
596
+ const err = ensureConnected();
597
+ if (err)
598
+ return err;
599
+ const app = String(args.app);
600
+ if (!app)
601
+ return 'Error: app (bundle ID or package name) is required';
602
+ try {
603
+ const client = getClient();
604
+ return await client.terminateApp(app);
605
+ }
606
+ catch (termErr) {
607
+ return `Error terminating app: ${termErr instanceof Error ? termErr.message : String(termErr)}`;
608
+ }
609
+ },
610
+ });
611
+ }
612
+ //# sourceMappingURL=mobile-automation.js.map
@@ -100,6 +100,33 @@ const PRESETS = {
100
100
  filter: { cutoff: 0.28, resonance: 8, drive: 20 },
101
101
  character: 'dark',
102
102
  },
103
+ 'ethereal-dream': {
104
+ name: 'kbot - Ethereal Dream',
105
+ type: 'pad',
106
+ oscA: { unison: 8, detune: 0.20, width: 100 },
107
+ oscB: { enabled: true, volume: -3, detune: 0.15 },
108
+ env: { attack: 2.0, decay: 0.5, sustain: -0.5, release: 5.0 },
109
+ filter: { cutoff: 0.55, resonance: 3, drive: 2 },
110
+ character: 'dreamy',
111
+ },
112
+ 'midnight-aurora': {
113
+ name: 'kbot - Midnight Aurora',
114
+ type: 'pad',
115
+ oscA: { unison: 12, detune: 0.25, width: 100 },
116
+ oscB: { enabled: true, volume: -2, detune: 0.20 },
117
+ env: { attack: 3.0, decay: 0.3, sustain: -0.2, release: 6.0 },
118
+ filter: { cutoff: 0.45, resonance: 5, drive: 4 },
119
+ character: 'lush',
120
+ },
121
+ 'cloud-cathedral': {
122
+ name: 'kbot - Cloud Cathedral',
123
+ type: 'pad',
124
+ oscA: { unison: 16, detune: 0.30, width: 100 },
125
+ oscB: { enabled: true, volume: -1, detune: 0.22 },
126
+ env: { attack: 4.0, decay: 0.2, sustain: -0.1, release: 8.0 },
127
+ filter: { cutoff: 0.38, resonance: 6, drive: 1 },
128
+ character: 'dreamy',
129
+ },
103
130
  };
104
131
  export function registerSerum2PresetTools() {
105
132
  registerTool({
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@kernel.chat/kbot",
3
- "version": "3.71.0",
4
- "description": "Open-source terminal AI agent. 739+ tools, 35 agents, 20 providers. Dreams, learns, watches your system. Fully local, fully sovereign. MIT.",
3
+ "version": "3.73.0",
4
+ "description": "Open-source terminal AI agent. 764+ tools, 35 agents, 20 providers. Dreams, learns, watches your system. Controls your phone. Fully local, fully sovereign. MIT.",
5
5
  "type": "module",
6
6
  "repository": {
7
7
  "type": "git",