@pulso/companion 0.1.3 → 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.
- package/dist/index.js +278 -26
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -45,6 +45,64 @@ 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
|
+
}
|
|
58
|
+
var spotifyToken = null;
|
|
59
|
+
var spotifyTokenExpiry = 0;
|
|
60
|
+
async function getSpotifyToken() {
|
|
61
|
+
if (spotifyToken && Date.now() < spotifyTokenExpiry) return spotifyToken;
|
|
62
|
+
try {
|
|
63
|
+
const res = await fetch("https://open.spotify.com/embed/track/4u7EnebtmKWzUH433cf5Qv", {
|
|
64
|
+
headers: { "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36" }
|
|
65
|
+
});
|
|
66
|
+
const html = await res.text();
|
|
67
|
+
const match = html.match(/"accessToken":"([^"]+)"/);
|
|
68
|
+
if (!match) return null;
|
|
69
|
+
spotifyToken = match[1];
|
|
70
|
+
spotifyTokenExpiry = Date.now() + 55 * 60 * 1e3;
|
|
71
|
+
return spotifyToken;
|
|
72
|
+
} catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
async function spotifySearch(query) {
|
|
77
|
+
const token = await getSpotifyToken();
|
|
78
|
+
if (!token) return null;
|
|
79
|
+
try {
|
|
80
|
+
const res = await fetch(
|
|
81
|
+
`https://api.spotify.com/v1/search?q=${encodeURIComponent(query)}&type=track&limit=1`,
|
|
82
|
+
{ headers: { Authorization: `Bearer ${token}` } }
|
|
83
|
+
);
|
|
84
|
+
if (!res.ok) {
|
|
85
|
+
if (res.status === 401) {
|
|
86
|
+
spotifyToken = null;
|
|
87
|
+
spotifyTokenExpiry = 0;
|
|
88
|
+
const newToken = await getSpotifyToken();
|
|
89
|
+
if (!newToken) return null;
|
|
90
|
+
const retry = await fetch(
|
|
91
|
+
`https://api.spotify.com/v1/search?q=${encodeURIComponent(query)}&type=track&limit=1`,
|
|
92
|
+
{ headers: { Authorization: `Bearer ${newToken}` } }
|
|
93
|
+
);
|
|
94
|
+
if (!retry.ok) return null;
|
|
95
|
+
const retryData = await retry.json();
|
|
96
|
+
return retryData.tracks?.items?.[0]?.uri ?? null;
|
|
97
|
+
}
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
const data = await res.json();
|
|
101
|
+
return data.tracks?.items?.[0]?.uri ?? null;
|
|
102
|
+
} catch {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
48
106
|
async function handleCommand(command, params) {
|
|
49
107
|
try {
|
|
50
108
|
switch (command) {
|
|
@@ -112,25 +170,20 @@ async function handleCommand(command, params) {
|
|
|
112
170
|
case "search_play": {
|
|
113
171
|
const query = params.query;
|
|
114
172
|
if (!query) return { success: false, error: "Missing search query" };
|
|
115
|
-
await
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
tell
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
await new Promise((r) => setTimeout(r, 1500));
|
|
127
|
-
try {
|
|
128
|
-
const track = await runAppleScript('tell application "Spotify" to name of current track');
|
|
129
|
-
const artist = await runAppleScript('tell application "Spotify" to artist of current track');
|
|
130
|
-
return { success: true, data: { searched: query, nowPlaying: `${track} - ${artist}` } };
|
|
131
|
-
} catch {
|
|
132
|
-
return { success: true, data: { searched: query, note: "Search opened and play triggered" } };
|
|
173
|
+
const trackUri = await spotifySearch(query);
|
|
174
|
+
if (trackUri) {
|
|
175
|
+
await runAppleScript(`tell application "Spotify" to play track "${trackUri}"`);
|
|
176
|
+
await new Promise((r) => setTimeout(r, 1500));
|
|
177
|
+
try {
|
|
178
|
+
const track = await runAppleScript('tell application "Spotify" to name of current track');
|
|
179
|
+
const artist = await runAppleScript('tell application "Spotify" to artist of current track');
|
|
180
|
+
return { success: true, data: { searched: query, nowPlaying: `${track} - ${artist}`, state: "playing" } };
|
|
181
|
+
} catch {
|
|
182
|
+
return { success: true, data: { searched: query, note: "Playing track" } };
|
|
183
|
+
}
|
|
133
184
|
}
|
|
185
|
+
await runShell(`open "spotify:search:${encodeURIComponent(query)}"`);
|
|
186
|
+
return { success: true, data: { searched: query, note: "Opened Spotify search (API unavailable). Select a song to play." } };
|
|
134
187
|
}
|
|
135
188
|
case "volume": {
|
|
136
189
|
const level = params.level;
|
|
@@ -183,20 +236,219 @@ async function handleCommand(command, params) {
|
|
|
183
236
|
return { success: true, data: { path, written: content.length } };
|
|
184
237
|
}
|
|
185
238
|
case "sys_screenshot": {
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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);
|
|
190
252
|
const base64 = buf.toString("base64");
|
|
191
|
-
exec(`rm -f ${
|
|
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
|
+
}
|
|
192
259
|
return {
|
|
193
260
|
success: true,
|
|
194
261
|
data: {
|
|
195
|
-
image: `data:image/
|
|
196
|
-
|
|
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."
|
|
197
267
|
}
|
|
198
268
|
};
|
|
199
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
|
+
}
|
|
200
452
|
case "sys_run_shortcut": {
|
|
201
453
|
const name = params.name;
|
|
202
454
|
const input = params.input;
|
|
@@ -291,7 +543,7 @@ function scheduleReconnect() {
|
|
|
291
543
|
}
|
|
292
544
|
console.log("");
|
|
293
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");
|
|
294
|
-
console.log(" \u2551 \u{1FAC0} Pulso Mac Companion v0.1.
|
|
546
|
+
console.log(" \u2551 \u{1FAC0} Pulso Mac Companion v0.1.5 \u2551");
|
|
295
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");
|
|
296
548
|
console.log("");
|
|
297
549
|
connect();
|