@kernel.chat/kbot 3.99.31 → 3.99.33

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.
Files changed (58) hide show
  1. package/README.md +1 -1
  2. package/dist/agents/security-agent.d.ts +31 -0
  3. package/dist/agents/security-agent.js +180 -0
  4. package/dist/agents/security-rules.d.ts +35 -0
  5. package/dist/agents/security-rules.js +206 -0
  6. package/dist/agents/specialists.d.ts +6 -0
  7. package/dist/agents/specialists.js +45 -0
  8. package/dist/architect.js +5 -0
  9. package/dist/auth.js +1 -1
  10. package/dist/channels/matrix.d.ts +4 -0
  11. package/dist/channels/matrix.js +28 -0
  12. package/dist/channels/office.d.ts +78 -0
  13. package/dist/channels/office.js +169 -0
  14. package/dist/channels/registry.d.ts +8 -0
  15. package/dist/channels/registry.js +38 -0
  16. package/dist/channels/signal.d.ts +4 -0
  17. package/dist/channels/signal.js +29 -0
  18. package/dist/channels/slack.d.ts +4 -0
  19. package/dist/channels/slack.js +97 -0
  20. package/dist/channels/teams.d.ts +4 -0
  21. package/dist/channels/teams.js +29 -0
  22. package/dist/channels/telegram.d.ts +4 -0
  23. package/dist/channels/telegram.js +28 -0
  24. package/dist/channels/types.d.ts +50 -0
  25. package/dist/channels/types.js +13 -0
  26. package/dist/channels/whatsapp.d.ts +4 -0
  27. package/dist/channels/whatsapp.js +28 -0
  28. package/dist/computer-use-coordinator.d.ts +44 -0
  29. package/dist/computer-use-coordinator.js +0 -0
  30. package/dist/file-library.d.ts +76 -0
  31. package/dist/file-library.js +269 -0
  32. package/dist/managed-agents-anthropic.d.ts +90 -0
  33. package/dist/managed-agents-anthropic.js +123 -0
  34. package/dist/plugins-integrity.d.ts +72 -0
  35. package/dist/plugins-integrity.js +153 -0
  36. package/dist/plugins.d.ts +13 -2
  37. package/dist/plugins.js +87 -10
  38. package/dist/tools/anthropic-managed-agents-tools.d.ts +22 -0
  39. package/dist/tools/anthropic-managed-agents-tools.js +191 -0
  40. package/dist/tools/channel-tools.d.ts +4 -0
  41. package/dist/tools/channel-tools.js +80 -0
  42. package/dist/tools/computer-coordinator-tools.d.ts +13 -0
  43. package/dist/tools/computer-coordinator-tools.js +104 -0
  44. package/dist/tools/computer.js +463 -299
  45. package/dist/tools/file-library-tools.d.ts +12 -0
  46. package/dist/tools/file-library-tools.js +191 -0
  47. package/dist/tools/image-thoughtful.d.ts +31 -0
  48. package/dist/tools/image-thoughtful.js +233 -0
  49. package/dist/tools/index.js +1 -0
  50. package/dist/tools/security-agent-tools.d.ts +34 -0
  51. package/dist/tools/security-agent-tools.js +30 -0
  52. package/dist/tools/swarm-2026-04.d.ts +2 -0
  53. package/dist/tools/swarm-2026-04.js +91 -0
  54. package/dist/tools/workspace-agent-tools.d.ts +19 -0
  55. package/dist/tools/workspace-agent-tools.js +191 -0
  56. package/dist/workspace-agents.d.ts +132 -0
  57. package/dist/workspace-agents.js +379 -0
  58. package/package.json +1 -1
@@ -3,66 +3,111 @@
3
3
  // Capabilities: screenshot, click, type, scroll, drag, key combos,
4
4
  // app launch/focus, window management (list/resize/move/minimize)
5
5
  //
6
- // Safety: per-app session approval, machine-wide lock file,
7
- // terminal excluded from screenshots, permission check flow
6
+ // Safety: per-app sub-locks via the Coordinator (parallel multi-agent),
7
+ // per-app session approval, terminal excluded from screenshots,
8
+ // permission check flow.
8
9
  //
9
10
  // Requires explicit opt-in via --computer-use flag.
10
11
  // macOS: AppleScript + screencapture + cliclick fallback
11
12
  // Linux: xdotool + import/gnome-screenshot
12
13
  import { execSync } from 'node:child_process';
14
+ import { randomUUID } from 'node:crypto';
13
15
  import { tmpdir, homedir } from 'node:os';
14
16
  import { join } from 'node:path';
15
- import { readFileSync, writeFileSync, unlinkSync, existsSync, mkdirSync, rmSync } from 'node:fs';
17
+ import { readFileSync, unlinkSync, existsSync, mkdirSync, rmSync } from 'node:fs';
16
18
  import { registerTool } from './index.js';
19
+ import { Coordinator } from '../computer-use-coordinator.js';
17
20
  const platform = process.platform;
18
21
  const LOCK_DIR = join(homedir(), '.kbot');
22
+ // Legacy single-session lock path. Retained as a constant for back-compat
23
+ // with any callers that referenced it; the actual locking is now performed
24
+ // per-app via the Coordinator.
19
25
  const LOCK_FILE = join(LOCK_DIR, 'computer-use.lock');
20
26
  // ── Session state ──────────────────────────────────────────────────
21
27
  /** Apps approved for this session */
22
28
  const approvedApps = new Set();
23
29
  /** Whether permissions have been verified this session */
24
30
  let permissionsVerified = false;
25
- /** Current lock holder PID */
31
+ /** Legacy single-session "lock held" flag — kept so computer_check / screen_info
32
+ * can still report something meaningful. The real locking is the Coordinator. */
26
33
  let lockHeld = false;
