@pulso/companion 0.1.4 → 0.1.5

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 (2) hide show
  1. package/dist/index.js +217 -8
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -45,6 +45,16 @@ function runShell(cmd, timeout = 1e4) {
45
45
  });
46
46
  });
47
47
  }
48
+ function runSwift(code, timeout = 1e4) {
49
+ return new Promise((resolve2, reject) => {
50
+ const child = exec(`swift -`, { timeout }, (err, stdout, stderr) => {
51
+ if (err) reject(new Error(stderr || err.message));
52
+ else resolve2(stdout.trim());
53
+ });
54
+ child.stdin?.write(code);
55
+ child.stdin?.end();
56
+ });
57
+ }
48
58
  var spotifyToken = null;
49
59
  var spotifyTokenExpiry = 0;
50
60
  async function getSpotifyToken() {
@@ -226,20 +236,219 @@ async function handleCommand(command, params) {
226
236
  return { success: true, data: { path, written: content.length } };
227
237
  }
228
238
  case "sys_screenshot": {
229
- const tmpPath = `/tmp/pulso-screenshot-${Date.now()}.png`;
230
- await runShell(`screencapture -x ${tmpPath}`, 15e3);
231
- if (!existsSync(tmpPath)) return { success: false, error: "Screenshot failed" };
232
- const buf = readFileSync(tmpPath);
239
+ const ts = Date.now();
240
+ const pngPath = `/tmp/pulso-ss-${ts}.png`;
241
+ const jpgPath = `/tmp/pulso-ss-${ts}.jpg`;
242
+ await runShell(`screencapture -x ${pngPath}`, 15e3);
243
+ if (!existsSync(pngPath)) return { success: false, error: "Screenshot failed" };
244
+ try {
245
+ await runShell(`sips --resampleWidth 1280 --setProperty format jpeg --setProperty formatOptions 60 ${pngPath} --out ${jpgPath}`, 1e4);
246
+ } catch {
247
+ const buf2 = readFileSync(pngPath);
248
+ exec(`rm -f ${pngPath}`);
249
+ return { success: true, data: { image: `data:image/png;base64,${buf2.toString("base64")}`, format: "png", note: "Full screen screenshot" } };
250
+ }
251
+ const buf = readFileSync(jpgPath);
233
252
  const base64 = buf.toString("base64");
234
- exec(`rm -f ${tmpPath}`);
253
+ exec(`rm -f ${pngPath} ${jpgPath}`);
254
+ let screenSize = "unknown";
255
+ try {
256
+ screenSize = await runShell(`system_profiler SPDisplaysDataType 2>/dev/null | grep Resolution | head -1 | sed 's/.*: //'`);
257
+ } catch {
258
+ }
235
259
  return {
236
260
  success: true,
237
261
  data: {
238
- image: `data:image/png;base64,${base64}`,
239
- note: "Full screen screenshot captured"
262
+ image: `data:image/jpeg;base64,${base64}`,
263
+ format: "jpeg",
264
+ resolution: screenSize.trim(),
265
+ imageWidth: 1280,
266
+ note: "Screenshot captured. Coordinates in this image are scaled \u2014 multiply x by (screen_width/1280) for actual clicks."
240
267
  }
241
268
  };
242
269
  }
270
+ // ── Computer-Use: Mouse & Keyboard ────────────────────
271
+ case "sys_mouse_click": {
272
+ const x = Number(params.x);
273
+ const y = Number(params.y);
274
+ const button = params.button || "left";
275
+ if (isNaN(x) || isNaN(y)) return { success: false, error: "Missing x, y coordinates" };
276
+ const mouseType = button === "right" ? "rightMouseDown" : "leftMouseDown";
277
+ const mouseTypeUp = button === "right" ? "rightMouseUp" : "leftMouseUp";
278
+ const mouseButton = button === "right" ? ".right" : ".left";
279
+ const swift = `
280
+ import Cocoa
281
+ let p = CGPoint(x: ${x}, y: ${y})
282
+ let d = CGEvent(mouseEventSource: nil, mouseType: .${mouseType}, mouseCursorPosition: p, mouseButton: ${mouseButton})!
283
+ d.post(tap: .cghidEventTap)
284
+ usleep(50000)
285
+ let u = CGEvent(mouseEventSource: nil, mouseType: .${mouseTypeUp}, mouseCursorPosition: p, mouseButton: ${mouseButton})!
286
+ u.post(tap: .cghidEventTap)
287
+ print("clicked")`;
288
+ await runSwift(swift);
289
+ return { success: true, data: { clicked: { x, y }, button } };
290
+ }
291
+ case "sys_mouse_double_click": {
292
+ const x = Number(params.x);
293
+ const y = Number(params.y);
294
+ if (isNaN(x) || isNaN(y)) return { success: false, error: "Missing x, y coordinates" };
295
+ const swift = `
296
+ import Cocoa
297
+ let p = CGPoint(x: ${x}, y: ${y})
298
+ let d1 = CGEvent(mouseEventSource: nil, mouseType: .leftMouseDown, mouseCursorPosition: p, mouseButton: .left)!
299
+ d1.setIntegerValueField(.mouseEventClickState, value: 1)
300
+ d1.post(tap: .cghidEventTap)
301
+ usleep(30000)
302
+ let u1 = CGEvent(mouseEventSource: nil, mouseType: .leftMouseUp, mouseCursorPosition: p, mouseButton: .left)!
303
+ u1.setIntegerValueField(.mouseEventClickState, value: 1)
304
+ u1.post(tap: .cghidEventTap)
305
+ usleep(80000)
306
+ let d2 = CGEvent(mouseEventSource: nil, mouseType: .leftMouseDown, mouseCursorPosition: p, mouseButton: .left)!
307
+ d2.setIntegerValueField(.mouseEventClickState, value: 2)
308
+ d2.post(tap: .cghidEventTap)
309
+ usleep(30000)
310
+ let u2 = CGEvent(mouseEventSource: nil, mouseType: .leftMouseUp, mouseCursorPosition: p, mouseButton: .left)!
311
+ u2.setIntegerValueField(.mouseEventClickState, value: 2)
312
+ u2.post(tap: .cghidEventTap)
313
+ print("double-clicked")`;
314
+ await runSwift(swift);
315
+ return { success: true, data: { doubleClicked: { x, y } } };
316
+ }
317
+ case "sys_mouse_scroll": {
318
+ const x = Number(params.x) || 0;
319
+ const y = Number(params.y) || 0;
320
+ const scrollY = Number(params.scrollY) || 0;
321
+ const scrollX = Number(params.scrollX) || 0;
322
+ if (!scrollY && !scrollX) return { success: false, error: "Missing scrollY or scrollX" };
323
+ const swift = `
324
+ import Cocoa
325
+ let p = CGPoint(x: ${x}, y: ${y})
326
+ let move = CGEvent(mouseEventSource: nil, mouseType: .mouseMoved, mouseCursorPosition: p, mouseButton: .left)!
327
+ move.post(tap: .cghidEventTap)
328
+ usleep(50000)
329
+ let scroll = CGEvent(scrollWheelEvent2Source: nil, units: .pixel, wheelCount: 2, wheel1: Int32(${-scrollY}), wheel2: Int32(${-scrollX}))!
330
+ scroll.post(tap: .cghidEventTap)
331
+ print("scrolled")`;
332
+ await runSwift(swift);
333
+ return { success: true, data: { scrolled: { x, y, scrollY, scrollX } } };
334
+ }
335
+ case "sys_keyboard_type": {
336
+ const text = params.text;
337
+ if (!text) return { success: false, error: "Missing text" };
338
+ await runAppleScript(`tell application "System Events" to keystroke "${text.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`);
339
+ return { success: true, data: { typed: text.slice(0, 100) } };
340
+ }
341
+ case "sys_key_press": {
342
+ const key = params.key;
343
+ if (!key) return { success: false, error: "Missing key" };
344
+ const rawMods = params.modifiers;
345
+ const modifiers = Array.isArray(rawMods) ? rawMods : typeof rawMods === "string" ? rawMods.split(",").map((m) => m.trim()).filter(Boolean) : [];
346
+ const modMap = {
347
+ cmd: "command down",
348
+ command: "command down",
349
+ shift: "shift down",
350
+ alt: "option down",
351
+ option: "option down",
352
+ ctrl: "control down",
353
+ control: "control down"
354
+ };
355
+ const modStr = modifiers.map((m) => modMap[m.toLowerCase()] || "").filter(Boolean);
356
+ const keyCodeMap = {
357
+ enter: 36,
358
+ return: 36,
359
+ tab: 48,
360
+ escape: 27,
361
+ esc: 27,
362
+ delete: 51,
363
+ backspace: 51,
364
+ space: 49,
365
+ up: 126,
366
+ down: 125,
367
+ left: 123,
368
+ right: 124,
369
+ f1: 122,
370
+ f2: 120,
371
+ f3: 99,
372
+ f4: 118,
373
+ f5: 96,
374
+ f6: 97,
375
+ f7: 98,
376
+ f8: 100,
377
+ f9: 101,
378
+ f10: 109,
379
+ f11: 103,
380
+ f12: 111,
381
+ home: 115,
382
+ end: 119,
383
+ pageup: 116,
384
+ pagedown: 121
385
+ };
386
+ const keyCode = keyCodeMap[key.toLowerCase()];
387
+ if (keyCode !== void 0) {
388
+ const using = modStr.length > 0 ? ` using {${modStr.join(", ")}}` : "";
389
+ await runAppleScript(`tell application "System Events" to key code ${keyCode}${using}`);
390
+ } else if (key.length === 1) {
391
+ const using = modStr.length > 0 ? ` using {${modStr.join(", ")}}` : "";
392
+ await runAppleScript(`tell application "System Events" to keystroke "${key}"${using}`);
393
+ } else {
394
+ return { success: false, error: `Unknown key: ${key}. Use single characters or: enter, tab, escape, delete, space, up, down, left, right, f1-f12, home, end, pageup, pagedown` };
395
+ }
396
+ return { success: true, data: { pressed: key, modifiers } };
397
+ }
398
+ case "sys_mouse_move": {
399
+ const x = Number(params.x);
400
+ const y = Number(params.y);
401
+ if (isNaN(x) || isNaN(y)) return { success: false, error: "Missing x, y coordinates" };
402
+ const swift = `
403
+ import Cocoa
404
+ let p = CGPoint(x: ${x}, y: ${y})
405
+ let m = CGEvent(mouseEventSource: nil, mouseType: .mouseMoved, mouseCursorPosition: p, mouseButton: .left)!
406
+ m.post(tap: .cghidEventTap)
407
+ print("moved")`;
408
+ await runSwift(swift);
409
+ return { success: true, data: { movedTo: { x, y } } };
410
+ }
411
+ case "sys_drag": {
412
+ const fromX = Number(params.fromX);
413
+ const fromY = Number(params.fromY);
414
+ const toX = Number(params.toX);
415
+ const toY = Number(params.toY);
416
+ if ([fromX, fromY, toX, toY].some(isNaN)) return { success: false, error: "Missing fromX, fromY, toX, toY" };
417
+ const swift = `
418
+ import Cocoa
419
+ let from = CGPoint(x: ${fromX}, y: ${fromY})
420
+ let to = CGPoint(x: ${toX}, y: ${toY})
421
+ let d = CGEvent(mouseEventSource: nil, mouseType: .leftMouseDown, mouseCursorPosition: from, mouseButton: .left)!
422
+ d.post(tap: .cghidEventTap)
423
+ usleep(100000)
424
+ let steps = 10
425
+ for i in 1...steps {
426
+ let frac = Double(i) / Double(steps)
427
+ let p = CGPoint(x: from.x + (to.x - from.x) * frac, y: from.y + (to.y - from.y) * frac)
428
+ let drag = CGEvent(mouseEventSource: nil, mouseType: .leftMouseDragged, mouseCursorPosition: p, mouseButton: .left)!
429
+ drag.post(tap: .cghidEventTap)
430
+ usleep(20000)
431
+ }
432
+ let u = CGEvent(mouseEventSource: nil, mouseType: .leftMouseUp, mouseCursorPosition: to, mouseButton: .left)!
433
+ u.post(tap: .cghidEventTap)
434
+ print("dragged")`;
435
+ await runSwift(swift);
436
+ return { success: true, data: { dragged: { from: { x: fromX, y: fromY }, to: { x: toX, y: toY } } } };
437
+ }
438
+ case "sys_get_cursor_position": {
439
+ const swift = `
440
+ import Cocoa
441
+ let loc = NSEvent.mouseLocation
442
+ let screens = NSScreen.screens
443
+ let mainHeight = screens.first?.frame.height ?? 0
444
+ // Convert from bottom-left to top-left coordinate system
445
+ let x = Int(loc.x)
446
+ let y = Int(mainHeight - loc.y)
447
+ print("\\(x),\\(y)")`;
448
+ const pos = await runSwift(swift);
449
+ const [cx, cy] = pos.trim().split(",").map(Number);
450
+ return { success: true, data: { x: cx, y: cy } };
451
+ }
243
452
  case "sys_run_shortcut": {
244
453
  const name = params.name;
245
454
  const input = params.input;
@@ -334,7 +543,7 @@ function scheduleReconnect() {
334
543
  }
335
544
  console.log("");
336
545
  console.log(" \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557");
337
- console.log(" \u2551 \u{1FAC0} Pulso Mac Companion v0.1.4 \u2551");
546
+ console.log(" \u2551 \u{1FAC0} Pulso Mac Companion v0.1.5 \u2551");
338
547
  console.log(" \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D");
339
548
  console.log("");
340
549
  connect();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pulso/companion",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "type": "module",
5
5
  "description": "Pulso Companion — gives your AI agent real control over your computer",
6
6
  "bin": {