27
- // ── Lock file (one session at a time) ──────────────────────────────
34
+ // ── Coordinator (per-app sub-locks) ────────────────────────────────
35
+ /** Stable agent id for this kbot process. Override with KBOT_COMPUTER_USE_AGENT_ID
36
+ * for deterministic tests / multi-process coordination. */
37
+ const AGENT_ID = process.env.KBOT_COMPUTER_USE_AGENT_ID || randomUUID();
38
+ /** Module-scoped coordinator. Lock files live on disk under
39
+ * ~/.kbot/computer-use/<app>.lock, so even if other processes have their own
40
+ * Coordinator instance, they'll see each other's locks. */
41
+ const coordinator = new Coordinator();
42
+ /** Format a Coordinator denial as a single error string. */
43
+ function formatDenied(app, heldBy, since) {
44
+ const sinceStr = since ? new Date(since).toISOString() : 'unknown';
45
+ return `computer_use: app '${app}' is held by ${heldBy ?? 'unknown'} since ${sinceStr} — wait or unregister that agent.`;
46
+ }
47
+ /** Acquire a per-app claim. Returns an error string on denial, or null on success.
48
+ * When `app` is undefined (legacy callers that didn't specify an app), falls
49
+ * back to the single-session legacy behaviour: just mark `lockHeld = true` and
50
+ * let any prior global lock file get cleaned up.
51
+ *
52
+ * App names are normalised to lowercase for the Coordinator so that
53
+ * `Ableton` and `ableton` collide on the same lock file. */
54
+ function claimApp(app) {
55
+ if (!app) {
56
+ // Legacy single-lock fallback — don't break existing scripts that call a
57
+ // tool without specifying an app. We still set lockHeld so screen_info /
58
+ // computer_check report sane state.
59
+ if (!existsSync(LOCK_DIR))
60
+ mkdirSync(LOCK_DIR, { recursive: true });
61
+ lockHeld = true;
62
+ return null;
63
+ }
64
+ const key = app.toLowerCase();
65
+ // Make sure this agent is registered for the app before claiming.
66
+ if (!approvedApps.has(key)) {
67
+ return `Error: ${app} is not approved. Call app_approve first.`;
68
+ }
69
+ // Re-register so the coordinator knows about the (possibly new) app.
70
+ coordinator.register(AGENT_ID, { apps: [...approvedApps] });
71
+ const result = coordinator.claim(AGENT_ID, key);
72
+ if (!result.granted) {
73
+ return formatDenied(app, result.heldBy, result.since);
74
+ }
75
+ lockHeld = true;
76
+ return null;
77
+ }
78
+ /** Release a per-app claim. Safe to call when nothing was claimed. */
79
+ function releaseApp(app) {
80
+ if (!app)
81
+ return;
82
+ try {
83
+ coordinator.release(AGENT_ID, app.toLowerCase());
84
+ }
85
+ catch { /* best effort */ }
86
+ }
87
+ // Clean up any locks held by this process on exit.
88
+ const cleanupOnExit = () => {
89
+ try {
90
+ coordinator.unregister(AGENT_ID);
91
+ }
92
+ catch { /* best effort */ }
93
+ try {
94
+ if (existsSync(LOCK_FILE))
95
+ rmSync(LOCK_FILE);
96
+ }
97
+ catch { /* best effort */ }
98
+ };
99
+ process.on('exit', cleanupOnExit);
100
+ process.on('SIGINT', cleanupOnExit);
101
+ process.on('SIGTERM', cleanupOnExit);
102
+ // ── Legacy lock helpers (kept as no-op stubs for back-compat) ──────
103
+ /** @deprecated kept for back-compat — Coordinator handles locking now. */
28
104
  function acquireLock() {
29
105
  if (!existsSync(LOCK_DIR))
30
106
  mkdirSync(LOCK_DIR, { recursive: true });
31
- if (existsSync(LOCK_FILE)) {
32
- try {
33
- const lock = JSON.parse(readFileSync(LOCK_FILE, 'utf-8'));
34
- // Check if the holding process is still alive
35
- try {
36
- process.kill(lock.pid, 0); // signal 0 = existence check
37
- return `Computer use is held by another kbot session (PID ${lock.pid}, started ${lock.started}). Finish that session first.`;
38
- }
39
- catch {
40
- // Process is dead — stale lock, clean it up
41
- rmSync(LOCK_FILE);
42
- }
43
- }
44
- catch {
45
- rmSync(LOCK_FILE);
46
- }
47
- }
48
- writeFileSync(LOCK_FILE, JSON.stringify({
49
- pid: process.pid,
50
- started: new Date().toISOString(),
51
- }));
52
107
  lockHeld = true;
53
- // Clean up on exit
54
- const cleanup = () => {
55
- try {
56
- if (existsSync(LOCK_FILE))
57
- rmSync(LOCK_FILE);
58
- }
59
- catch { /* best effort */ }
60
- };
61
- process.on('exit', cleanup);
62
- process.on('SIGINT', cleanup);
63
- process.on('SIGTERM', cleanup);
64
108
  return null;
65
109
  }
110
+ /** @deprecated kept for back-compat — Coordinator handles locking now. */
66
111
  function releaseLock() {
67
112
  if (lockHeld) {
68
113
  try {
@@ -141,11 +186,12 @@ function ensurePermissions() {
141
186
  permissionsVerified = true;
142
187
  return null;
143
188
  }
144
- /** Ensure lock is acquired */
189
+ /** Ensure base lock dir exists. Per-app claims are handled by claimApp(). */
145
190
  function ensureLock() {
146
- if (lockHeld)
147
- return null;
148
- return acquireLock();
191
+ if (!existsSync(LOCK_DIR))
192
+ mkdirSync(LOCK_DIR, { recursive: true });
193
+ lockHeld = true;
194
+ return null;
149
195
  }
150
196
  // ── App approval system ────────────────────────────────────────────
151
197
  /** Apps with elevated access warnings */
@@ -218,11 +264,13 @@ export function registerComputerTools() {
218
264
  if (permErr)
219
265
  return permErr;
220
266
  const approvedList = getApprovedApps();
267
+ const coordStatus = coordinator.status();
221
268
  return [
222
269
  'Computer use ready.',
223
270
  `Platform: ${platform}`,
224
- `Lock: held (PID ${process.pid})`,
271
+ `Agent ID: ${AGENT_ID}`,
225
272
  `Approved apps: ${approvedList.length > 0 ? approvedList.join(', ') : 'none yet (use app_approve to approve apps)'}`,
273
+ `Coordinator: ${JSON.stringify(coordStatus)}`,
226
274
  ].join('\n');
227
275
  },
228
276
  });
@@ -244,6 +292,14 @@ export function registerComputerTools() {
244
292
  result += `Warning: ${app} — ${warning}\n`;
245
293
  }
246
294
  approveApp(app);
295
+ // Re-register with the coordinator so it knows this agent intends to
296
+ // drive the newly-approved app.
297
+ try {
298
+ coordinator.register(AGENT_ID, { apps: [...approvedApps] });
299
+ }
300
+ catch (err) {
301
+ return `${result}Approved ${app} but coordinator registration failed: ${err instanceof Error ? err.message : String(err)}`;
302
+ }
247
303
  result += `Approved ${app} for this session.`;
248
304
  return result;
249
305
  },
@@ -273,29 +329,37 @@ export function registerComputerTools() {
273
329
  if (!isAppApproved(app)) {
274
330
  return `Error: ${app} is not approved. Call app_approve first.`;
275
331
  }
276
- if (platform === 'darwin') {
277
- try {
278
- osascript(`tell application "${escapeAppleScript(app)}" to activate`);
279
- // Wait a beat for the app to come forward
280
- await new Promise(r => setTimeout(r, 500));
281
- return `Launched/focused: ${app}`;
332
+ const claimErr = claimApp(app);
333
+ if (claimErr)
334
+ return `Error: ${claimErr}`;
335
+ try {
336
+ if (platform === 'darwin') {
337
+ try {
338
+ osascript(`tell application "${escapeAppleScript(app)}" to activate`);
339
+ // Wait a beat for the app to come forward
340
+ await new Promise(r => setTimeout(r, 500));
341
+ return `Launched/focused: ${app}`;
342
+ }
343
+ catch (err) {
344
+ return `Error launching ${app}: ${err instanceof Error ? err.message : String(err)}`;
345
+ }
282
346
  }
283
- catch (err) {
284
- return `Error launching ${app}: ${err instanceof Error ? err.message : String(err)}`;
347
+ else if (platform === 'linux') {
348
+ try {
349
+ execSync(`wmctrl -a "${app}" 2>/dev/null || xdg-open "${app}" 2>/dev/null`, {
350
+ timeout: 10_000, stdio: 'pipe',
351
+ });
352
+ return `Launched/focused: ${app}`;
353
+ }
354
+ catch {
355
+ return `Error: Could not launch ${app}. Ensure it's installed.`;
356
+ }
285
357
  }
358
+ return 'Error: Unsupported platform';
286
359
  }
287
- else if (platform === 'linux') {
288
- try {
289
- execSync(`wmctrl -a "${app}" 2>/dev/null || xdg-open "${app}" 2>/dev/null`, {
290
- timeout: 10_000, stdio: 'pipe',
291
- });
292
- return `Launched/focused: ${app}`;
293
- }
294
- catch {
295
- return `Error: Could not launch ${app}. Ensure it's installed.`;
296
- }
360
+ finally {
361
+ releaseApp(app);
297
362
  }
298
- return 'Error: Unsupported platform';
299
363
  },
300
364
  });
301
365
  // ── Screenshot ──
@@ -305,12 +369,19 @@ export function registerComputerTools() {
305
369
  parameters: {
306
370
  window: { type: 'string', description: 'Window title to capture (optional — captures full screen if omitted)' },
307
371
  region: { type: 'string', description: 'Capture region as "x,y,w,h" (optional)' },
372
+ // Optional `app` enables Coordinator per-app locking. When omitted we
373
+ // fall back to the legacy single-lock path so existing scripts work.
374
+ app: { type: 'string', description: 'App being targeted, for parallel-agent coordination (optional)' },
308
375
  },
309
376
  tier: 'free',
310
377
  async execute(args) {
311
378
  const lockErr = ensureLock();
312
379
  if (lockErr)
313
380
  return `Error: ${lockErr}`;
381
+ const app = args.app ? String(args.app) : undefined;
382
+ const claimErr = claimApp(app);
383
+ if (claimErr)
384
+ return `Error: ${claimErr}`;
314
385
  const tmpPath = join(tmpdir(), `kbot-screenshot-${Date.now()}.png`);
315
386
  try {
316
387
  if (platform === 'darwin') {
@@ -377,6 +448,9 @@ export function registerComputerTools() {
377
448
  catch (err) {
378
449
  return `Screenshot failed: ${err instanceof Error ? err.message : String(err)}`;
379
450
  }
451
+ finally {
452
+ releaseApp(app);
453
+ }
380
454
  },
381
455
  });
382
456
  // ── Mouse click ──
@@ -387,63 +461,77 @@ export function registerComputerTools() {
387
461
  x: { type: 'number', description: 'X coordinate', required: true },
388
462
  y: { type: 'number', description: 'Y coordinate', required: true },
389
463
  button: { type: 'string', description: 'Mouse button: left, right, double (default: left)' },
464
+ // Optional `app` enables per-app coordination so multiple agents can
465
+ // drive different apps in parallel. Omit for legacy single-lock fallback.
466
+ app: { type: 'string', description: 'App being targeted, for parallel-agent coordination (optional)' },
390
467
  },
391
468
  tier: 'free',
392
469
  async execute(args) {
393
470
  const lockErr = ensureLock();
394
471
  if (lockErr)
395
472
  return `Error: ${lockErr}`;
473
+ const app = args.app ? String(args.app) : undefined;
474
+ const claimErr = claimApp(app);
475
+ if (claimErr)
476
+ return `Error: ${claimErr}`;
396
477
  const x = Math.round(Number(args.x));
397
478
  const y = Math.round(Number(args.y));
398
479
  const button = String(args.button || 'left').toLowerCase();
399
- if (isNaN(x) || isNaN(y))
480
+ if (isNaN(x) || isNaN(y)) {
481
+ releaseApp(app);
400
482
  return 'Error: x and y must be numbers';
401
- if (platform === 'darwin') {
402
- try {
403
- if (button === 'double') {
404
- // Double click
405
- try {
406
- execSync(`cliclick dc:${x},${y}`, { timeout: 5_000, stdio: 'pipe' });
407
- }
408
- catch {
409
- osascript(`tell application "System Events" to click at {${x}, ${y}}`);
410
- await new Promise(r => setTimeout(r, 100));
411
- osascript(`tell application "System Events" to click at {${x}, ${y}}`);
483
+ }
484
+ try {
485
+ if (platform === 'darwin') {
486
+ try {
487
+ if (button === 'double') {
488
+ // Double click
489
+ try {
490
+ execSync(`cliclick dc:${x},${y}`, { timeout: 5_000, stdio: 'pipe' });
491
+ }
492
+ catch {
493
+ osascript(`tell application "System Events" to click at {${x}, ${y}}`);
494
+ await new Promise(r => setTimeout(r, 100));
495
+ osascript(`tell application "System Events" to click at {${x}, ${y}}`);
496
+ }
412
497
  }
413
- }
414
- else if (button === 'right') {
415
- try {
416
- execSync(`cliclick rc:${x},${y}`, { timeout: 5_000, stdio: 'pipe' });
498
+ else if (button === 'right') {
499
+ try {
500
+ execSync(`cliclick rc:${x},${y}`, { timeout: 5_000, stdio: 'pipe' });
501
+ }
502
+ catch {
503
+ osascript(`tell application "System Events" to click at {${x}, ${y}} using control down`);
504
+ }
417
505
  }
418
- catch {
419
- osascript(`tell application "System Events" to click at {${x}, ${y}} using control down`);
506
+ else {
507
+ try {
508
+ execSync(`cliclick c:${x},${y}`, { timeout: 5_000, stdio: 'pipe' });
509
+ }
510
+ catch {
511
+ osascript(`tell application "System Events" to click at {${x}, ${y}}`);
512
+ }
420
513
  }
514
+ return `Clicked ${button} at (${x}, ${y})`;
421
515
  }
422
- else {
423
- try {
424
- execSync(`cliclick c:${x},${y}`, { timeout: 5_000, stdio: 'pipe' });
425
- }
426
- catch {
427
- osascript(`tell application "System Events" to click at {${x}, ${y}}`);
428
- }
516
+ catch (err) {
517
+ return `Click failed: ${err instanceof Error ? err.message : String(err)}`;
429
518
  }
430
- return `Clicked ${button} at (${x}, ${y})`;
431
519
  }
432
- catch (err) {
433
- return `Click failed: ${err instanceof Error ? err.message : String(err)}`;
520
+ else if (platform === 'linux') {
521
+ try {
522
+ const btn = button === 'right' ? 3 : button === 'double' ? '--repeat 2 1' : '1';
523
+ execSync(`xdotool mousemove ${x} ${y} click ${btn}`, { timeout: 5_000 });
524
+ return `Clicked ${button} at (${x}, ${y})`;
525
+ }
526
+ catch {
527
+ return 'Error: Click requires xdotool (apt install xdotool)';
528
+ }
434
529
  }
530
+ return 'Error: Unsupported platform';
435
531
  }
436
- else if (platform === 'linux') {
437
- try {
438
- const btn = button === 'right' ? 3 : button === 'double' ? '--repeat 2 1' : '1';
439
- execSync(`xdotool mousemove ${x} ${y} click ${btn}`, { timeout: 5_000 });
440
- return `Clicked ${button} at (${x}, ${y})`;
441
- }
442
- catch {
443
- return 'Error: Click requires xdotool (apt install xdotool)';
444
- }
532
+ finally {
533
+ releaseApp(app);
445
534
  }
446
- return 'Error: Unsupported platform';
447
535
  },
448
536
  });
449
537
  // ── Mouse scroll ──
@@ -455,72 +543,83 @@ export function registerComputerTools() {
455
543
  amount: { type: 'number', description: 'Scroll amount in clicks (default: 3)' },
456
544
  x: { type: 'number', description: 'X coordinate to scroll at (optional — uses current position)' },
457
545
  y: { type: 'number', description: 'Y coordinate to scroll at (optional)' },
546
+ // Optional `app` enables Coordinator per-app locking. Omit for legacy fallback.
547
+ app: { type: 'string', description: 'App being targeted, for parallel-agent coordination (optional)' },
458
548
  },
459
549
  tier: 'free',
460
550
  async execute(args) {
461
551
  const lockErr = ensureLock();
462
552
  if (lockErr)
463
553
  return `Error: ${lockErr}`;
464
- const direction = String(args.direction).toLowerCase();
465
- const amount = Math.round(Number(args.amount) || 3);
466
- if (!['up', 'down', 'left', 'right'].includes(direction)) {
467
- return 'Error: direction must be up, down, left, or right';
468
- }
469
- // Move mouse first if coordinates given
470
- if (args.x !== undefined && args.y !== undefined) {
471
- const x = Math.round(Number(args.x));
472
- const y = Math.round(Number(args.y));
473
- if (platform === 'darwin') {
474
- try {
475
- execSync(`cliclick m:${x},${y}`, { timeout: 3_000, stdio: 'pipe' });
476
- }
477
- catch { /* best effort move */ }
554
+ const app = args.app ? String(args.app) : undefined;
555
+ const claimErr = claimApp(app);
556
+ if (claimErr)
557
+ return `Error: ${claimErr}`;
558
+ try {
559
+ const direction = String(args.direction).toLowerCase();
560
+ const amount = Math.round(Number(args.amount) || 3);
561
+ if (!['up', 'down', 'left', 'right'].includes(direction)) {
562
+ return 'Error: direction must be up, down, left, or right';
478
563
  }
479
- else if (platform === 'linux') {
480
- try {
481
- execSync(`xdotool mousemove ${x} ${y}`, { timeout: 3_000, stdio: 'pipe' });
564
+ // Move mouse first if coordinates given
565
+ if (args.x !== undefined && args.y !== undefined) {
566
+ const x = Math.round(Number(args.x));
567
+ const y = Math.round(Number(args.y));
568
+ if (platform === 'darwin') {
569
+ try {
570
+ execSync(`cliclick m:${x},${y}`, { timeout: 3_000, stdio: 'pipe' });
571
+ }
572
+ catch { /* best effort move */ }
482
573
  }
483
- catch { /* best effort move */ }
484
- }
485
- }
486
- if (platform === 'darwin') {
487
- try {
488
- // cliclick scroll: positive = up, negative = down
489
- const scrollDir = direction === 'up' ? amount : direction === 'down' ? -amount : 0;
490
- if (direction === 'up' || direction === 'down') {
574
+ else if (platform === 'linux') {
491
575
  try {
492
- execSync(`cliclick "ku:${scrollDir > 0 ? `+${scrollDir}` : scrollDir}"`, { timeout: 5_000, stdio: 'pipe' });
576
+ execSync(`xdotool mousemove ${x} ${y}`, { timeout: 3_000, stdio: 'pipe' });
493
577
  }
494
- catch {
495
- // Fallback to AppleScript scroll
496
- const scrollAmount = direction === 'up' ? -amount : amount;
497
- osascript(`tell application "System Events" to scroll area 1 by ${scrollAmount}`);
578
+ catch { /* best effort move */ }
579
+ }
580
+ }
581
+ if (platform === 'darwin') {
582
+ try {
583
+ // cliclick scroll: positive = up, negative = down
584
+ const scrollDir = direction === 'up' ? amount : direction === 'down' ? -amount : 0;
585
+ if (direction === 'up' || direction === 'down') {
586
+ try {
587
+ execSync(`cliclick "ku:${scrollDir > 0 ? `+${scrollDir}` : scrollDir}"`, { timeout: 5_000, stdio: 'pipe' });
588
+ }
589
+ catch {
590
+ // Fallback to AppleScript scroll
591
+ const scrollAmount = direction === 'up' ? -amount : amount;
592
+ osascript(`tell application "System Events" to scroll area 1 by ${scrollAmount}`);
593
+ }
594
+ }
595
+ else {
596
+ // Horizontal scroll via AppleScript
597
+ const horiz = direction === 'left' ? -amount : amount;
598
+ osascript(`tell application "System Events" to scroll area 1 by ${horiz}`);
498
599
  }
600
+ return `Scrolled ${direction} by ${amount}`;
499
601
  }
500
- else {
501
- // Horizontal scroll via AppleScript
502
- const horiz = direction === 'left' ? -amount : amount;
503
- osascript(`tell application "System Events" to scroll area 1 by ${horiz}`);
602
+ catch (err) {
603
+ return `Scroll failed: ${err instanceof Error ? err.message : String(err)}`;
504
604
  }
505
- return `Scrolled ${direction} by ${amount}`;
506
605
  }
507
- catch (err) {
508
- return `Scroll failed: ${err instanceof Error ? err.message : String(err)}`;
606
+ else if (platform === 'linux') {
607
+ try {
608
+ // xdotool: button 4=up, 5=down, 6=left, 7=right
609
+ const buttonMap = { up: 4, down: 5, left: 6, right: 7 };
610
+ const btn = buttonMap[direction];
611
+ execSync(`xdotool click --repeat ${amount} ${btn}`, { timeout: 5_000 });
612
+ return `Scrolled ${direction} by ${amount}`;
613
+ }
614
+ catch {
615
+ return 'Error: Scroll requires xdotool';
616
+ }
509
617
  }
618
+ return 'Error: Unsupported platform';
510
619
  }
511
- else if (platform === 'linux') {
512
- try {
513
- // xdotool: button 4=up, 5=down, 6=left, 7=right
514
- const buttonMap = { up: 4, down: 5, left: 6, right: 7 };
515
- const btn = buttonMap[direction];
516
- execSync(`xdotool click --repeat ${amount} ${btn}`, { timeout: 5_000 });
517
- return `Scrolled ${direction} by ${amount}`;
518
- }
519
- catch {
520
- return 'Error: Scroll requires xdotool';
521
- }
620
+ finally {
621
+ releaseApp(app);
522
622
  }
523
- return 'Error: Unsupported platform';
524
623
  },
525
624
  });
526
625
  // ── Mouse drag ──
@@ -533,48 +632,59 @@ export function registerComputerTools() {
533
632
  to_x: { type: 'number', description: 'End X coordinate', required: true },
534
633
  to_y: { type: 'number', description: 'End Y coordinate', required: true },
535
634
  duration_ms: { type: 'number', description: 'Drag duration in milliseconds (default: 500)' },
635
+ // Optional `app` enables Coordinator per-app locking. Omit for legacy fallback.
636
+ app: { type: 'string', description: 'App being targeted, for parallel-agent coordination (optional)' },
536
637
  },
537
638
  tier: 'free',
538
639
  async execute(args) {
539
640
  const lockErr = ensureLock();
540
641
  if (lockErr)
541
642
  return `Error: ${lockErr}`;
542
- const fx = Math.round(Number(args.from_x));
543
- const fy = Math.round(Number(args.from_y));
544
- const tx = Math.round(Number(args.to_x));
545
- const ty = Math.round(Number(args.to_y));
546
- if ([fx, fy, tx, ty].some(isNaN))
547
- return 'Error: All coordinates must be numbers';
548
- if (platform === 'darwin') {
549
- try {
643
+ const app = args.app ? String(args.app) : undefined;
644
+ const claimErr = claimApp(app);
645
+ if (claimErr)
646
+ return `Error: ${claimErr}`;
647
+ try {
648
+ const fx = Math.round(Number(args.from_x));
649
+ const fy = Math.round(Number(args.from_y));
650
+ const tx = Math.round(Number(args.to_x));
651
+ const ty = Math.round(Number(args.to_y));
652
+ if ([fx, fy, tx, ty].some(isNaN))
653
+ return 'Error: All coordinates must be numbers';
654
+ if (platform === 'darwin') {
550
655
  try {
551
- execSync(`cliclick dd:${fx},${fy} du:${tx},${ty}`, { timeout: 10_000, stdio: 'pipe' });
552
- }
553
- catch {
554
- // Fallback: AppleScript mouse down, move, mouse up
555
- osascript(`
656
+ try {
657
+ execSync(`cliclick dd:${fx},${fy} du:${tx},${ty}`, { timeout: 10_000, stdio: 'pipe' });
658
+ }
659
+ catch {
660
+ // Fallback: AppleScript mouse down, move, mouse up
661
+ osascript(`
556
662
  tell application "System Events"
557
663
  set mouseLocation to {${fx}, ${fy}}
558
664
  click at mouseLocation
559
665
  end tell
560
666
  `.trim(), 10_000);
667
+ }
668
+ return `Dragged from (${fx},${fy}) to (${tx},${ty})`;
669
+ }
670
+ catch (err) {
671
+ return `Drag failed: ${err instanceof Error ? err.message : String(err)}`;
561
672
  }
562
- return `Dragged from (${fx},${fy}) to (${tx},${ty})`;
563
673
  }
564
- catch (err) {
565
- return `Drag failed: ${err instanceof Error ? err.message : String(err)}`;
674
+ else if (platform === 'linux') {
675
+ try {
676
+ execSync(`xdotool mousemove ${fx} ${fy} mousedown 1 mousemove --sync ${tx} ${ty} mouseup 1`, { timeout: 10_000 });
677
+ return `Dragged from (${fx},${fy}) to (${tx},${ty})`;
678
+ }
679
+ catch {
680
+ return 'Error: Drag requires xdotool';
681
+ }
566
682
  }
683
+ return 'Error: Unsupported platform';
567
684
  }
568
- else if (platform === 'linux') {
569
- try {
570
- execSync(`xdotool mousemove ${fx} ${fy} mousedown 1 mousemove --sync ${tx} ${ty} mouseup 1`, { timeout: 10_000 });
571
- return `Dragged from (${fx},${fy}) to (${tx},${ty})`;
572
- }
573
- catch {
574
- return 'Error: Drag requires xdotool';
575
- }
685
+ finally {
686
+ releaseApp(app);
576
687
  }
577
- return 'Error: Unsupported platform';
578
688
  },
579
689
  });
580
690
  // ── Keyboard type ──
@@ -583,35 +693,46 @@ export function registerComputerTools() {
583
693
  description: 'Type text using the keyboard. Types each character as if pressed by the user.',
584
694
  parameters: {
585
695
  text: { type: 'string', description: 'Text to type', required: true },
696
+ // Optional `app` enables Coordinator per-app locking. Omit for legacy fallback.
697
+ app: { type: 'string', description: 'App being targeted, for parallel-agent coordination (optional)' },
586
698
  },
587
699
  tier: 'free',
588
700
  async execute(args) {
589
701
  const lockErr = ensureLock();
590
702
  if (lockErr)
591
703
  return `Error: ${lockErr}`;
592
- const text = String(args.text);
593
- if (!text)
594
- return 'Error: text is required';
595
- if (platform === 'darwin') {
596
- const escaped = escapeAppleScript(text);
597
- try {
598
- osascript(`tell application "System Events" to keystroke "${escaped}"`, 10_000);
599
- return `Typed: ${text.slice(0, 80)}${text.length > 80 ? '...' : ''}`;
704
+ const app = args.app ? String(args.app) : undefined;
705
+ const claimErr = claimApp(app);
706
+ if (claimErr)
707
+ return `Error: ${claimErr}`;
708
+ try {
709
+ const text = String(args.text);
710
+ if (!text)
711
+ return 'Error: text is required';
712
+ if (platform === 'darwin') {
713
+ const escaped = escapeAppleScript(text);
714
+ try {
715
+ osascript(`tell application "System Events" to keystroke "${escaped}"`, 10_000);
716
+ return `Typed: ${text.slice(0, 80)}${text.length > 80 ? '...' : ''}`;
717
+ }
718
+ catch {
719
+ return 'Error: Typing requires Accessibility permissions';
720
+ }
600
721
  }
601
- catch {
602
- return 'Error: Typing requires Accessibility permissions';
722
+ else if (platform === 'linux') {
723
+ try {
724
+ execSync(`xdotool type -- "${text.replace(/"/g, '\\"')}"`, { timeout: 10_000 });
725
+ return `Typed: ${text.slice(0, 80)}${text.length > 80 ? '...' : ''}`;
726
+ }
727
+ catch {
728
+ return 'Error: Typing requires xdotool';
729
+ }
603
730
  }
731
+ return 'Error: Unsupported platform';
604
732
  }
605
- else if (platform === 'linux') {
606
- try {
607
- execSync(`xdotool type -- "${text.replace(/"/g, '\\"')}"`, { timeout: 10_000 });
608
- return `Typed: ${text.slice(0, 80)}${text.length > 80 ? '...' : ''}`;
609
- }
610
- catch {
611
- return 'Error: Typing requires xdotool';
612
- }
733
+ finally {
734
+ releaseApp(app);
613
735
  }
614
- return 'Error: Unsupported platform';
615
736
  },
616
737
  });
617
738
  // ── Keyboard key ──
@@ -620,72 +741,83 @@ export function registerComputerTools() {
620
741
  description: 'Press a key or key combination. Supports modifiers: cmd/ctrl/alt/shift + key.',
621
742
  parameters: {
622
743
  key: { type: 'string', description: 'Key: enter, tab, escape, space, backspace, delete, up, down, left, right, cmd+c, ctrl+v, cmd+shift+s, etc.', required: true },
744
+ // Optional `app` enables Coordinator per-app locking. Omit for legacy fallback.
745
+ app: { type: 'string', description: 'App being targeted, for parallel-agent coordination (optional)' },
623
746
  },
624
747
  tier: 'free',
625
748
  async execute(args) {
626
749
  const lockErr = ensureLock();
627
750
  if (lockErr)
628
751
  return `Error: ${lockErr}`;
629
- const key = String(args.key).toLowerCase();
630
- if (platform === 'darwin') {
631
- // Key code map for non-character keys
632
- const keyCodeMap = {
633
- enter: 36, return: 36, tab: 48, escape: 53, space: 49,
634
- backspace: 51, delete: 117, up: 126, down: 125, left: 123, right: 124,
635
- home: 115, end: 119, pageup: 116, pagedown: 121,
636
- f1: 122, f2: 120, f3: 99, f4: 118, f5: 96, f6: 97,
637
- f7: 98, f8: 100, f9: 101, f10: 109, f11: 103, f12: 111,
638
- };
639
- try {
640
- if (key.includes('+')) {
641
- const parts = key.split('+');
642
- const mainKey = parts.pop();
643
- const modifiers = parts.map(m => {
644
- if (m === 'cmd' || m === 'command')
645
- return 'command down';
646
- if (m === 'ctrl' || m === 'control')
647
- return 'control down';
648
- if (m === 'alt' || m === 'option')
649
- return 'option down';
650
- if (m === 'shift')
651
- return 'shift down';
652
- return '';
653
- }).filter(Boolean).join(', ');
654
- const code = keyCodeMap[mainKey];
655
- if (code !== undefined) {
656
- osascript(`tell application "System Events" to key code ${code} using {${modifiers}}`);
752
+ const app = args.app ? String(args.app) : undefined;
753
+ const claimErr = claimApp(app);
754
+ if (claimErr)
755
+ return `Error: ${claimErr}`;
756
+ try {
757
+ const key = String(args.key).toLowerCase();
758
+ if (platform === 'darwin') {
759
+ // Key code map for non-character keys
760
+ const keyCodeMap = {
761
+ enter: 36, return: 36, tab: 48, escape: 53, space: 49,
762
+ backspace: 51, delete: 117, up: 126, down: 125, left: 123, right: 124,
763
+ home: 115, end: 119, pageup: 116, pagedown: 121,
764
+ f1: 122, f2: 120, f3: 99, f4: 118, f5: 96, f6: 97,
765
+ f7: 98, f8: 100, f9: 101, f10: 109, f11: 103, f12: 111,
766
+ };
767
+ try {
768
+ if (key.includes('+')) {
769
+ const parts = key.split('+');
770
+ const mainKey = parts.pop();
771
+ const modifiers = parts.map(m => {
772
+ if (m === 'cmd' || m === 'command')
773
+ return 'command down';
774
+ if (m === 'ctrl' || m === 'control')
775
+ return 'control down';
776
+ if (m === 'alt' || m === 'option')
777
+ return 'option down';
778
+ if (m === 'shift')
779
+ return 'shift down';
780
+ return '';
781
+ }).filter(Boolean).join(', ');
782
+ const code = keyCodeMap[mainKey];
783
+ if (code !== undefined) {
784
+ osascript(`tell application "System Events" to key code ${code} using {${modifiers}}`);
785
+ }
786
+ else {
787
+ osascript(`tell application "System Events" to keystroke "${escapeAppleScript(mainKey)}" using {${modifiers}}`);
788
+ }
657
789
  }
658
790
  else {
659
- osascript(`tell application "System Events" to keystroke "${escapeAppleScript(mainKey)}" using {${modifiers}}`);
791
+ const code = keyCodeMap[key];
792
+ if (code !== undefined) {
793
+ osascript(`tell application "System Events" to key code ${code}`);
794
+ }
795
+ else {
796
+ osascript(`tell application "System Events" to keystroke "${escapeAppleScript(key)}"`);
797
+ }
660
798
  }
799
+ return `Pressed: ${key}`;
661
800
  }
662
- else {
663
- const code = keyCodeMap[key];
664
- if (code !== undefined) {
665
- osascript(`tell application "System Events" to key code ${code}`);
666
- }
667
- else {
668
- osascript(`tell application "System Events" to keystroke "${escapeAppleScript(key)}"`);
669
- }
801
+ catch {
802
+ return 'Error: Key press requires Accessibility permissions';
670
803
  }
671
- return `Pressed: ${key}`;
672
804
  }
673
- catch {
674
- return 'Error: Key press requires Accessibility permissions';
805
+ else if (platform === 'linux') {
806
+ try {
807
+ // xdotool uses + for combos: ctrl+c, super+l, etc.
808
+ const xdoKey = key.replace('cmd', 'super').replace('command', 'super');
809
+ execSync(`xdotool key ${xdoKey}`, { timeout: 5_000 });
810
+ return `Pressed: ${key}`;
811
+ }
812
+ catch {
813
+ return 'Error: Key press requires xdotool';
814
+ }
675
815
  }
816
+ return 'Error: Unsupported platform';
676
817
  }
677
- else if (platform === 'linux') {
678
- try {
679
- // xdotool uses + for combos: ctrl+c, super+l, etc.
680
- const xdoKey = key.replace('cmd', 'super').replace('command', 'super');
681
- execSync(`xdotool key ${xdoKey}`, { timeout: 5_000 });
682
- return `Pressed: ${key}`;
683
- }
684
- catch {
685
- return 'Error: Key press requires xdotool';
686
- }
818
+ finally {
819
+ releaseApp(app);
687
820
  }
688
- return 'Error: Unsupported platform';
689
821
  },
690
822
  });
691
823
  // ── Window management ──
@@ -756,32 +888,40 @@ export function registerComputerTools() {
756
888
  return `Error: ${app} not approved. Call app_approve first.`;
757
889
  if (isNaN(w) || isNaN(h))
758
890
  return 'Error: width and height must be numbers';
759
- if (platform === 'darwin') {
760
- try {
761
- osascript(`tell application "${escapeAppleScript(app)}" to set bounds of front window to {0, 0, ${w}, ${h}}`, 5_000);
762
- return `Resized ${app} to ${w}x${h}`;
891
+ const claimErr = claimApp(app);
892
+ if (claimErr)
893
+ return `Error: ${claimErr}`;
894
+ try {
895
+ if (platform === 'darwin') {
896
+ try {
897
+ osascript(`tell application "${escapeAppleScript(app)}" to set bounds of front window to {0, 0, ${w}, ${h}}`, 5_000);
898
+ return `Resized ${app} to ${w}x${h}`;
899
+ }
900
+ catch {
901
+ // Fallback via System Events
902
+ try {
903
+ osascript(`tell application "System Events" to tell process "${escapeAppleScript(app)}" to set size of front window to {${w}, ${h}}`);
904
+ return `Resized ${app} to ${w}x${h}`;
905
+ }
906
+ catch (err) {
907
+ return `Error: ${err instanceof Error ? err.message : String(err)}`;
908
+ }
909
+ }
763
910
  }
764
- catch {
765
- // Fallback via System Events
911
+ else if (platform === 'linux') {
766
912
  try {
767
- osascript(`tell application "System Events" to tell process "${escapeAppleScript(app)}" to set size of front window to {${w}, ${h}}`);
913
+ execSync(`wmctrl -r "${app}" -e 0,-1,-1,${w},${h}`, { timeout: 5_000 });
768
914
  return `Resized ${app} to ${w}x${h}`;
769
915
  }
770
- catch (err) {
771
- return `Error: ${err instanceof Error ? err.message : String(err)}`;
916
+ catch {
917
+ return 'Error: Requires wmctrl';
772
918
  }
773
919
  }
920
+ return 'Error: Unsupported platform';
774
921
  }
775
- else if (platform === 'linux') {
776
- try {
777
- execSync(`wmctrl -r "${app}" -e 0,-1,-1,${w},${h}`, { timeout: 5_000 });
778
- return `Resized ${app} to ${w}x${h}`;
779
- }
780
- catch {
781
- return 'Error: Requires wmctrl';
782
- }
922
+ finally {
923
+ releaseApp(app);
783
924
  }
784
- return 'Error: Unsupported platform';
785
925
  },
786
926
  });
787
927
  registerTool({
@@ -799,25 +939,33 @@ export function registerComputerTools() {
799
939
  const y = Math.round(Number(args.y));
800
940
  if (!isAppApproved(app))
801
941
  return `Error: ${app} not approved. Call app_approve first.`;
802
- if (platform === 'darwin') {
803
- try {
804
- osascript(`tell application "System Events" to tell process "${escapeAppleScript(app)}" to set position of front window to {${x}, ${y}}`);
805
- return `Moved ${app} to (${x}, ${y})`;
942
+ const claimErr = claimApp(app);
943
+ if (claimErr)
944
+ return `Error: ${claimErr}`;
945
+ try {
946
+ if (platform === 'darwin') {
947
+ try {
948
+ osascript(`tell application "System Events" to tell process "${escapeAppleScript(app)}" to set position of front window to {${x}, ${y}}`);
949
+ return `Moved ${app} to (${x}, ${y})`;
950
+ }
951
+ catch (err) {
952
+ return `Error: ${err instanceof Error ? err.message : String(err)}`;
953
+ }
806
954
  }
807
- catch (err) {
808
- return `Error: ${err instanceof Error ? err.message : String(err)}`;
955
+ else if (platform === 'linux') {
956
+ try {
957
+ execSync(`wmctrl -r "${app}" -e 0,${x},${y},-1,-1`, { timeout: 5_000 });
958
+ return `Moved ${app} to (${x}, ${y})`;
959
+ }
960
+ catch {
961
+ return 'Error: Requires wmctrl';
962
+ }
809
963
  }
964
+ return 'Error: Unsupported platform';
810
965
  }
811
- else if (platform === 'linux') {
812
- try {
813
- execSync(`wmctrl -r "${app}" -e 0,${x},${y},-1,-1`, { timeout: 5_000 });
814
- return `Moved ${app} to (${x}, ${y})`;
815
- }
816
- catch {
817
- return 'Error: Requires wmctrl';
818
- }
966
+ finally {
967
+ releaseApp(app);
819
968
  }
820
- return 'Error: Unsupported platform';
821
969
  },
822
970
  });
823
971
  registerTool({
@@ -833,35 +981,43 @@ export function registerComputerTools() {
833
981
  const action = String(args.action || 'minimize').toLowerCase();
834
982
  if (!isAppApproved(app))
835
983
  return `Error: ${app} not approved. Call app_approve first.`;
836
- if (platform === 'darwin') {
837
- try {
838
- if (action === 'restore') {
839
- osascript(`tell application "${escapeAppleScript(app)}" to activate`);
984
+ const claimErr = claimApp(app);
985
+ if (claimErr)
986
+ return `Error: ${claimErr}`;
987
+ try {
988
+ if (platform === 'darwin') {
989
+ try {
990
+ if (action === 'restore') {
991
+ osascript(`tell application "${escapeAppleScript(app)}" to activate`);
992
+ }
993
+ else {
994
+ osascript(`tell application "System Events" to tell process "${escapeAppleScript(app)}" to set miniaturized of front window to true`);
995
+ }
996
+ return `${action === 'restore' ? 'Restored' : 'Minimized'} ${app}`;
840
997
  }
841
- else {
842
- osascript(`tell application "System Events" to tell process "${escapeAppleScript(app)}" to set miniaturized of front window to true`);
998
+ catch (err) {
999
+ return `Error: ${err instanceof Error ? err.message : String(err)}`;
843
1000
  }
844
- return `${action === 'restore' ? 'Restored' : 'Minimized'} ${app}`;
845
- }
846
- catch (err) {
847
- return `Error: ${err instanceof Error ? err.message : String(err)}`;
848
1001
  }
849
- }
850
- else if (platform === 'linux') {
851
- try {
852
- if (action === 'restore') {
853
- execSync(`wmctrl -r "${app}" -b remove,hidden`, { timeout: 5_000 });
1002
+ else if (platform === 'linux') {
1003
+ try {
1004
+ if (action === 'restore') {
1005
+ execSync(`wmctrl -r "${app}" -b remove,hidden`, { timeout: 5_000 });
1006
+ }
1007
+ else {
1008
+ execSync(`xdotool search --name "${app}" windowminimize`, { timeout: 5_000 });
1009
+ }
1010
+ return `${action === 'restore' ? 'Restored' : 'Minimized'} ${app}`;
854
1011
  }
855
- else {
856
- execSync(`xdotool search --name "${app}" windowminimize`, { timeout: 5_000 });
1012
+ catch {
1013
+ return 'Error: Requires wmctrl/xdotool';
857
1014
  }
858
- return `${action === 'restore' ? 'Restored' : 'Minimized'} ${app}`;
859
- }
860
- catch {
861
- return 'Error: Requires wmctrl/xdotool';
862
1015
  }
1016
+ return 'Error: Unsupported platform';
1017
+ }
1018
+ finally {
1019
+ releaseApp(app);
863
1020
  }
864
- return 'Error: Unsupported platform';
865
1021
  },
866
1022
  });
867
1023
  // ── Screen info ──
@@ -928,10 +1084,18 @@ export function registerComputerTools() {
928
1084
  parameters: {},
929
1085
  tier: 'free',
930
1086
  async execute() {
1087
+ let released = [];
1088
+ try {
1089
+ released = coordinator.unregister(AGENT_ID);
1090
+ }
1091
+ catch { /* best effort */ }
931
1092
  releaseLock();
932
1093
  approvedApps.clear();
933
1094
  permissionsVerified = false;
934
- return 'Computer use session ended. Lock released.';
1095
+ const releasedNote = released.length > 0
1096
+ ? ` Released app locks: ${released.join(', ')}.`
1097
+ : '';
1098
+ return `Computer use session ended.${releasedNote}`;
935
1099
  },
936
1100
  });
937
1101
  }