@pulso/companion 0.4.3 → 0.4.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 +2033 -559
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -346,7 +346,7 @@ var require_dist = __commonJS({
|
|
|
346
346
|
|
|
347
347
|
// src/index.ts
|
|
348
348
|
import WebSocket from "ws";
|
|
349
|
-
import { exec as exec5, execSync as execSync3 } from "child_process";
|
|
349
|
+
import { exec as exec5, execSync as execSync3, spawn } from "child_process";
|
|
350
350
|
import { createRequire } from "module";
|
|
351
351
|
import { createInterface } from "readline/promises";
|
|
352
352
|
import { stdin as input, stdout as output } from "process";
|
|
@@ -381,13 +381,7 @@ import { homedir, hostname, cpus, totalmem, freemem, uptime } from "os";
|
|
|
381
381
|
import { join, resolve, basename, extname } from "path";
|
|
382
382
|
var HOME = homedir();
|
|
383
383
|
var NOTES_DIR = join(HOME, "Documents", "PulsoNotes");
|
|
384
|
-
var SAFE_DIRS = [
|
|
385
|
-
"Documents",
|
|
386
|
-
"Desktop",
|
|
387
|
-
"Downloads",
|
|
388
|
-
"Projects",
|
|
389
|
-
"Projetos"
|
|
390
|
-
];
|
|
384
|
+
var SAFE_DIRS = ["Documents", "Desktop", "Downloads", "Projects", "Projetos"];
|
|
391
385
|
var ACCESS_LEVEL = process.env.PULSO_ACCESS ?? "sandboxed";
|
|
392
386
|
function detectDisplayServer() {
|
|
393
387
|
const sessionType = process.env.XDG_SESSION_TYPE;
|
|
@@ -674,9 +668,7 @@ var LinuxAdapter = class {
|
|
|
674
668
|
async getVolume() {
|
|
675
669
|
try {
|
|
676
670
|
if (await commandExists("wpctl")) {
|
|
677
|
-
const output2 = await runShell(
|
|
678
|
-
"wpctl get-volume @DEFAULT_AUDIO_SINK@"
|
|
679
|
-
);
|
|
671
|
+
const output2 = await runShell("wpctl get-volume @DEFAULT_AUDIO_SINK@");
|
|
680
672
|
const match = output2.match(/Volume:\s+([\d.]+)/);
|
|
681
673
|
if (match) {
|
|
682
674
|
const volume = Math.round(parseFloat(match[1]) * 100);
|
|
@@ -703,9 +695,7 @@ var LinuxAdapter = class {
|
|
|
703
695
|
}
|
|
704
696
|
}
|
|
705
697
|
if (await commandExists("amixer")) {
|
|
706
|
-
const output2 = await runShell(
|
|
707
|
-
"amixer sget Master | tail -1"
|
|
708
|
-
);
|
|
698
|
+
const output2 = await runShell("amixer sget Master | tail -1");
|
|
709
699
|
const match = output2.match(/\[(\d+)%\]/);
|
|
710
700
|
const muteMatch = output2.match(/\[(on|off)\]/);
|
|
711
701
|
if (match) {
|
|
@@ -763,7 +753,10 @@ var LinuxAdapter = class {
|
|
|
763
753
|
if (backlightDirs.length > 0) {
|
|
764
754
|
const bl = backlightDirs[0];
|
|
765
755
|
const current = parseInt(
|
|
766
|
-
readFileSync(
|
|
756
|
+
readFileSync(
|
|
757
|
+
`/sys/class/backlight/${bl}/brightness`,
|
|
758
|
+
"utf-8"
|
|
759
|
+
).trim(),
|
|
767
760
|
10
|
|
768
761
|
);
|
|
769
762
|
const max = parseInt(
|
|
@@ -845,9 +838,7 @@ var LinuxAdapter = class {
|
|
|
845
838
|
"xrandr --query | grep ' connected' | head -1 | cut -d' ' -f1"
|
|
846
839
|
);
|
|
847
840
|
if (displays) {
|
|
848
|
-
await runShell(
|
|
849
|
-
`xrandr --output ${displays} --brightness ${scalar}`
|
|
850
|
-
);
|
|
841
|
+
await runShell(`xrandr --output ${displays} --brightness ${scalar}`);
|
|
851
842
|
return {
|
|
852
843
|
success: true,
|
|
853
844
|
data: {
|
|
@@ -931,20 +922,14 @@ var LinuxAdapter = class {
|
|
|
931
922
|
if (wifiOut) wifi = wifiOut;
|
|
932
923
|
} catch {
|
|
933
924
|
try {
|
|
934
|
-
const wifiOut = await runShell(
|
|
935
|
-
"iwgetid -r 2>/dev/null",
|
|
936
|
-
3e3
|
|
937
|
-
);
|
|
925
|
+
const wifiOut = await runShell("iwgetid -r 2>/dev/null", 3e3);
|
|
938
926
|
if (wifiOut) wifi = wifiOut;
|
|
939
927
|
} catch {
|
|
940
928
|
}
|
|
941
929
|
}
|
|
942
930
|
let ip;
|
|
943
931
|
try {
|
|
944
|
-
const ipOut = await runShell(
|
|
945
|
-
"hostname -I | awk '{print $1}'",
|
|
946
|
-
3e3
|
|
947
|
-
);
|
|
932
|
+
const ipOut = await runShell("hostname -I | awk '{print $1}'", 3e3);
|
|
948
933
|
if (ipOut) ip = ipOut;
|
|
949
934
|
} catch {
|
|
950
935
|
}
|
|
@@ -1233,7 +1218,10 @@ var LinuxAdapter = class {
|
|
|
1233
1218
|
let finalBuffer = imgBuffer;
|
|
1234
1219
|
if (await commandExists("convert")) {
|
|
1235
1220
|
try {
|
|
1236
|
-
const resizedFile = join(
|
|
1221
|
+
const resizedFile = join(
|
|
1222
|
+
"/tmp",
|
|
1223
|
+
`pulso-screenshot-${ts}-resized.png`
|
|
1224
|
+
);
|
|
1237
1225
|
await runShell(
|
|
1238
1226
|
`convert '${tmpFile}' -resize '1280x>' '${resizedFile}'`,
|
|
1239
1227
|
1e4
|
|
@@ -1447,9 +1435,7 @@ var LinuxAdapter = class {
|
|
|
1447
1435
|
for (let i = 1; i <= steps; i++) {
|
|
1448
1436
|
const cx = Math.round(fromX + (toX - fromX) * i / steps);
|
|
1449
1437
|
const cy = Math.round(fromY + (toY - fromY) * i / steps);
|
|
1450
|
-
await runShell(
|
|
1451
|
-
`ydotool mousemove --absolute -x ${cx} -y ${cy}`
|
|
1452
|
-
);
|
|
1438
|
+
await runShell(`ydotool mousemove --absolute -x ${cx} -y ${cy}`);
|
|
1453
1439
|
await new Promise((r) => setTimeout(r, 20));
|
|
1454
1440
|
}
|
|
1455
1441
|
await runShell(`ydotool mouseup 0xC0`);
|
|
@@ -1464,9 +1450,7 @@ var LinuxAdapter = class {
|
|
|
1464
1450
|
};
|
|
1465
1451
|
}
|
|
1466
1452
|
if (await commandExists("xdotool")) {
|
|
1467
|
-
await runShell(
|
|
1468
|
-
`xdotool mousemove ${fromX} ${fromY} mousedown 1`
|
|
1469
|
-
);
|
|
1453
|
+
await runShell(`xdotool mousemove ${fromX} ${fromY} mousedown 1`);
|
|
1470
1454
|
await new Promise((r) => setTimeout(r, 50));
|
|
1471
1455
|
const steps = 10;
|
|
1472
1456
|
for (let i = 1; i <= steps; i++) {
|
|
@@ -1538,10 +1522,7 @@ var LinuxAdapter = class {
|
|
|
1538
1522
|
}
|
|
1539
1523
|
if (await commandExists("xdotool")) {
|
|
1540
1524
|
const safeText = text.replace(/'/g, "'\\''");
|
|
1541
|
-
await runShell(
|
|
1542
|
-
`xdotool type --clearmodifiers -- '${safeText}'`,
|
|
1543
|
-
1e4
|
|
1544
|
-
);
|
|
1525
|
+
await runShell(`xdotool type --clearmodifiers -- '${safeText}'`, 1e4);
|
|
1545
1526
|
return { success: true, data: { text, length: text.length } };
|
|
1546
1527
|
}
|
|
1547
1528
|
return {
|
|
@@ -1626,7 +1607,12 @@ var LinuxAdapter = class {
|
|
|
1626
1607
|
await runShell(`ydotool key ${keyName}`);
|
|
1627
1608
|
return {
|
|
1628
1609
|
success: true,
|
|
1629
|
-
data: {
|
|
1610
|
+
data: {
|
|
1611
|
+
key,
|
|
1612
|
+
modifiers: modifiers || [],
|
|
1613
|
+
combo,
|
|
1614
|
+
note: "ydotool has limited modifier support"
|
|
1615
|
+
}
|
|
1630
1616
|
};
|
|
1631
1617
|
}
|
|
1632
1618
|
return {
|
|
@@ -1724,9 +1710,7 @@ var LinuxAdapter = class {
|
|
|
1724
1710
|
const page = tabs.find((t) => t.type === "page");
|
|
1725
1711
|
if (page) {
|
|
1726
1712
|
await this.cdpRequest(`/json/activate/${page.id}`, "GET");
|
|
1727
|
-
await runShell(
|
|
1728
|
-
`xdg-open '${url.replace(/'/g, "'\\''")}'`
|
|
1729
|
-
);
|
|
1713
|
+
await runShell(`xdg-open '${url.replace(/'/g, "'\\''")}'`);
|
|
1730
1714
|
return {
|
|
1731
1715
|
success: true,
|
|
1732
1716
|
data: { url, browser: browser || "default" }
|
|
@@ -1735,9 +1719,7 @@ var LinuxAdapter = class {
|
|
|
1735
1719
|
throw new Error("No CDP page found");
|
|
1736
1720
|
} catch {
|
|
1737
1721
|
try {
|
|
1738
|
-
await runShell(
|
|
1739
|
-
`xdg-open '${url.replace(/'/g, "'\\''")}'`
|
|
1740
|
-
);
|
|
1722
|
+
await runShell(`xdg-open '${url.replace(/'/g, "'\\''")}'`);
|
|
1741
1723
|
return {
|
|
1742
1724
|
success: true,
|
|
1743
1725
|
data: { url, browser: browser || "default", method: "xdg-open" }
|
|
@@ -1773,9 +1755,7 @@ var LinuxAdapter = class {
|
|
|
1773
1755
|
};
|
|
1774
1756
|
} catch {
|
|
1775
1757
|
try {
|
|
1776
|
-
await runShell(
|
|
1777
|
-
`xdg-open '${url.replace(/'/g, "'\\''")}'`
|
|
1778
|
-
);
|
|
1758
|
+
await runShell(`xdg-open '${url.replace(/'/g, "'\\''")}'`);
|
|
1779
1759
|
return {
|
|
1780
1760
|
success: true,
|
|
1781
1761
|
data: { url, browser: "default", method: "xdg-open" }
|
|
@@ -1835,6 +1815,60 @@ var LinuxAdapter = class {
|
|
|
1835
1815
|
};
|
|
1836
1816
|
}
|
|
1837
1817
|
}
|
|
1818
|
+
async browserListProfiles() {
|
|
1819
|
+
const home = process.env.HOME || "";
|
|
1820
|
+
const browserPaths = [
|
|
1821
|
+
{ browser: "Google Chrome", dir: `${home}/.config/google-chrome` },
|
|
1822
|
+
{ browser: "Chromium", dir: `${home}/.config/chromium` },
|
|
1823
|
+
{ browser: "Microsoft Edge", dir: `${home}/.config/microsoft-edge` },
|
|
1824
|
+
{
|
|
1825
|
+
browser: "Brave Browser",
|
|
1826
|
+
dir: `${home}/.config/BraveSoftware/Brave-Browser`
|
|
1827
|
+
}
|
|
1828
|
+
];
|
|
1829
|
+
const profiles = [];
|
|
1830
|
+
for (const { browser, dir } of browserPaths) {
|
|
1831
|
+
if (!existsSync(dir)) continue;
|
|
1832
|
+
let entries = [];
|
|
1833
|
+
try {
|
|
1834
|
+
entries = readdirSync(dir);
|
|
1835
|
+
} catch {
|
|
1836
|
+
continue;
|
|
1837
|
+
}
|
|
1838
|
+
for (const entry of entries) {
|
|
1839
|
+
if (entry !== "Default" && !entry.startsWith("Profile ")) continue;
|
|
1840
|
+
const prefsPath = `${dir}/${entry}/Preferences`;
|
|
1841
|
+
if (!existsSync(prefsPath)) continue;
|
|
1842
|
+
try {
|
|
1843
|
+
const prefs = JSON.parse(readFileSync(prefsPath, "utf-8"));
|
|
1844
|
+
profiles.push({
|
|
1845
|
+
browser,
|
|
1846
|
+
profileDir: `${dir}/${entry}`,
|
|
1847
|
+
name: prefs.profile?.name || entry,
|
|
1848
|
+
email: prefs.account_info?.[0]?.email,
|
|
1849
|
+
isDefault: entry === "Default"
|
|
1850
|
+
});
|
|
1851
|
+
} catch {
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
const ffDir = `${home}/.mozilla/firefox`;
|
|
1856
|
+
if (existsSync(ffDir)) {
|
|
1857
|
+
try {
|
|
1858
|
+
for (const entry of readdirSync(ffDir)) {
|
|
1859
|
+
if (!existsSync(`${ffDir}/${entry}/prefs.js`)) continue;
|
|
1860
|
+
profiles.push({
|
|
1861
|
+
browser: "Firefox",
|
|
1862
|
+
profileDir: `${ffDir}/${entry}`,
|
|
1863
|
+
name: entry.replace(/^[a-z0-9]+\./, ""),
|
|
1864
|
+
isDefault: entry.includes("default")
|
|
1865
|
+
});
|
|
1866
|
+
}
|
|
1867
|
+
} catch {
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
return { success: true, data: { profiles, total: profiles.length } };
|
|
1871
|
+
}
|
|
1838
1872
|
/* ══════════════════════════════════════════════════════════
|
|
1839
1873
|
* Productivity: Calendar
|
|
1840
1874
|
*
|
|
@@ -1994,9 +2028,7 @@ ${body}`;
|
|
|
1994
2028
|
if (method === "gmail") {
|
|
1995
2029
|
try {
|
|
1996
2030
|
const gmailUrl = `https://mail.google.com/mail/?view=cm&to=${encodeURIComponent(to)}&su=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`;
|
|
1997
|
-
await runShell(
|
|
1998
|
-
`xdg-open '${gmailUrl.replace(/'/g, "'\\''")}'`
|
|
1999
|
-
);
|
|
2031
|
+
await runShell(`xdg-open '${gmailUrl.replace(/'/g, "'\\''")}'`);
|
|
2000
2032
|
return {
|
|
2001
2033
|
success: true,
|
|
2002
2034
|
data: {
|
|
@@ -2015,9 +2047,7 @@ ${body}`;
|
|
|
2015
2047
|
}
|
|
2016
2048
|
try {
|
|
2017
2049
|
const mailtoUrl = `mailto:${encodeURIComponent(to)}?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`;
|
|
2018
|
-
await runShell(
|
|
2019
|
-
`xdg-open '${mailtoUrl.replace(/'/g, "'\\''")}'`
|
|
2020
|
-
);
|
|
2050
|
+
await runShell(`xdg-open '${mailtoUrl.replace(/'/g, "'\\''")}'`);
|
|
2021
2051
|
return {
|
|
2022
2052
|
success: true,
|
|
2023
2053
|
data: {
|
|
@@ -2190,9 +2220,7 @@ ${body}`;
|
|
|
2190
2220
|
}
|
|
2191
2221
|
if (await commandExists("gio")) {
|
|
2192
2222
|
try {
|
|
2193
|
-
await runShell(
|
|
2194
|
-
`gio trash '${fullPath.replace(/'/g, "'\\''")}'`
|
|
2195
|
-
);
|
|
2223
|
+
await runShell(`gio trash '${fullPath.replace(/'/g, "'\\''")}'`);
|
|
2196
2224
|
return {
|
|
2197
2225
|
success: true,
|
|
2198
2226
|
data: { path, action: "moved_to_trash" }
|
|
@@ -2214,9 +2242,7 @@ ${body}`;
|
|
|
2214
2242
|
}
|
|
2215
2243
|
if (await commandExists("trash-put")) {
|
|
2216
2244
|
try {
|
|
2217
|
-
await runShell(
|
|
2218
|
-
`trash-put '${fullPath.replace(/'/g, "'\\''")}'`
|
|
2219
|
-
);
|
|
2245
|
+
await runShell(`trash-put '${fullPath.replace(/'/g, "'\\''")}'`);
|
|
2220
2246
|
return {
|
|
2221
2247
|
success: true,
|
|
2222
2248
|
data: { path, action: "moved_to_trash" }
|
|
@@ -2449,9 +2475,7 @@ ${body}`;
|
|
|
2449
2475
|
const gy = y !== void 0 ? y : -1;
|
|
2450
2476
|
const gw = width !== void 0 ? width : -1;
|
|
2451
2477
|
const gh = height !== void 0 ? height : -1;
|
|
2452
|
-
await runShell(
|
|
2453
|
-
`wmctrl -r '${safeApp}' -e 0,${gx},${gy},${gw},${gh}`
|
|
2454
|
-
);
|
|
2478
|
+
await runShell(`wmctrl -r '${safeApp}' -e 0,${gx},${gy},${gw},${gh}`);
|
|
2455
2479
|
return {
|
|
2456
2480
|
success: true,
|
|
2457
2481
|
data: { app, x, y, width, height }
|
|
@@ -2619,9 +2643,7 @@ ${body}`;
|
|
|
2619
2643
|
`dbus-send --print-reply --dest=org.mpris.MediaPlayer2.spotify /org/mpris/MediaPlayer2 org.freedesktop.DBus.Properties.Get string:'org.mpris.MediaPlayer2.Player' string:'Metadata'`,
|
|
2620
2644
|
5e3
|
|
2621
2645
|
);
|
|
2622
|
-
const titleMatch = output2.match(
|
|
2623
|
-
/xesam:title.*?string "([^"]+)"/
|
|
2624
|
-
);
|
|
2646
|
+
const titleMatch = output2.match(/xesam:title.*?string "([^"]+)"/);
|
|
2625
2647
|
const artistMatch = output2.match(
|
|
2626
2648
|
/xesam:artist.*?string "([^"]+)"/
|
|
2627
2649
|
);
|
|
@@ -2651,9 +2673,7 @@ ${body}`;
|
|
|
2651
2673
|
return { success: false, error: "Missing search query" };
|
|
2652
2674
|
}
|
|
2653
2675
|
const uri = `spotify:search:${encodeURIComponent(query)}`;
|
|
2654
|
-
await runShell(
|
|
2655
|
-
`xdg-open '${uri}' 2>/dev/null &`
|
|
2656
|
-
);
|
|
2676
|
+
await runShell(`xdg-open '${uri}' 2>/dev/null &`);
|
|
2657
2677
|
return {
|
|
2658
2678
|
success: true,
|
|
2659
2679
|
data: {
|
|
@@ -2777,12 +2797,9 @@ ${body}`;
|
|
|
2777
2797
|
if (!lightId) {
|
|
2778
2798
|
return { success: false, error: `Light '${light}' not found` };
|
|
2779
2799
|
}
|
|
2780
|
-
const res = await hueRequest(
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
"PUT",
|
|
2784
|
-
{ on: false }
|
|
2785
|
-
);
|
|
2800
|
+
const res = await hueRequest(config, `lights/${lightId}/state`, "PUT", {
|
|
2801
|
+
on: false
|
|
2802
|
+
});
|
|
2786
2803
|
return {
|
|
2787
2804
|
success: true,
|
|
2788
2805
|
data: { light: lightId, action: "off", response: res }
|
|
@@ -2809,15 +2826,10 @@ ${body}`;
|
|
|
2809
2826
|
return { success: false, error: `Unrecognized color: ${color}` };
|
|
2810
2827
|
}
|
|
2811
2828
|
const [x, y] = rgbToXy(...rgb);
|
|
2812
|
-
const res = await hueRequest(
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
{
|
|
2817
|
-
on: true,
|
|
2818
|
-
xy: [x, y]
|
|
2819
|
-
}
|
|
2820
|
-
);
|
|
2829
|
+
const res = await hueRequest(config, `lights/${lightId}/state`, "PUT", {
|
|
2830
|
+
on: true,
|
|
2831
|
+
xy: [x, y]
|
|
2832
|
+
});
|
|
2821
2833
|
return {
|
|
2822
2834
|
success: true,
|
|
2823
2835
|
data: { light: lightId, color, xy: [x, y], response: res }
|
|
@@ -2840,15 +2852,10 @@ ${body}`;
|
|
|
2840
2852
|
return { success: false, error: `Light '${light}' not found` };
|
|
2841
2853
|
}
|
|
2842
2854
|
const bri = Math.max(1, Math.min(254, brightness));
|
|
2843
|
-
const res = await hueRequest(
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
{
|
|
2848
|
-
on: true,
|
|
2849
|
-
bri
|
|
2850
|
-
}
|
|
2851
|
-
);
|
|
2855
|
+
const res = await hueRequest(config, `lights/${lightId}/state`, "PUT", {
|
|
2856
|
+
on: true,
|
|
2857
|
+
bri
|
|
2858
|
+
});
|
|
2852
2859
|
return {
|
|
2853
2860
|
success: true,
|
|
2854
2861
|
data: { light: lightId, brightness: bri, response: res }
|
|
@@ -2881,14 +2888,9 @@ ${body}`;
|
|
|
2881
2888
|
error: `Scene '${scene}' not found. Available: ${Object.values(scenes).map((s) => s.name).join(", ")}`
|
|
2882
2889
|
};
|
|
2883
2890
|
}
|
|
2884
|
-
const res = await hueRequest(
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
"PUT",
|
|
2888
|
-
{
|
|
2889
|
-
scene: sceneId
|
|
2890
|
-
}
|
|
2891
|
-
);
|
|
2891
|
+
const res = await hueRequest(config, `groups/${groupId}/action`, "PUT", {
|
|
2892
|
+
scene: sceneId
|
|
2893
|
+
});
|
|
2892
2894
|
return {
|
|
2893
2895
|
success: true,
|
|
2894
2896
|
data: { scene, sceneId, group: groupId, response: res }
|
|
@@ -2953,10 +2955,7 @@ ${body}`;
|
|
|
2953
2955
|
error: "Sonos not configured. Set SONOS_API_URL environment variable (e.g., http://localhost:5005)."
|
|
2954
2956
|
};
|
|
2955
2957
|
}
|
|
2956
|
-
const res = await sonosRequest(
|
|
2957
|
-
url,
|
|
2958
|
-
`${encodeURIComponent(room)}/play`
|
|
2959
|
-
);
|
|
2958
|
+
const res = await sonosRequest(url, `${encodeURIComponent(room)}/play`);
|
|
2960
2959
|
return {
|
|
2961
2960
|
success: true,
|
|
2962
2961
|
data: { room, action: "play", response: res }
|
|
@@ -2971,12 +2970,8 @@ ${body}`;
|
|
|
2971
2970
|
async sonosPause(room) {
|
|
2972
2971
|
try {
|
|
2973
2972
|
const url = getSonosApiUrl();
|
|
2974
|
-
if (!url)
|
|
2975
|
-
|
|
2976
|
-
const res = await sonosRequest(
|
|
2977
|
-
url,
|
|
2978
|
-
`${encodeURIComponent(room)}/pause`
|
|
2979
|
-
);
|
|
2973
|
+
if (!url) return { success: false, error: "Sonos not configured." };
|
|
2974
|
+
const res = await sonosRequest(url, `${encodeURIComponent(room)}/pause`);
|
|
2980
2975
|
return {
|
|
2981
2976
|
success: true,
|
|
2982
2977
|
data: { room, action: "pause", response: res }
|
|
@@ -2991,8 +2986,7 @@ ${body}`;
|
|
|
2991
2986
|
async sonosVolume(room, level) {
|
|
2992
2987
|
try {
|
|
2993
2988
|
const url = getSonosApiUrl();
|
|
2994
|
-
if (!url)
|
|
2995
|
-
return { success: false, error: "Sonos not configured." };
|
|
2989
|
+
if (!url) return { success: false, error: "Sonos not configured." };
|
|
2996
2990
|
const vol = Math.max(0, Math.min(100, level));
|
|
2997
2991
|
const res = await sonosRequest(
|
|
2998
2992
|
url,
|
|
@@ -3012,8 +3006,7 @@ ${body}`;
|
|
|
3012
3006
|
async sonosPlayUri(room, uri, title) {
|
|
3013
3007
|
try {
|
|
3014
3008
|
const url = getSonosApiUrl();
|
|
3015
|
-
if (!url)
|
|
3016
|
-
return { success: false, error: "Sonos not configured." };
|
|
3009
|
+
if (!url) return { success: false, error: "Sonos not configured." };
|
|
3017
3010
|
if (uri.startsWith("spotify:")) {
|
|
3018
3011
|
const res2 = await sonosRequest(
|
|
3019
3012
|
url,
|
|
@@ -3039,8 +3032,7 @@ ${body}`;
|
|
|
3039
3032
|
async sonosRooms() {
|
|
3040
3033
|
try {
|
|
3041
3034
|
const url = getSonosApiUrl();
|
|
3042
|
-
if (!url)
|
|
3043
|
-
return { success: false, error: "Sonos not configured." };
|
|
3035
|
+
if (!url) return { success: false, error: "Sonos not configured." };
|
|
3044
3036
|
const res = await sonosRequest(url, "zones");
|
|
3045
3037
|
return { success: true, data: res };
|
|
3046
3038
|
} catch (err) {
|
|
@@ -3053,12 +3045,8 @@ ${body}`;
|
|
|
3053
3045
|
async sonosNext(room) {
|
|
3054
3046
|
try {
|
|
3055
3047
|
const url = getSonosApiUrl();
|
|
3056
|
-
if (!url)
|
|
3057
|
-
|
|
3058
|
-
const res = await sonosRequest(
|
|
3059
|
-
url,
|
|
3060
|
-
`${encodeURIComponent(room)}/next`
|
|
3061
|
-
);
|
|
3048
|
+
if (!url) return { success: false, error: "Sonos not configured." };
|
|
3049
|
+
const res = await sonosRequest(url, `${encodeURIComponent(room)}/next`);
|
|
3062
3050
|
return {
|
|
3063
3051
|
success: true,
|
|
3064
3052
|
data: { room, action: "next", response: res }
|
|
@@ -3073,8 +3061,7 @@ ${body}`;
|
|
|
3073
3061
|
async sonosPrevious(room) {
|
|
3074
3062
|
try {
|
|
3075
3063
|
const url = getSonosApiUrl();
|
|
3076
|
-
if (!url)
|
|
3077
|
-
return { success: false, error: "Sonos not configured." };
|
|
3064
|
+
if (!url) return { success: false, error: "Sonos not configured." };
|
|
3078
3065
|
const res = await sonosRequest(
|
|
3079
3066
|
url,
|
|
3080
3067
|
`${encodeURIComponent(room)}/previous`
|
|
@@ -3093,12 +3080,8 @@ ${body}`;
|
|
|
3093
3080
|
async sonosNowPlaying(room) {
|
|
3094
3081
|
try {
|
|
3095
3082
|
const url = getSonosApiUrl();
|
|
3096
|
-
if (!url)
|
|
3097
|
-
|
|
3098
|
-
const res = await sonosRequest(
|
|
3099
|
-
url,
|
|
3100
|
-
`${encodeURIComponent(room)}/state`
|
|
3101
|
-
);
|
|
3083
|
+
if (!url) return { success: false, error: "Sonos not configured." };
|
|
3084
|
+
const res = await sonosRequest(url, `${encodeURIComponent(room)}/state`);
|
|
3102
3085
|
return {
|
|
3103
3086
|
success: true,
|
|
3104
3087
|
data: { room, ...res }
|
|
@@ -3148,7 +3131,8 @@ async function loadKokoro() {
|
|
|
3148
3131
|
console.log(" \u{1F399}\uFE0F Loading Kokoro TTS (~83MB, first use only)...");
|
|
3149
3132
|
const mod = await import("./kokoro-UIHMLMG3.js");
|
|
3150
3133
|
const KokoroTTS = mod.KokoroTTS ?? mod.default?.KokoroTTS;
|
|
3151
|
-
if (!KokoroTTS)
|
|
3134
|
+
if (!KokoroTTS)
|
|
3135
|
+
throw new Error("KokoroTTS class not found in kokoro-js export");
|
|
3152
3136
|
kokoroTts = await KokoroTTS.from_pretrained(KOKORO_MODEL, {
|
|
3153
3137
|
dtype: KOKORO_DTYPE,
|
|
3154
3138
|
device: "cpu"
|
|
@@ -3158,7 +3142,10 @@ async function loadKokoro() {
|
|
|
3158
3142
|
} catch (err) {
|
|
3159
3143
|
kokoroState = "failed";
|
|
3160
3144
|
const msg = err.message ?? String(err);
|
|
3161
|
-
console.warn(
|
|
3145
|
+
console.warn(
|
|
3146
|
+
" \u2139\uFE0F Kokoro TTS unavailable (optional):",
|
|
3147
|
+
msg.slice(0, 120)
|
|
3148
|
+
);
|
|
3162
3149
|
}
|
|
3163
3150
|
})();
|
|
3164
3151
|
return kokoroLoadPromise;
|
|
@@ -3291,12 +3278,18 @@ async function speakKokoro(text, voice) {
|
|
|
3291
3278
|
const tmpFile = join2(tmpdir(), `pulso-tts-${Date.now()}.wav`);
|
|
3292
3279
|
try {
|
|
3293
3280
|
const result = await kokoroTts.generate(text.slice(0, 500), { voice });
|
|
3294
|
-
const wav = float32ToWav(
|
|
3281
|
+
const wav = float32ToWav(
|
|
3282
|
+
result.audio,
|
|
3283
|
+
result.sampling_rate
|
|
3284
|
+
);
|
|
3295
3285
|
writeFileSync2(tmpFile, wav);
|
|
3296
3286
|
await playWavFile(tmpFile);
|
|
3297
3287
|
return true;
|
|
3298
3288
|
} catch (err) {
|
|
3299
|
-
console.warn(
|
|
3289
|
+
console.warn(
|
|
3290
|
+
" \u26A0\uFE0F Kokoro speak failed:",
|
|
3291
|
+
err.message.slice(0, 100)
|
|
3292
|
+
);
|
|
3300
3293
|
return false;
|
|
3301
3294
|
} finally {
|
|
3302
3295
|
try {
|
|
@@ -3317,7 +3310,8 @@ function playWavFile(filePath) {
|
|
|
3317
3310
|
cmd = `aplay "${filePath}" 2>/dev/null || paplay "${filePath}" 2>/dev/null || ffplay -nodisp -autoexit "${filePath}" 2>/dev/null || true`;
|
|
3318
3311
|
}
|
|
3319
3312
|
exec2(cmd, (err) => {
|
|
3320
|
-
if (err)
|
|
3313
|
+
if (err)
|
|
3314
|
+
console.warn(" \u26A0\uFE0F Audio playback error:", err.message.slice(0, 80));
|
|
3321
3315
|
resolve5();
|
|
3322
3316
|
});
|
|
3323
3317
|
});
|
|
@@ -3343,7 +3337,11 @@ async function speak(text, opts = {}) {
|
|
|
3343
3337
|
}
|
|
3344
3338
|
function getTTSInfo() {
|
|
3345
3339
|
if (isKokoroReady()) {
|
|
3346
|
-
return {
|
|
3340
|
+
return {
|
|
3341
|
+
engine: "kokoro",
|
|
3342
|
+
voice: DEFAULT_KOKORO_VOICE,
|
|
3343
|
+
model: KOKORO_MODEL
|
|
3344
|
+
};
|
|
3347
3345
|
}
|
|
3348
3346
|
if (process.platform === "darwin") {
|
|
3349
3347
|
return { engine: "native", voice: getBestMacVoice() };
|
|
@@ -3410,27 +3408,27 @@ function runAppleScript(script) {
|
|
|
3410
3408
|
return new Promise((resolve5, reject) => {
|
|
3411
3409
|
const tmpPath = `/tmp/pulso-as-${Date.now()}-${Math.random().toString(36).slice(2, 6)}.scpt`;
|
|
3412
3410
|
writeFileSync3(tmpPath, script, "utf-8");
|
|
3413
|
-
exec3(
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
try {
|
|
3418
|
-
unlinkSync3(tmpPath);
|
|
3419
|
-
} catch {
|
|
3420
|
-
}
|
|
3421
|
-
if (err) reject(new Error(stderr || err.message));
|
|
3422
|
-
else resolve5(stdout.trim());
|
|
3411
|
+
exec3(`osascript ${tmpPath}`, { timeout: 15e3 }, (err, stdout, stderr) => {
|
|
3412
|
+
try {
|
|
3413
|
+
unlinkSync3(tmpPath);
|
|
3414
|
+
} catch {
|
|
3423
3415
|
}
|
|
3424
|
-
|
|
3416
|
+
if (err) reject(new Error(stderr || err.message));
|
|
3417
|
+
else resolve5(stdout.trim());
|
|
3418
|
+
});
|
|
3425
3419
|
});
|
|
3426
3420
|
}
|
|
3427
3421
|
function runShell2(cmd, timeout = 1e4) {
|
|
3428
3422
|
return new Promise((resolve5, reject) => {
|
|
3429
3423
|
const shell = process.env.SHELL || "/bin/zsh";
|
|
3430
|
-
exec3(
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
|
|
3424
|
+
exec3(
|
|
3425
|
+
cmd,
|
|
3426
|
+
{ timeout, shell, env: { ...process.env, PATH: augmentedPath() } },
|
|
3427
|
+
(err, stdout, stderr) => {
|
|
3428
|
+
if (err) reject(new Error(stderr || err.message));
|
|
3429
|
+
else resolve5(stdout.trim());
|
|
3430
|
+
}
|
|
3431
|
+
);
|
|
3434
3432
|
});
|
|
3435
3433
|
}
|
|
3436
3434
|
function runSwift(code, timeout = 1e4) {
|
|
@@ -3443,6 +3441,21 @@ function runSwift(code, timeout = 1e4) {
|
|
|
3443
3441
|
child.stdin?.end();
|
|
3444
3442
|
});
|
|
3445
3443
|
}
|
|
3444
|
+
async function hasScreenRecordingPermission() {
|
|
3445
|
+
try {
|
|
3446
|
+
const out = await runSwift(
|
|
3447
|
+
`
|
|
3448
|
+
import Cocoa
|
|
3449
|
+
import CoreGraphics
|
|
3450
|
+
print(CGPreflightScreenCaptureAccess() ? "granted" : "denied")
|
|
3451
|
+
`,
|
|
3452
|
+
6e3
|
|
3453
|
+
);
|
|
3454
|
+
return out.trim().toLowerCase() === "granted";
|
|
3455
|
+
} catch {
|
|
3456
|
+
return false;
|
|
3457
|
+
}
|
|
3458
|
+
}
|
|
3446
3459
|
function safePath2(relative) {
|
|
3447
3460
|
const full = resolve2(HOME2, relative);
|
|
3448
3461
|
if (!full.startsWith(HOME2)) return null;
|
|
@@ -3450,7 +3463,8 @@ function safePath2(relative) {
|
|
|
3450
3463
|
if (accessLevel === "full") return full;
|
|
3451
3464
|
const relFromHome = full.slice(HOME2.length + 1);
|
|
3452
3465
|
const topDir = relFromHome.split("/")[0];
|
|
3453
|
-
if (!topDir || !SAFE_DIRS2.some((d) => topDir.toLowerCase() === d.toLowerCase()))
|
|
3466
|
+
if (!topDir || !SAFE_DIRS2.some((d) => topDir.toLowerCase() === d.toLowerCase()))
|
|
3467
|
+
return null;
|
|
3454
3468
|
return full;
|
|
3455
3469
|
}
|
|
3456
3470
|
var MacOSAdapter = class {
|
|
@@ -3471,7 +3485,10 @@ var MacOSAdapter = class {
|
|
|
3471
3485
|
try {
|
|
3472
3486
|
await runShell2(`open -a "${app}"`);
|
|
3473
3487
|
} catch (e) {
|
|
3474
|
-
return {
|
|
3488
|
+
return {
|
|
3489
|
+
success: false,
|
|
3490
|
+
error: `Failed to open "${app}": ${e.message}`
|
|
3491
|
+
};
|
|
3475
3492
|
}
|
|
3476
3493
|
let launched = false;
|
|
3477
3494
|
for (let i = 0; i < 10; i++) {
|
|
@@ -3541,7 +3558,8 @@ var MacOSAdapter = class {
|
|
|
3541
3558
|
}
|
|
3542
3559
|
}
|
|
3543
3560
|
async notification(title, message) {
|
|
3544
|
-
if (!title || !message)
|
|
3561
|
+
if (!title || !message)
|
|
3562
|
+
return { success: false, error: "Missing title or message" };
|
|
3545
3563
|
await runAppleScript(
|
|
3546
3564
|
`display notification "${message.replace(/"/g, '\\"')}" with title "${title.replace(/"/g, '\\"')}"`
|
|
3547
3565
|
);
|
|
@@ -3558,10 +3576,18 @@ var MacOSAdapter = class {
|
|
|
3558
3576
|
}
|
|
3559
3577
|
async getBrightness() {
|
|
3560
3578
|
try {
|
|
3561
|
-
const raw = await runShell2(
|
|
3579
|
+
const raw = await runShell2(
|
|
3580
|
+
"brightness -l 2>/dev/null | grep brightness | head -1 | awk '{print $NF}'"
|
|
3581
|
+
);
|
|
3562
3582
|
return { success: true, data: { brightness: parseFloat(raw) || 0.5 } };
|
|
3563
3583
|
} catch {
|
|
3564
|
-
return {
|
|
3584
|
+
return {
|
|
3585
|
+
success: true,
|
|
3586
|
+
data: {
|
|
3587
|
+
brightness: "unknown",
|
|
3588
|
+
note: "Install 'brightness' via brew for control"
|
|
3589
|
+
}
|
|
3590
|
+
};
|
|
3565
3591
|
}
|
|
3566
3592
|
}
|
|
3567
3593
|
async setBrightness(level) {
|
|
@@ -3578,7 +3604,10 @@ var MacOSAdapter = class {
|
|
|
3578
3604
|
["os", "sw_vers -productVersion"],
|
|
3579
3605
|
["cpu", "sysctl -n machdep.cpu.brand_string"],
|
|
3580
3606
|
["memory", "vm_stat | head -5"],
|
|
3581
|
-
[
|
|
3607
|
+
[
|
|
3608
|
+
"disk",
|
|
3609
|
+
`df -h / | tail -1 | awk '{print $3 " used / " $2 " total (" $5 " used)"}'`
|
|
3610
|
+
],
|
|
3582
3611
|
["uptime", "uptime | sed 's/.*up /up /' | sed 's/,.*//'"],
|
|
3583
3612
|
["battery", "pmset -g batt | grep -Eo '\\d+%'"],
|
|
3584
3613
|
["wifi", "networksetup -getairportnetwork en0 2>/dev/null | cut -d: -f2"],
|
|
@@ -3613,27 +3642,51 @@ var MacOSAdapter = class {
|
|
|
3613
3642
|
const t = Number(timeout) || 15e3;
|
|
3614
3643
|
try {
|
|
3615
3644
|
const output2 = await runShell2(command, t);
|
|
3616
|
-
return {
|
|
3645
|
+
return {
|
|
3646
|
+
success: true,
|
|
3647
|
+
data: {
|
|
3648
|
+
command,
|
|
3649
|
+
output: output2.slice(0, 1e4),
|
|
3650
|
+
truncated: output2.length > 1e4
|
|
3651
|
+
}
|
|
3652
|
+
};
|
|
3617
3653
|
} catch (err) {
|
|
3618
|
-
return {
|
|
3654
|
+
return {
|
|
3655
|
+
success: false,
|
|
3656
|
+
error: `Shell error: ${err.message.slice(0, 2e3)}`
|
|
3657
|
+
};
|
|
3619
3658
|
}
|
|
3620
3659
|
}
|
|
3621
3660
|
async runShortcut(name, input2) {
|
|
3622
3661
|
if (!name) return { success: false, error: "Missing shortcut name" };
|
|
3623
3662
|
const inputFlag = input2 ? `--input-type text --input "${input2.replace(/"/g, '\\"')}"` : "";
|
|
3624
|
-
const result = await runShell2(
|
|
3625
|
-
|
|
3663
|
+
const result = await runShell2(
|
|
3664
|
+
`shortcuts run "${name.replace(/"/g, '\\"')}" ${inputFlag}`,
|
|
3665
|
+
3e4
|
|
3666
|
+
);
|
|
3667
|
+
return {
|
|
3668
|
+
success: true,
|
|
3669
|
+
data: { shortcut: name, output: result || "Shortcut executed" }
|
|
3670
|
+
};
|
|
3626
3671
|
}
|
|
3627
3672
|
async dnd(enabled) {
|
|
3628
3673
|
if (enabled !== void 0) {
|
|
3629
3674
|
try {
|
|
3630
|
-
await runShell2(
|
|
3675
|
+
await runShell2(
|
|
3676
|
+
`shortcuts run "Toggle Do Not Disturb" 2>/dev/null || osascript -e 'do shell script "defaults write com.apple.ncprefs dnd_prefs -data 0"'`
|
|
3677
|
+
);
|
|
3631
3678
|
return { success: true, data: { dnd: enabled, note: "DND toggled" } };
|
|
3632
3679
|
} catch {
|
|
3633
|
-
return {
|
|
3680
|
+
return {
|
|
3681
|
+
success: true,
|
|
3682
|
+
data: { dnd: enabled, note: "Set DND manually in Control Center" }
|
|
3683
|
+
};
|
|
3634
3684
|
}
|
|
3635
3685
|
}
|
|
3636
|
-
return {
|
|
3686
|
+
return {
|
|
3687
|
+
success: true,
|
|
3688
|
+
data: { note: "Pass enabled: true/false to toggle DND" }
|
|
3689
|
+
};
|
|
3637
3690
|
}
|
|
3638
3691
|
/* ══════════════════════════════════════════════════════════
|
|
3639
3692
|
* Clipboard
|
|
@@ -3651,6 +3704,14 @@ var MacOSAdapter = class {
|
|
|
3651
3704
|
* Screenshots & Computer Use
|
|
3652
3705
|
* ══════════════════════════════════════════════════════════ */
|
|
3653
3706
|
async screenshot() {
|
|
3707
|
+
const allowed = await hasScreenRecordingPermission();
|
|
3708
|
+
if (!allowed) {
|
|
3709
|
+
return {
|
|
3710
|
+
image: "",
|
|
3711
|
+
format: "jpeg",
|
|
3712
|
+
note: "Screen Recording permission is not granted for this Companion binary. Enable it in System Settings -> Privacy & Security -> Screen Recording, then reopen Pulso Companion."
|
|
3713
|
+
};
|
|
3714
|
+
}
|
|
3654
3715
|
const ts = Date.now();
|
|
3655
3716
|
const pngPath = `/tmp/pulso-ss-${ts}.png`;
|
|
3656
3717
|
const jpgPath = `/tmp/pulso-ss-${ts}.jpg`;
|
|
@@ -3659,22 +3720,37 @@ var MacOSAdapter = class {
|
|
|
3659
3720
|
} catch (ssErr) {
|
|
3660
3721
|
const msg = ssErr.message || "";
|
|
3661
3722
|
if (msg.includes("could not create image") || msg.includes("display")) {
|
|
3662
|
-
return {
|
|
3723
|
+
return {
|
|
3724
|
+
image: "",
|
|
3725
|
+
format: "jpeg",
|
|
3726
|
+
note: "Screen Recording permission required."
|
|
3727
|
+
};
|
|
3663
3728
|
}
|
|
3664
3729
|
return { image: "", format: "jpeg", note: `Screenshot failed: ${msg}` };
|
|
3665
3730
|
}
|
|
3666
3731
|
if (!existsSync2(pngPath)) {
|
|
3667
|
-
return {
|
|
3732
|
+
return {
|
|
3733
|
+
image: "",
|
|
3734
|
+
format: "jpeg",
|
|
3735
|
+
note: "Screenshot failed \u2014 Screen Recording permission needed."
|
|
3736
|
+
};
|
|
3668
3737
|
}
|
|
3669
3738
|
try {
|
|
3670
|
-
await runShell2(
|
|
3739
|
+
await runShell2(
|
|
3740
|
+
`sips --resampleWidth 1600 --setProperty format jpeg --setProperty formatOptions 75 ${pngPath} --out ${jpgPath}`,
|
|
3741
|
+
1e4
|
|
3742
|
+
);
|
|
3671
3743
|
} catch {
|
|
3672
3744
|
const buf2 = readFileSync2(pngPath);
|
|
3673
3745
|
try {
|
|
3674
3746
|
unlinkSync3(pngPath);
|
|
3675
3747
|
} catch {
|
|
3676
3748
|
}
|
|
3677
|
-
return {
|
|
3749
|
+
return {
|
|
3750
|
+
image: `data:image/png;base64,${buf2.toString("base64")}`,
|
|
3751
|
+
format: "png",
|
|
3752
|
+
note: "Full screen screenshot (PNG fallback)"
|
|
3753
|
+
};
|
|
3678
3754
|
}
|
|
3679
3755
|
const buf = readFileSync2(jpgPath);
|
|
3680
3756
|
const base64 = buf.toString("base64");
|
|
@@ -3709,7 +3785,8 @@ print("\\(Int(main.frame.width)),\\(Int(main.frame.height))")`);
|
|
|
3709
3785
|
}
|
|
3710
3786
|
async mouseClick(x, y, button) {
|
|
3711
3787
|
const btn = button || "left";
|
|
3712
|
-
if (isNaN(x) || isNaN(y))
|
|
3788
|
+
if (isNaN(x) || isNaN(y))
|
|
3789
|
+
return { success: false, error: "Missing x, y coordinates" };
|
|
3713
3790
|
const mouseType = btn === "right" ? "rightMouseDown" : "leftMouseDown";
|
|
3714
3791
|
const mouseTypeUp = btn === "right" ? "rightMouseUp" : "leftMouseUp";
|
|
3715
3792
|
const mouseButton = btn === "right" ? ".right" : ".left";
|
|
@@ -3726,7 +3803,8 @@ print("clicked")`;
|
|
|
3726
3803
|
return { success: true, data: { clicked: { x, y }, button: btn } };
|
|
3727
3804
|
}
|
|
3728
3805
|
async mouseDoubleClick(x, y) {
|
|
3729
|
-
if (isNaN(x) || isNaN(y))
|
|
3806
|
+
if (isNaN(x) || isNaN(y))
|
|
3807
|
+
return { success: false, error: "Missing x, y coordinates" };
|
|
3730
3808
|
const swift = `
|
|
3731
3809
|
import Cocoa
|
|
3732
3810
|
let p = CGPoint(x: ${x}, y: ${y})
|
|
@@ -3752,7 +3830,8 @@ print("double-clicked")`;
|
|
|
3752
3830
|
async mouseScroll(scrollY, scrollX, x, y) {
|
|
3753
3831
|
const sx = scrollX || 0;
|
|
3754
3832
|
const sy = scrollY || 0;
|
|
3755
|
-
if (!sy && !sx)
|
|
3833
|
+
if (!sy && !sx)
|
|
3834
|
+
return { success: false, error: "Missing scrollY or scrollX" };
|
|
3756
3835
|
const swift = `
|
|
3757
3836
|
import Cocoa
|
|
3758
3837
|
let p = CGPoint(x: ${x || 0}, y: ${y || 0})
|
|
@@ -3763,10 +3842,14 @@ let scroll = CGEvent(scrollWheelEvent2Source: nil, units: .pixel, wheelCount: 2,
|
|
|
3763
3842
|
scroll.post(tap: .cghidEventTap)
|
|
3764
3843
|
print("scrolled")`;
|
|
3765
3844
|
await runSwift(swift);
|
|
3766
|
-
return {
|
|
3845
|
+
return {
|
|
3846
|
+
success: true,
|
|
3847
|
+
data: { scrolled: { x: x || 0, y: y || 0, scrollY: sy, scrollX: sx } }
|
|
3848
|
+
};
|
|
3767
3849
|
}
|
|
3768
3850
|
async mouseMove(x, y) {
|
|
3769
|
-
if (isNaN(x) || isNaN(y))
|
|
3851
|
+
if (isNaN(x) || isNaN(y))
|
|
3852
|
+
return { success: false, error: "Missing x, y coordinates" };
|
|
3770
3853
|
const swift = `
|
|
3771
3854
|
import Cocoa
|
|
3772
3855
|
let p = CGPoint(x: ${x}, y: ${y})
|
|
@@ -3798,7 +3881,12 @@ let u = CGEvent(mouseEventSource: nil, mouseType: .leftMouseUp, mouseCursorPosit
|
|
|
3798
3881
|
u.post(tap: .cghidEventTap)
|
|
3799
3882
|
print("dragged")`;
|
|
3800
3883
|
await runSwift(swift);
|
|
3801
|
-
return {
|
|
3884
|
+
return {
|
|
3885
|
+
success: true,
|
|
3886
|
+
data: {
|
|
3887
|
+
dragged: { from: { x: fromX, y: fromY }, to: { x: toX, y: toY } }
|
|
3888
|
+
}
|
|
3889
|
+
};
|
|
3802
3890
|
}
|
|
3803
3891
|
async getCursorPosition() {
|
|
3804
3892
|
const swift = `
|
|
@@ -3866,12 +3954,19 @@ print("\\(x),\\(y)")`;
|
|
|
3866
3954
|
const keyCode = keyCodeMap[key.toLowerCase()];
|
|
3867
3955
|
if (keyCode !== void 0) {
|
|
3868
3956
|
const using = modStr.length > 0 ? ` using {${modStr.join(", ")}}` : "";
|
|
3869
|
-
await runAppleScript(
|
|
3957
|
+
await runAppleScript(
|
|
3958
|
+
`tell application "System Events" to key code ${keyCode}${using}`
|
|
3959
|
+
);
|
|
3870
3960
|
} else if (key.length === 1) {
|
|
3871
3961
|
const using = modStr.length > 0 ? ` using {${modStr.join(", ")}}` : "";
|
|
3872
|
-
await runAppleScript(
|
|
3962
|
+
await runAppleScript(
|
|
3963
|
+
`tell application "System Events" to keystroke "${key}"${using}`
|
|
3964
|
+
);
|
|
3873
3965
|
} else {
|
|
3874
|
-
return {
|
|
3966
|
+
return {
|
|
3967
|
+
success: false,
|
|
3968
|
+
error: `Unknown key: ${key}. Use single characters or: enter, tab, escape, delete, space, up, down, left, right, f1-f12, home, end, pageup, pagedown`
|
|
3969
|
+
};
|
|
3875
3970
|
}
|
|
3876
3971
|
return { success: true, data: { pressed: key, modifiers: mods } };
|
|
3877
3972
|
}
|
|
@@ -3879,7 +3974,13 @@ print("\\(x),\\(y)")`;
|
|
|
3879
3974
|
* Browser Automation
|
|
3880
3975
|
* ══════════════════════════════════════════════════════════ */
|
|
3881
3976
|
async browserListTabs() {
|
|
3882
|
-
const browsers = [
|
|
3977
|
+
const browsers = [
|
|
3978
|
+
"Google Chrome",
|
|
3979
|
+
"Safari",
|
|
3980
|
+
"Arc",
|
|
3981
|
+
"Firefox",
|
|
3982
|
+
"Microsoft Edge"
|
|
3983
|
+
];
|
|
3883
3984
|
const allTabs = [];
|
|
3884
3985
|
for (const browser of browsers) {
|
|
3885
3986
|
try {
|
|
@@ -3903,7 +4004,12 @@ print("\\(x),\\(y)")`;
|
|
|
3903
4004
|
const tabStr = rest.join("~~~");
|
|
3904
4005
|
const pairs = tabStr.split("|||").filter(Boolean);
|
|
3905
4006
|
for (let i = 0; i < pairs.length - 1; i += 2) {
|
|
3906
|
-
allTabs.push({
|
|
4007
|
+
allTabs.push({
|
|
4008
|
+
browser: "Safari",
|
|
4009
|
+
title: pairs[i],
|
|
4010
|
+
url: pairs[i + 1],
|
|
4011
|
+
active: pairs[i + 1] === activeURL.trim()
|
|
4012
|
+
});
|
|
3907
4013
|
}
|
|
3908
4014
|
} else {
|
|
3909
4015
|
const tabData = await runAppleScript(`
|
|
@@ -3921,7 +4027,12 @@ print("\\(x),\\(y)")`;
|
|
|
3921
4027
|
const tabStr = rest.join("~~~");
|
|
3922
4028
|
const pairs = tabStr.split("|||").filter(Boolean);
|
|
3923
4029
|
for (let i = 0; i < pairs.length - 1; i += 2) {
|
|
3924
|
-
allTabs.push({
|
|
4030
|
+
allTabs.push({
|
|
4031
|
+
browser,
|
|
4032
|
+
title: pairs[i],
|
|
4033
|
+
url: pairs[i + 1],
|
|
4034
|
+
active: pairs[i + 1] === activeURL.trim()
|
|
4035
|
+
});
|
|
3925
4036
|
}
|
|
3926
4037
|
}
|
|
3927
4038
|
} catch {
|
|
@@ -3949,7 +4060,10 @@ print("\\(x),\\(y)")`;
|
|
|
3949
4060
|
}
|
|
3950
4061
|
return { success: true, data: { navigated: url, browser: b } };
|
|
3951
4062
|
} catch (err) {
|
|
3952
|
-
return {
|
|
4063
|
+
return {
|
|
4064
|
+
success: false,
|
|
4065
|
+
error: `Failed to navigate: ${err.message}`
|
|
4066
|
+
};
|
|
3953
4067
|
}
|
|
3954
4068
|
}
|
|
3955
4069
|
async browserNewTab(url, browser) {
|
|
@@ -3972,7 +4086,10 @@ print("\\(x),\\(y)")`;
|
|
|
3972
4086
|
}
|
|
3973
4087
|
return { success: true, data: { opened: url, browser: b } };
|
|
3974
4088
|
} catch (err) {
|
|
3975
|
-
return {
|
|
4089
|
+
return {
|
|
4090
|
+
success: false,
|
|
4091
|
+
error: `Failed to open window: ${err.message}`
|
|
4092
|
+
};
|
|
3976
4093
|
}
|
|
3977
4094
|
}
|
|
3978
4095
|
async browserReadPage(browser, maxLength) {
|
|
@@ -3998,11 +4115,17 @@ print("\\(x),\\(y)")`;
|
|
|
3998
4115
|
} catch {
|
|
3999
4116
|
try {
|
|
4000
4117
|
const savedClipboard = await runShell2("pbpaste 2>/dev/null || true");
|
|
4001
|
-
await runAppleScript(
|
|
4118
|
+
await runAppleScript(
|
|
4119
|
+
`tell application "${b.replace(/"/g, '\\"')}" to activate`
|
|
4120
|
+
);
|
|
4002
4121
|
await new Promise((r) => setTimeout(r, 300));
|
|
4003
|
-
await runAppleScript(
|
|
4122
|
+
await runAppleScript(
|
|
4123
|
+
'tell application "System Events" to keystroke "a" using command down'
|
|
4124
|
+
);
|
|
4004
4125
|
await new Promise((r) => setTimeout(r, 200));
|
|
4005
|
-
await runAppleScript(
|
|
4126
|
+
await runAppleScript(
|
|
4127
|
+
'tell application "System Events" to keystroke "c" using command down'
|
|
4128
|
+
);
|
|
4006
4129
|
await new Promise((r) => setTimeout(r, 300));
|
|
4007
4130
|
content = await runShell2("pbpaste");
|
|
4008
4131
|
method = "clipboard";
|
|
@@ -4011,18 +4134,29 @@ print("\\(x),\\(y)")`;
|
|
|
4011
4134
|
execSync2(`echo ${JSON.stringify(savedClipboard)} | pbcopy`);
|
|
4012
4135
|
}
|
|
4013
4136
|
} catch (clipErr) {
|
|
4014
|
-
return {
|
|
4137
|
+
return {
|
|
4138
|
+
success: false,
|
|
4139
|
+
error: `Could not read page: ${clipErr.message}`
|
|
4140
|
+
};
|
|
4015
4141
|
}
|
|
4016
4142
|
}
|
|
4017
4143
|
let pageUrl = "";
|
|
4018
4144
|
let pageTitle = "";
|
|
4019
4145
|
try {
|
|
4020
4146
|
if (b === "Safari") {
|
|
4021
|
-
pageUrl = await runAppleScript(
|
|
4022
|
-
|
|
4147
|
+
pageUrl = await runAppleScript(
|
|
4148
|
+
'tell application "Safari" to return URL of front document'
|
|
4149
|
+
);
|
|
4150
|
+
pageTitle = await runAppleScript(
|
|
4151
|
+
'tell application "Safari" to return name of front document'
|
|
4152
|
+
);
|
|
4023
4153
|
} else {
|
|
4024
|
-
pageUrl = await runAppleScript(
|
|
4025
|
-
|
|
4154
|
+
pageUrl = await runAppleScript(
|
|
4155
|
+
`tell application "${b.replace(/"/g, '\\"')}" to return URL of active tab of front window`
|
|
4156
|
+
);
|
|
4157
|
+
pageTitle = await runAppleScript(
|
|
4158
|
+
`tell application "${b.replace(/"/g, '\\"')}" to return title of active tab of front window`
|
|
4159
|
+
);
|
|
4026
4160
|
}
|
|
4027
4161
|
} catch {
|
|
4028
4162
|
}
|
|
@@ -4062,44 +4196,170 @@ end tell`);
|
|
|
4062
4196
|
}
|
|
4063
4197
|
return { success: true, data: { result: (result || "").slice(0, 5e3) } };
|
|
4064
4198
|
} catch (err) {
|
|
4065
|
-
return {
|
|
4066
|
-
|
|
4067
|
-
|
|
4068
|
-
|
|
4069
|
-
* Productivity: Calendar
|
|
4070
|
-
* ══════════════════════════════════════════════════════════ */
|
|
4071
|
-
async calendarList(days) {
|
|
4072
|
-
const d = days || 7;
|
|
4073
|
-
try {
|
|
4074
|
-
await runAppleScript('tell application "Calendar" to launch');
|
|
4075
|
-
await new Promise((r) => setTimeout(r, 500));
|
|
4076
|
-
} catch {
|
|
4199
|
+
return {
|
|
4200
|
+
success: false,
|
|
4201
|
+
error: `JS execution failed: ${err.message}`
|
|
4202
|
+
};
|
|
4077
4203
|
}
|
|
4078
|
-
const script = `
|
|
4079
|
-
set output to ""
|
|
4080
|
-
tell application "Calendar"
|
|
4081
|
-
repeat with cal in calendars
|
|
4082
|
-
set calName to name of cal
|
|
4083
|
-
set evts to (every event of cal whose start date >= (current date) and start date < ((current date) + ${d} * days))
|
|
4084
|
-
repeat with e in evts
|
|
4085
|
-
set output to output & calName & " | " & summary of e & " | " & (start date of e as string) & " | " & (end date of e as string) & linefeed
|
|
4086
|
-
end repeat
|
|
4087
|
-
end repeat
|
|
4088
|
-
end tell
|
|
4089
|
-
return output`;
|
|
4090
|
-
const raw = await runAppleScript(script);
|
|
4091
|
-
return raw.split("\n").filter(Boolean).map((line) => {
|
|
4092
|
-
const [cal, summary, start, end] = line.split(" | ");
|
|
4093
|
-
return { calendar: cal?.trim(), title: summary?.trim() || "", startDate: start?.trim() || "", endDate: end?.trim() };
|
|
4094
|
-
});
|
|
4095
4204
|
}
|
|
4096
|
-
async
|
|
4097
|
-
|
|
4098
|
-
const
|
|
4099
|
-
|
|
4100
|
-
|
|
4101
|
-
|
|
4102
|
-
|
|
4205
|
+
async browserListProfiles() {
|
|
4206
|
+
const os = homedir2();
|
|
4207
|
+
const browserPaths = [
|
|
4208
|
+
{
|
|
4209
|
+
browser: "Google Chrome",
|
|
4210
|
+
dir: `${os}/Library/Application Support/Google/Chrome`
|
|
4211
|
+
},
|
|
4212
|
+
{
|
|
4213
|
+
browser: "Google Chrome Beta",
|
|
4214
|
+
dir: `${os}/Library/Application Support/Google/Chrome Beta`
|
|
4215
|
+
},
|
|
4216
|
+
{
|
|
4217
|
+
browser: "Google Chrome Dev",
|
|
4218
|
+
dir: `${os}/Library/Application Support/Google/Chrome Dev`
|
|
4219
|
+
},
|
|
4220
|
+
{
|
|
4221
|
+
browser: "Microsoft Edge",
|
|
4222
|
+
dir: `${os}/Library/Application Support/Microsoft Edge`
|
|
4223
|
+
},
|
|
4224
|
+
{
|
|
4225
|
+
browser: "Brave Browser",
|
|
4226
|
+
dir: `${os}/Library/Application Support/BraveSoftware/Brave-Browser`
|
|
4227
|
+
},
|
|
4228
|
+
{
|
|
4229
|
+
browser: "Opera",
|
|
4230
|
+
dir: `${os}/Library/Application Support/com.operasoftware.Opera`
|
|
4231
|
+
},
|
|
4232
|
+
{ browser: "Vivaldi", dir: `${os}/Library/Application Support/Vivaldi` },
|
|
4233
|
+
{
|
|
4234
|
+
browser: "Arc",
|
|
4235
|
+
dir: `${os}/Library/Application Support/Arc/User Data`
|
|
4236
|
+
},
|
|
4237
|
+
{
|
|
4238
|
+
browser: "Chromium",
|
|
4239
|
+
dir: `${os}/Library/Application Support/Chromium`
|
|
4240
|
+
}
|
|
4241
|
+
];
|
|
4242
|
+
const profiles = [];
|
|
4243
|
+
for (const { browser, dir } of browserPaths) {
|
|
4244
|
+
if (!existsSync2(dir)) continue;
|
|
4245
|
+
let dirs = [];
|
|
4246
|
+
try {
|
|
4247
|
+
dirs = readdirSync2(dir);
|
|
4248
|
+
} catch {
|
|
4249
|
+
continue;
|
|
4250
|
+
}
|
|
4251
|
+
for (const entry of dirs) {
|
|
4252
|
+
if (entry !== "Default" && !entry.startsWith("Profile ")) continue;
|
|
4253
|
+
const prefsPath = `${dir}/${entry}/Preferences`;
|
|
4254
|
+
if (!existsSync2(prefsPath)) continue;
|
|
4255
|
+
try {
|
|
4256
|
+
const raw = readFileSync2(prefsPath, "utf-8");
|
|
4257
|
+
const prefs = JSON.parse(raw);
|
|
4258
|
+
const name = prefs.profile?.name || entry;
|
|
4259
|
+
const email = prefs.account_info?.[0]?.email;
|
|
4260
|
+
profiles.push({
|
|
4261
|
+
browser,
|
|
4262
|
+
profileDir: `${dir}/${entry}`,
|
|
4263
|
+
name,
|
|
4264
|
+
email,
|
|
4265
|
+
isDefault: entry === "Default"
|
|
4266
|
+
});
|
|
4267
|
+
} catch {
|
|
4268
|
+
}
|
|
4269
|
+
}
|
|
4270
|
+
}
|
|
4271
|
+
const firefoxPaths = [
|
|
4272
|
+
`${os}/Library/Application Support/Firefox/Profiles`,
|
|
4273
|
+
`${os}/Library/Application Support/Firefox Developer Edition/Profiles`
|
|
4274
|
+
];
|
|
4275
|
+
for (const ffDir of firefoxPaths) {
|
|
4276
|
+
if (!existsSync2(ffDir)) continue;
|
|
4277
|
+
try {
|
|
4278
|
+
const dirs = readdirSync2(ffDir);
|
|
4279
|
+
const browserName = ffDir.includes("Developer") ? "Firefox Developer Edition" : "Firefox";
|
|
4280
|
+
for (const entry of dirs) {
|
|
4281
|
+
const userJs = `${ffDir}/${entry}/user.js`;
|
|
4282
|
+
const prefsJs = `${ffDir}/${entry}/prefs.js`;
|
|
4283
|
+
if (!existsSync2(prefsJs) && !existsSync2(userJs)) continue;
|
|
4284
|
+
profiles.push({
|
|
4285
|
+
browser: browserName,
|
|
4286
|
+
profileDir: `${ffDir}/${entry}`,
|
|
4287
|
+
name: entry.replace(/^[a-z0-9]+\./, ""),
|
|
4288
|
+
// strip hash prefix
|
|
4289
|
+
isDefault: entry.includes("default")
|
|
4290
|
+
});
|
|
4291
|
+
}
|
|
4292
|
+
} catch {
|
|
4293
|
+
}
|
|
4294
|
+
}
|
|
4295
|
+
let running = [];
|
|
4296
|
+
try {
|
|
4297
|
+
const ps = await runShell2(
|
|
4298
|
+
"ps aux | grep -E '(Chrome|Edge|Brave|Firefox|Opera|Vivaldi|Arc)' | grep -v grep | awk '{print $11}'"
|
|
4299
|
+
);
|
|
4300
|
+
running = ps.split("\n").filter(Boolean);
|
|
4301
|
+
} catch {
|
|
4302
|
+
}
|
|
4303
|
+
return {
|
|
4304
|
+
success: true,
|
|
4305
|
+
data: {
|
|
4306
|
+
profiles: profiles.map((p) => ({
|
|
4307
|
+
...p,
|
|
4308
|
+
isRunning: running.some(
|
|
4309
|
+
(r) => r.toLowerCase().includes(p.browser.toLowerCase().replace(/ /g, ""))
|
|
4310
|
+
)
|
|
4311
|
+
})),
|
|
4312
|
+
total: profiles.length
|
|
4313
|
+
}
|
|
4314
|
+
};
|
|
4315
|
+
}
|
|
4316
|
+
/* ══════════════════════════════════════════════════════════
|
|
4317
|
+
* Productivity: Calendar
|
|
4318
|
+
* ══════════════════════════════════════════════════════════ */
|
|
4319
|
+
async calendarList(days) {
|
|
4320
|
+
const d = days || 7;
|
|
4321
|
+
try {
|
|
4322
|
+
await runAppleScript('tell application "Calendar" to launch');
|
|
4323
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
4324
|
+
} catch {
|
|
4325
|
+
}
|
|
4326
|
+
const script = `
|
|
4327
|
+
set output to ""
|
|
4328
|
+
tell application "Calendar"
|
|
4329
|
+
repeat with cal in calendars
|
|
4330
|
+
set calName to name of cal
|
|
4331
|
+
set evts to (every event of cal whose start date >= (current date) and start date < ((current date) + ${d} * days))
|
|
4332
|
+
repeat with e in evts
|
|
4333
|
+
set output to output & calName & " | " & summary of e & " | " & (start date of e as string) & " | " & (end date of e as string) & linefeed
|
|
4334
|
+
end repeat
|
|
4335
|
+
end repeat
|
|
4336
|
+
end tell
|
|
4337
|
+
return output`;
|
|
4338
|
+
const raw = await runAppleScript(script);
|
|
4339
|
+
return raw.split("\n").filter(Boolean).map((line) => {
|
|
4340
|
+
const [cal, summary, start, end] = line.split(" | ");
|
|
4341
|
+
return {
|
|
4342
|
+
calendar: cal?.trim(),
|
|
4343
|
+
title: summary?.trim() || "",
|
|
4344
|
+
startDate: start?.trim() || "",
|
|
4345
|
+
endDate: end?.trim()
|
|
4346
|
+
};
|
|
4347
|
+
});
|
|
4348
|
+
}
|
|
4349
|
+
async calendarCreate(title, startDate, endDate, calendar, notes) {
|
|
4350
|
+
if (!title || !startDate)
|
|
4351
|
+
return { success: false, error: "Missing title or start date" };
|
|
4352
|
+
const parseDate = (iso) => {
|
|
4353
|
+
const d = new Date(iso);
|
|
4354
|
+
if (isNaN(d.getTime())) return null;
|
|
4355
|
+
return {
|
|
4356
|
+
y: d.getFullYear(),
|
|
4357
|
+
mo: d.getMonth() + 1,
|
|
4358
|
+
d: d.getDate(),
|
|
4359
|
+
h: d.getHours(),
|
|
4360
|
+
mi: d.getMinutes()
|
|
4361
|
+
};
|
|
4362
|
+
};
|
|
4103
4363
|
const buildDateScript = (varName, iso) => {
|
|
4104
4364
|
const p = parseDate(iso);
|
|
4105
4365
|
if (!p) return "";
|
|
@@ -4114,7 +4374,8 @@ tell ${varName}
|
|
|
4114
4374
|
end tell`;
|
|
4115
4375
|
};
|
|
4116
4376
|
const startDateScript = buildDateScript("startD", startDate);
|
|
4117
|
-
if (!startDateScript)
|
|
4377
|
+
if (!startDateScript)
|
|
4378
|
+
return { success: false, error: `Invalid start date: ${startDate}` };
|
|
4118
4379
|
const endDateScript = endDate ? buildDateScript("endD", endDate) : "";
|
|
4119
4380
|
const calTarget = calendar ? `calendar "${calendar.replace(/"/g, '\\"')}"` : "default calendar";
|
|
4120
4381
|
const notesPart = notes ? `
|
|
@@ -4133,7 +4394,10 @@ tell application "Calendar"
|
|
|
4133
4394
|
set newEvent to make new event with properties {summary:"${title.replace(/"/g, '\\"')}", start date:startD}${endPart}${notesPart}
|
|
4134
4395
|
end tell
|
|
4135
4396
|
end tell`);
|
|
4136
|
-
return {
|
|
4397
|
+
return {
|
|
4398
|
+
success: true,
|
|
4399
|
+
data: { created: title, start: startDate, end: endDate || "1 hour" }
|
|
4400
|
+
};
|
|
4137
4401
|
}
|
|
4138
4402
|
/* ══════════════════════════════════════════════════════════
|
|
4139
4403
|
* Productivity: Reminders
|
|
@@ -4158,11 +4422,22 @@ end tell`);
|
|
|
4158
4422
|
const raw = await runAppleScript(script);
|
|
4159
4423
|
const reminders = raw.split("\n").filter(Boolean).map((line) => {
|
|
4160
4424
|
const parts = line.split(" | ");
|
|
4161
|
-
return parts.length === 3 ? {
|
|
4425
|
+
return parts.length === 3 ? {
|
|
4426
|
+
list: parts[0]?.trim(),
|
|
4427
|
+
name: parts[1]?.trim(),
|
|
4428
|
+
due: parts[2]?.trim()
|
|
4429
|
+
} : { name: parts[0]?.trim(), due: parts[1]?.trim() };
|
|
4162
4430
|
});
|
|
4163
4431
|
return { success: true, data: { reminders, count: reminders.length } };
|
|
4164
4432
|
} catch {
|
|
4165
|
-
return {
|
|
4433
|
+
return {
|
|
4434
|
+
success: true,
|
|
4435
|
+
data: {
|
|
4436
|
+
reminders: [],
|
|
4437
|
+
count: 0,
|
|
4438
|
+
note: "No reminders or Reminders app not accessible"
|
|
4439
|
+
}
|
|
4440
|
+
};
|
|
4166
4441
|
}
|
|
4167
4442
|
}
|
|
4168
4443
|
async reminderCreate(title, list, dueDate) {
|
|
@@ -4170,7 +4445,9 @@ end tell`);
|
|
|
4170
4445
|
let listName = list || "";
|
|
4171
4446
|
if (!listName) {
|
|
4172
4447
|
try {
|
|
4173
|
-
listName = (await runAppleScript(
|
|
4448
|
+
listName = (await runAppleScript(
|
|
4449
|
+
'tell application "Reminders" to return name of default list'
|
|
4450
|
+
)).trim();
|
|
4174
4451
|
} catch {
|
|
4175
4452
|
listName = "Reminders";
|
|
4176
4453
|
}
|
|
@@ -4196,20 +4473,27 @@ tell application "Reminders"
|
|
|
4196
4473
|
make new reminder with properties {name:"${title.replace(/"/g, '\\"')}"${dueProperty}}
|
|
4197
4474
|
end tell
|
|
4198
4475
|
end tell`);
|
|
4199
|
-
return {
|
|
4476
|
+
return {
|
|
4477
|
+
success: true,
|
|
4478
|
+
data: { created: title, due: dueDate || "none", list: listName }
|
|
4479
|
+
};
|
|
4200
4480
|
}
|
|
4201
4481
|
/* ══════════════════════════════════════════════════════════
|
|
4202
4482
|
* Productivity: Messages
|
|
4203
4483
|
* ══════════════════════════════════════════════════════════ */
|
|
4204
4484
|
async sendMessage(to, message) {
|
|
4205
|
-
if (!to || !message)
|
|
4485
|
+
if (!to || !message)
|
|
4486
|
+
return { success: false, error: "Missing 'to' or 'message'" };
|
|
4206
4487
|
await runAppleScript(`
|
|
4207
4488
|
tell application "Messages"
|
|
4208
4489
|
set targetService to 1st account whose service type = iMessage
|
|
4209
4490
|
set targetBuddy to participant "${to.replace(/"/g, '\\"')}" of targetService
|
|
4210
4491
|
send "${message.replace(/"/g, '\\"')}" to targetBuddy
|
|
4211
4492
|
end tell`);
|
|
4212
|
-
return {
|
|
4493
|
+
return {
|
|
4494
|
+
success: true,
|
|
4495
|
+
data: { sent: true, to, message: message.slice(0, 100) }
|
|
4496
|
+
};
|
|
4213
4497
|
}
|
|
4214
4498
|
/* ══════════════════════════════════════════════════════════
|
|
4215
4499
|
* Productivity: Contacts
|
|
@@ -4236,7 +4520,11 @@ end tell`);
|
|
|
4236
4520
|
end tell`);
|
|
4237
4521
|
return raw.split("\n").filter(Boolean).map((line) => {
|
|
4238
4522
|
const [name, email, phone] = line.split(" | ");
|
|
4239
|
-
return {
|
|
4523
|
+
return {
|
|
4524
|
+
name: name?.trim() || "",
|
|
4525
|
+
email: email?.trim(),
|
|
4526
|
+
phone: phone?.trim()
|
|
4527
|
+
};
|
|
4240
4528
|
});
|
|
4241
4529
|
}
|
|
4242
4530
|
/* ══════════════════════════════════════════════════════════
|
|
@@ -4273,11 +4561,15 @@ end tell`);
|
|
|
4273
4561
|
* Email
|
|
4274
4562
|
* ══════════════════════════════════════════════════════════ */
|
|
4275
4563
|
async emailSend(to, subject, body, method) {
|
|
4276
|
-
if (!to || !subject || !body)
|
|
4564
|
+
if (!to || !subject || !body)
|
|
4565
|
+
return { success: false, error: "Missing to, subject, or body" };
|
|
4277
4566
|
if (method === "gmail") {
|
|
4278
4567
|
const gmailUrl = `https://mail.google.com/mail/u/0/?view=cm&fs=1&to=${encodeURIComponent(to)}&su=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`;
|
|
4279
4568
|
await runShell2(`open "${gmailUrl}"`);
|
|
4280
|
-
return {
|
|
4569
|
+
return {
|
|
4570
|
+
success: true,
|
|
4571
|
+
data: { method: "gmail", to, subject, note: "Gmail compose opened." }
|
|
4572
|
+
};
|
|
4281
4573
|
}
|
|
4282
4574
|
try {
|
|
4283
4575
|
await runAppleScript(`
|
|
@@ -4288,11 +4580,22 @@ end tell`);
|
|
|
4288
4580
|
end tell
|
|
4289
4581
|
send newMessage
|
|
4290
4582
|
end tell`);
|
|
4291
|
-
return {
|
|
4583
|
+
return {
|
|
4584
|
+
success: true,
|
|
4585
|
+
data: { method: "mail", to, subject, sent: true }
|
|
4586
|
+
};
|
|
4292
4587
|
} catch (err) {
|
|
4293
4588
|
const gmailUrl = `https://mail.google.com/mail/u/0/?view=cm&fs=1&to=${encodeURIComponent(to)}&su=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`;
|
|
4294
4589
|
await runShell2(`open "${gmailUrl}"`);
|
|
4295
|
-
return {
|
|
4590
|
+
return {
|
|
4591
|
+
success: true,
|
|
4592
|
+
data: {
|
|
4593
|
+
method: "gmail_fallback",
|
|
4594
|
+
to,
|
|
4595
|
+
subject,
|
|
4596
|
+
note: `Mail.app failed (${err.message}). Gmail opened instead.`
|
|
4597
|
+
}
|
|
4598
|
+
};
|
|
4296
4599
|
}
|
|
4297
4600
|
}
|
|
4298
4601
|
/* ══════════════════════════════════════════════════════════
|
|
@@ -4301,15 +4604,33 @@ end tell`);
|
|
|
4301
4604
|
async fileRead(path) {
|
|
4302
4605
|
if (!path) return { success: false, error: "Missing file path" };
|
|
4303
4606
|
const fullPath = safePath2(path);
|
|
4304
|
-
if (!fullPath)
|
|
4305
|
-
|
|
4607
|
+
if (!fullPath)
|
|
4608
|
+
return {
|
|
4609
|
+
success: false,
|
|
4610
|
+
error: `Access denied. Only files in ${SAFE_DIRS2.join(", ")} are allowed.`
|
|
4611
|
+
};
|
|
4612
|
+
if (!existsSync2(fullPath))
|
|
4613
|
+
return { success: false, error: `File not found: ${path}` };
|
|
4306
4614
|
const content = readFileSync2(fullPath, "utf-8");
|
|
4307
|
-
return {
|
|
4615
|
+
return {
|
|
4616
|
+
success: true,
|
|
4617
|
+
data: {
|
|
4618
|
+
path,
|
|
4619
|
+
content: content.slice(0, 1e4),
|
|
4620
|
+
size: content.length,
|
|
4621
|
+
truncated: content.length > 1e4
|
|
4622
|
+
}
|
|
4623
|
+
};
|
|
4308
4624
|
}
|
|
4309
4625
|
async fileWrite(path, content) {
|
|
4310
|
-
if (!path || !content)
|
|
4626
|
+
if (!path || !content)
|
|
4627
|
+
return { success: false, error: "Missing path or content" };
|
|
4311
4628
|
const fullPath = safePath2(path);
|
|
4312
|
-
if (!fullPath)
|
|
4629
|
+
if (!fullPath)
|
|
4630
|
+
return {
|
|
4631
|
+
success: false,
|
|
4632
|
+
error: `Access denied. Only files in ${SAFE_DIRS2.join(", ")} are allowed.`
|
|
4633
|
+
};
|
|
4313
4634
|
writeFileSync3(fullPath, content, "utf-8");
|
|
4314
4635
|
return { success: true, data: { path, written: content.length } };
|
|
4315
4636
|
}
|
|
@@ -4317,32 +4638,45 @@ end tell`);
|
|
|
4317
4638
|
const dirPath = path || "Desktop";
|
|
4318
4639
|
const fullDir = safePath2(dirPath);
|
|
4319
4640
|
if (!fullDir) return { success: false, error: `Access denied: ${dirPath}` };
|
|
4320
|
-
if (!existsSync2(fullDir))
|
|
4641
|
+
if (!existsSync2(fullDir))
|
|
4642
|
+
return { success: false, error: `Directory not found: ${dirPath}` };
|
|
4321
4643
|
const entries = readdirSync2(fullDir).map((name) => {
|
|
4322
4644
|
try {
|
|
4323
4645
|
const st = statSync2(join3(fullDir, name));
|
|
4324
|
-
return {
|
|
4646
|
+
return {
|
|
4647
|
+
name,
|
|
4648
|
+
type: st.isDirectory() ? "dir" : "file",
|
|
4649
|
+
size: st.size,
|
|
4650
|
+
modified: st.mtime.toISOString()
|
|
4651
|
+
};
|
|
4325
4652
|
} catch {
|
|
4326
4653
|
return { name, type: "unknown", size: 0, modified: "" };
|
|
4327
4654
|
}
|
|
4328
4655
|
});
|
|
4329
|
-
return {
|
|
4656
|
+
return {
|
|
4657
|
+
success: true,
|
|
4658
|
+
data: { path: dirPath, entries, count: entries.length }
|
|
4659
|
+
};
|
|
4330
4660
|
}
|
|
4331
4661
|
async fileMove(source, destination) {
|
|
4332
|
-
if (!source || !destination)
|
|
4662
|
+
if (!source || !destination)
|
|
4663
|
+
return { success: false, error: "Missing from/to paths" };
|
|
4333
4664
|
const fullSrc = safePath2(source);
|
|
4334
4665
|
const fullDst = safePath2(destination);
|
|
4335
4666
|
if (!fullSrc || !fullDst) return { success: false, error: "Access denied" };
|
|
4336
|
-
if (!existsSync2(fullSrc))
|
|
4667
|
+
if (!existsSync2(fullSrc))
|
|
4668
|
+
return { success: false, error: `Source not found: ${source}` };
|
|
4337
4669
|
renameSync2(fullSrc, fullDst);
|
|
4338
4670
|
return { success: true, data: { moved: source, to: destination } };
|
|
4339
4671
|
}
|
|
4340
4672
|
async fileCopy(source, destination) {
|
|
4341
|
-
if (!source || !destination)
|
|
4673
|
+
if (!source || !destination)
|
|
4674
|
+
return { success: false, error: "Missing from/to paths" };
|
|
4342
4675
|
const fullSrc = safePath2(source);
|
|
4343
4676
|
const fullDst = safePath2(destination);
|
|
4344
4677
|
if (!fullSrc || !fullDst) return { success: false, error: "Access denied" };
|
|
4345
|
-
if (!existsSync2(fullSrc))
|
|
4678
|
+
if (!existsSync2(fullSrc))
|
|
4679
|
+
return { success: false, error: `Source not found: ${source}` };
|
|
4346
4680
|
copyFileSync2(fullSrc, fullDst);
|
|
4347
4681
|
return { success: true, data: { copied: source, to: destination } };
|
|
4348
4682
|
}
|
|
@@ -4350,15 +4684,19 @@ end tell`);
|
|
|
4350
4684
|
if (!path) return { success: false, error: "Missing path" };
|
|
4351
4685
|
const fullTarget = safePath2(path);
|
|
4352
4686
|
if (!fullTarget) return { success: false, error: "Access denied" };
|
|
4353
|
-
if (!existsSync2(fullTarget))
|
|
4354
|
-
|
|
4687
|
+
if (!existsSync2(fullTarget))
|
|
4688
|
+
return { success: false, error: `Not found: ${path}` };
|
|
4689
|
+
await runShell2(
|
|
4690
|
+
`osascript -e 'tell application "Finder" to delete POSIX file "${fullTarget}"'`
|
|
4691
|
+
);
|
|
4355
4692
|
return { success: true, data: { deleted: path, method: "moved_to_trash" } };
|
|
4356
4693
|
}
|
|
4357
4694
|
async fileInfo(path) {
|
|
4358
4695
|
if (!path) return { success: false, error: "Missing path" };
|
|
4359
4696
|
const fullF = safePath2(path);
|
|
4360
4697
|
if (!fullF) return { success: false, error: "Access denied" };
|
|
4361
|
-
if (!existsSync2(fullF))
|
|
4698
|
+
if (!existsSync2(fullF))
|
|
4699
|
+
return { success: false, error: `Not found: ${path}` };
|
|
4362
4700
|
const st = statSync2(fullF);
|
|
4363
4701
|
return {
|
|
4364
4702
|
success: true,
|
|
@@ -4379,7 +4717,10 @@ end tell`);
|
|
|
4379
4717
|
const dlDest = destination || `Downloads/${basename2(new URL(url).pathname) || "download"}`;
|
|
4380
4718
|
const fullDl = safePath2(dlDest);
|
|
4381
4719
|
if (!fullDl) return { success: false, error: "Access denied" };
|
|
4382
|
-
await runShell2(
|
|
4720
|
+
await runShell2(
|
|
4721
|
+
`curl -sL -o "${fullDl}" "${url.replace(/"/g, '\\"')}"`,
|
|
4722
|
+
6e4
|
|
4723
|
+
);
|
|
4383
4724
|
const size = existsSync2(fullDl) ? statSync2(fullDl).size : 0;
|
|
4384
4725
|
return { success: true, data: { downloaded: url, saved: dlDest, size } };
|
|
4385
4726
|
}
|
|
@@ -4416,14 +4757,20 @@ end tell`);
|
|
|
4416
4757
|
}
|
|
4417
4758
|
async windowFocus(app) {
|
|
4418
4759
|
if (!app) return { success: false, error: "Missing app name" };
|
|
4419
|
-
await runAppleScript(
|
|
4760
|
+
await runAppleScript(
|
|
4761
|
+
`tell application "${app.replace(/"/g, '\\"')}" to activate`
|
|
4762
|
+
);
|
|
4420
4763
|
return { success: true, data: { focused: app } };
|
|
4421
4764
|
}
|
|
4422
4765
|
async windowResize(app, x, y, width, height) {
|
|
4423
4766
|
if (!app) return { success: false, error: "Missing app name" };
|
|
4424
4767
|
const posPart = x !== void 0 && y !== void 0 ? `set position of window 1 to {${x}, ${y}}` : "";
|
|
4425
4768
|
const sizePart = width !== void 0 && height !== void 0 ? `set size of window 1 to {${width}, ${height}}` : "";
|
|
4426
|
-
if (!posPart && !sizePart)
|
|
4769
|
+
if (!posPart && !sizePart)
|
|
4770
|
+
return {
|
|
4771
|
+
success: false,
|
|
4772
|
+
error: "Provide x,y for position and/or width,height for size"
|
|
4773
|
+
};
|
|
4427
4774
|
await runAppleScript(`
|
|
4428
4775
|
tell application "System Events"
|
|
4429
4776
|
tell process "${app.replace(/"/g, '\\"')}"
|
|
@@ -4431,7 +4778,14 @@ end tell`);
|
|
|
4431
4778
|
${sizePart}
|
|
4432
4779
|
end tell
|
|
4433
4780
|
end tell`);
|
|
4434
|
-
return {
|
|
4781
|
+
return {
|
|
4782
|
+
success: true,
|
|
4783
|
+
data: {
|
|
4784
|
+
app,
|
|
4785
|
+
position: posPart ? { x, y } : "unchanged",
|
|
4786
|
+
size: sizePart ? { width, height } : "unchanged"
|
|
4787
|
+
}
|
|
4788
|
+
};
|
|
4435
4789
|
}
|
|
4436
4790
|
/* ══════════════════════════════════════════════════════════
|
|
4437
4791
|
* OCR
|
|
@@ -4440,7 +4794,8 @@ end tell`);
|
|
|
4440
4794
|
if (!imagePath) return { success: false, error: "Missing image path" };
|
|
4441
4795
|
const fullImg = imagePath.startsWith("/tmp/") ? imagePath : safePath2(imagePath);
|
|
4442
4796
|
if (!fullImg) return { success: false, error: "Access denied" };
|
|
4443
|
-
if (!existsSync2(fullImg))
|
|
4797
|
+
if (!existsSync2(fullImg))
|
|
4798
|
+
return { success: false, error: `Image not found: ${imagePath}` };
|
|
4444
4799
|
const swiftOcr = `
|
|
4445
4800
|
import Foundation
|
|
4446
4801
|
import Vision
|
|
@@ -4462,7 +4817,14 @@ let text = results.compactMap { $0.topCandidates(1).first?.string }.joined(separ
|
|
|
4462
4817
|
print(text)`;
|
|
4463
4818
|
try {
|
|
4464
4819
|
const ocrText = await runSwift(swiftOcr, 3e4);
|
|
4465
|
-
return {
|
|
4820
|
+
return {
|
|
4821
|
+
success: true,
|
|
4822
|
+
data: {
|
|
4823
|
+
text: ocrText.slice(0, 1e4),
|
|
4824
|
+
length: ocrText.length,
|
|
4825
|
+
path: imagePath
|
|
4826
|
+
}
|
|
4827
|
+
};
|
|
4466
4828
|
} catch (err) {
|
|
4467
4829
|
return { success: false, error: `OCR failed: ${err.message}` };
|
|
4468
4830
|
}
|
|
@@ -4486,10 +4848,18 @@ print(text)`;
|
|
|
4486
4848
|
await runAppleScript('tell application "Spotify" to previous track');
|
|
4487
4849
|
return { success: true, data: { action: "previous" } };
|
|
4488
4850
|
case "now_playing": {
|
|
4489
|
-
const name = await runAppleScript(
|
|
4490
|
-
|
|
4491
|
-
|
|
4492
|
-
const
|
|
4851
|
+
const name = await runAppleScript(
|
|
4852
|
+
'tell application "Spotify" to name of current track'
|
|
4853
|
+
);
|
|
4854
|
+
const artist = await runAppleScript(
|
|
4855
|
+
'tell application "Spotify" to artist of current track'
|
|
4856
|
+
);
|
|
4857
|
+
const album = await runAppleScript(
|
|
4858
|
+
'tell application "Spotify" to album of current track'
|
|
4859
|
+
);
|
|
4860
|
+
const state = await runAppleScript(
|
|
4861
|
+
'tell application "Spotify" to player state as string'
|
|
4862
|
+
);
|
|
4493
4863
|
return { success: true, data: { track: name, artist, album, state } };
|
|
4494
4864
|
}
|
|
4495
4865
|
case "search_play": {
|
|
@@ -4497,34 +4867,68 @@ print(text)`;
|
|
|
4497
4867
|
if (!query) return { success: false, error: "Missing search query" };
|
|
4498
4868
|
const result = await this.spotifySearch(query);
|
|
4499
4869
|
if (result) {
|
|
4500
|
-
await runAppleScript(
|
|
4870
|
+
await runAppleScript(
|
|
4871
|
+
`tell application "Spotify" to play track "${result.uri}"`
|
|
4872
|
+
);
|
|
4501
4873
|
await new Promise((r) => setTimeout(r, 1500));
|
|
4502
4874
|
try {
|
|
4503
|
-
const track = await runAppleScript(
|
|
4504
|
-
|
|
4505
|
-
|
|
4875
|
+
const track = await runAppleScript(
|
|
4876
|
+
'tell application "Spotify" to name of current track'
|
|
4877
|
+
);
|
|
4878
|
+
const artist = await runAppleScript(
|
|
4879
|
+
'tell application "Spotify" to artist of current track'
|
|
4880
|
+
);
|
|
4881
|
+
return {
|
|
4882
|
+
success: true,
|
|
4883
|
+
data: {
|
|
4884
|
+
searched: query,
|
|
4885
|
+
resolved: `${result.name} - ${result.artist}`,
|
|
4886
|
+
nowPlaying: `${track} - ${artist}`
|
|
4887
|
+
}
|
|
4888
|
+
};
|
|
4506
4889
|
} catch {
|
|
4507
|
-
return {
|
|
4890
|
+
return {
|
|
4891
|
+
success: true,
|
|
4892
|
+
data: {
|
|
4893
|
+
searched: query,
|
|
4894
|
+
resolved: `${result.name} - ${result.artist}`,
|
|
4895
|
+
note: "Playing track"
|
|
4896
|
+
}
|
|
4897
|
+
};
|
|
4508
4898
|
}
|
|
4509
4899
|
}
|
|
4510
4900
|
await runShell2(`open "spotify:search:${encodeURIComponent(query)}"`);
|
|
4511
|
-
return {
|
|
4901
|
+
return {
|
|
4902
|
+
success: true,
|
|
4903
|
+
data: { searched: query, note: "Opened Spotify search." }
|
|
4904
|
+
};
|
|
4512
4905
|
}
|
|
4513
4906
|
case "volume": {
|
|
4514
4907
|
const level = p.level;
|
|
4515
|
-
if (level === void 0 || level < 0 || level > 100)
|
|
4516
|
-
|
|
4908
|
+
if (level === void 0 || level < 0 || level > 100)
|
|
4909
|
+
return { success: false, error: "Volume must be 0-100" };
|
|
4910
|
+
await runAppleScript(
|
|
4911
|
+
`tell application "Spotify" to set sound volume to ${level}`
|
|
4912
|
+
);
|
|
4517
4913
|
return { success: true, data: { volume: level } };
|
|
4518
4914
|
}
|
|
4519
4915
|
case "shuffle": {
|
|
4520
4916
|
const enabled = p.enabled;
|
|
4521
|
-
await runAppleScript(
|
|
4917
|
+
await runAppleScript(
|
|
4918
|
+
`tell application "Spotify" to set shuffling to ${enabled ? "true" : "false"}`
|
|
4919
|
+
);
|
|
4522
4920
|
return { success: true, data: { shuffling: enabled } };
|
|
4523
4921
|
}
|
|
4524
4922
|
case "repeat": {
|
|
4525
4923
|
const mode = p.mode;
|
|
4526
|
-
if (!mode)
|
|
4527
|
-
|
|
4924
|
+
if (!mode)
|
|
4925
|
+
return {
|
|
4926
|
+
success: false,
|
|
4927
|
+
error: "Missing mode (off, context, track)"
|
|
4928
|
+
};
|
|
4929
|
+
await runAppleScript(
|
|
4930
|
+
`tell application "Spotify" to set repeating to ${mode !== "off"}`
|
|
4931
|
+
);
|
|
4528
4932
|
return { success: true, data: { repeating: mode } };
|
|
4529
4933
|
}
|
|
4530
4934
|
default:
|
|
@@ -4537,11 +4941,17 @@ print(text)`;
|
|
|
4537
4941
|
async hueLightsOn(light, brightness, color) {
|
|
4538
4942
|
if (!light) return { success: false, error: "Missing light ID or name" };
|
|
4539
4943
|
const hueConfig = this.getHueConfig();
|
|
4540
|
-
if (!hueConfig)
|
|
4944
|
+
if (!hueConfig)
|
|
4945
|
+
return {
|
|
4946
|
+
success: false,
|
|
4947
|
+
error: "Philips Hue not configured. Set HUE_BRIDGE_IP and HUE_USERNAME environment variables."
|
|
4948
|
+
};
|
|
4541
4949
|
const lightId = await this.resolveHueLight(hueConfig, light);
|
|
4542
|
-
if (!lightId)
|
|
4950
|
+
if (!lightId)
|
|
4951
|
+
return { success: false, error: `Light '${light}' not found` };
|
|
4543
4952
|
const state = { on: true };
|
|
4544
|
-
if (brightness !== void 0)
|
|
4953
|
+
if (brightness !== void 0)
|
|
4954
|
+
state.bri = Math.max(1, Math.min(254, Number(brightness)));
|
|
4545
4955
|
if (color) {
|
|
4546
4956
|
const rgb = this.parseColor(color);
|
|
4547
4957
|
if (rgb) {
|
|
@@ -4549,46 +4959,91 @@ print(text)`;
|
|
|
4549
4959
|
state.xy = [x, y];
|
|
4550
4960
|
}
|
|
4551
4961
|
}
|
|
4552
|
-
const res = await this.hueRequest(
|
|
4553
|
-
|
|
4962
|
+
const res = await this.hueRequest(
|
|
4963
|
+
hueConfig,
|
|
4964
|
+
`lights/${lightId}/state`,
|
|
4965
|
+
"PUT",
|
|
4966
|
+
state
|
|
4967
|
+
);
|
|
4968
|
+
return {
|
|
4969
|
+
success: true,
|
|
4970
|
+
data: { light: lightId, action: "on", ...state, response: res }
|
|
4971
|
+
};
|
|
4554
4972
|
}
|
|
4555
4973
|
async hueLightsOff(light) {
|
|
4556
4974
|
if (!light) return { success: false, error: "Missing light ID or name" };
|
|
4557
4975
|
const hueConfig = this.getHueConfig();
|
|
4558
|
-
if (!hueConfig)
|
|
4976
|
+
if (!hueConfig)
|
|
4977
|
+
return { success: false, error: "Philips Hue not configured." };
|
|
4559
4978
|
const lightId = await this.resolveHueLight(hueConfig, light);
|
|
4560
|
-
if (!lightId)
|
|
4561
|
-
|
|
4562
|
-
|
|
4979
|
+
if (!lightId)
|
|
4980
|
+
return { success: false, error: `Light '${light}' not found` };
|
|
4981
|
+
const res = await this.hueRequest(
|
|
4982
|
+
hueConfig,
|
|
4983
|
+
`lights/${lightId}/state`,
|
|
4984
|
+
"PUT",
|
|
4985
|
+
{ on: false }
|
|
4986
|
+
);
|
|
4987
|
+
return {
|
|
4988
|
+
success: true,
|
|
4989
|
+
data: { light: lightId, action: "off", response: res }
|
|
4990
|
+
};
|
|
4563
4991
|
}
|
|
4564
4992
|
async hueLightsColor(light, color) {
|
|
4565
|
-
if (!light || !color)
|
|
4993
|
+
if (!light || !color)
|
|
4994
|
+
return { success: false, error: "Missing light or color" };
|
|
4566
4995
|
const hueConfig = this.getHueConfig();
|
|
4567
|
-
if (!hueConfig)
|
|
4996
|
+
if (!hueConfig)
|
|
4997
|
+
return { success: false, error: "Philips Hue not configured." };
|
|
4568
4998
|
const lightId = await this.resolveHueLight(hueConfig, light);
|
|
4569
|
-
if (!lightId)
|
|
4999
|
+
if (!lightId)
|
|
5000
|
+
return { success: false, error: `Light '${light}' not found` };
|
|
4570
5001
|
const rgb = this.parseColor(color);
|
|
4571
5002
|
if (!rgb) return { success: false, error: `Unrecognized color: ${color}` };
|
|
4572
5003
|
const [x, y] = this.rgbToXy(...rgb);
|
|
4573
|
-
const res = await this.hueRequest(
|
|
4574
|
-
|
|
5004
|
+
const res = await this.hueRequest(
|
|
5005
|
+
hueConfig,
|
|
5006
|
+
`lights/${lightId}/state`,
|
|
5007
|
+
"PUT",
|
|
5008
|
+
{ on: true, xy: [x, y] }
|
|
5009
|
+
);
|
|
5010
|
+
return {
|
|
5011
|
+
success: true,
|
|
5012
|
+
data: { light: lightId, color, xy: [x, y], response: res }
|
|
5013
|
+
};
|
|
4575
5014
|
}
|
|
4576
5015
|
async hueLightsBrightness(light, brightness) {
|
|
4577
|
-
if (!light || isNaN(brightness))
|
|
5016
|
+
if (!light || isNaN(brightness))
|
|
5017
|
+
return { success: false, error: "Missing light or brightness" };
|
|
4578
5018
|
const hueConfig = this.getHueConfig();
|
|
4579
|
-
if (!hueConfig)
|
|
5019
|
+
if (!hueConfig)
|
|
5020
|
+
return { success: false, error: "Philips Hue not configured." };
|
|
4580
5021
|
const lightId = await this.resolveHueLight(hueConfig, light);
|
|
4581
|
-
if (!lightId)
|
|
5022
|
+
if (!lightId)
|
|
5023
|
+
return { success: false, error: `Light '${light}' not found` };
|
|
4582
5024
|
const bri = Math.max(1, Math.min(254, brightness));
|
|
4583
|
-
const res = await this.hueRequest(
|
|
4584
|
-
|
|
5025
|
+
const res = await this.hueRequest(
|
|
5026
|
+
hueConfig,
|
|
5027
|
+
`lights/${lightId}/state`,
|
|
5028
|
+
"PUT",
|
|
5029
|
+
{ on: true, bri }
|
|
5030
|
+
);
|
|
5031
|
+
return {
|
|
5032
|
+
success: true,
|
|
5033
|
+
data: { light: lightId, brightness: bri, response: res }
|
|
5034
|
+
};
|
|
4585
5035
|
}
|
|
4586
5036
|
async hueLightsScene(scene, group) {
|
|
4587
5037
|
if (!scene) return { success: false, error: "Missing scene name" };
|
|
4588
5038
|
const hueConfig = this.getHueConfig();
|
|
4589
|
-
if (!hueConfig)
|
|
5039
|
+
if (!hueConfig)
|
|
5040
|
+
return { success: false, error: "Philips Hue not configured." };
|
|
4590
5041
|
const g = group || "0";
|
|
4591
|
-
const scenes = await this.hueRequest(
|
|
5042
|
+
const scenes = await this.hueRequest(
|
|
5043
|
+
hueConfig,
|
|
5044
|
+
"scenes",
|
|
5045
|
+
"GET"
|
|
5046
|
+
);
|
|
4592
5047
|
let sceneId = null;
|
|
4593
5048
|
for (const [id, s] of Object.entries(scenes)) {
|
|
4594
5049
|
if (s.name?.toLowerCase() === scene.toLowerCase()) {
|
|
@@ -4596,13 +5051,20 @@ print(text)`;
|
|
|
4596
5051
|
break;
|
|
4597
5052
|
}
|
|
4598
5053
|
}
|
|
4599
|
-
if (!sceneId)
|
|
4600
|
-
|
|
5054
|
+
if (!sceneId)
|
|
5055
|
+
return {
|
|
5056
|
+
success: false,
|
|
5057
|
+
error: `Scene '${scene}' not found. Available: ${Object.values(scenes).map((s) => s.name).join(", ")}`
|
|
5058
|
+
};
|
|
5059
|
+
const res = await this.hueRequest(hueConfig, `groups/${g}/action`, "PUT", {
|
|
5060
|
+
scene: sceneId
|
|
5061
|
+
});
|
|
4601
5062
|
return { success: true, data: { scene, sceneId, group: g, response: res } };
|
|
4602
5063
|
}
|
|
4603
5064
|
async hueLightsList() {
|
|
4604
5065
|
const hueConfig = this.getHueConfig();
|
|
4605
|
-
if (!hueConfig)
|
|
5066
|
+
if (!hueConfig)
|
|
5067
|
+
return { success: false, error: "Philips Hue not configured." };
|
|
4606
5068
|
const [lights, groups, scenes] = await Promise.all([
|
|
4607
5069
|
this.hueRequest(hueConfig, "lights", "GET"),
|
|
4608
5070
|
this.hueRequest(hueConfig, "groups", "GET"),
|
|
@@ -4611,9 +5073,24 @@ print(text)`;
|
|
|
4611
5073
|
return {
|
|
4612
5074
|
success: true,
|
|
4613
5075
|
data: {
|
|
4614
|
-
lights: Object.entries(lights).map(([id, l]) => ({
|
|
4615
|
-
|
|
4616
|
-
|
|
5076
|
+
lights: Object.entries(lights).map(([id, l]) => ({
|
|
5077
|
+
id,
|
|
5078
|
+
name: l.name,
|
|
5079
|
+
on: l.state?.on,
|
|
5080
|
+
brightness: l.state?.bri,
|
|
5081
|
+
type: l.type
|
|
5082
|
+
})),
|
|
5083
|
+
groups: Object.entries(groups).map(([id, g]) => ({
|
|
5084
|
+
id,
|
|
5085
|
+
name: g.name,
|
|
5086
|
+
type: g.type,
|
|
5087
|
+
lightCount: g.lights?.length
|
|
5088
|
+
})),
|
|
5089
|
+
scenes: Object.entries(scenes).map(([id, s]) => ({
|
|
5090
|
+
id,
|
|
5091
|
+
name: s.name,
|
|
5092
|
+
group: s.group
|
|
5093
|
+
}))
|
|
4617
5094
|
}
|
|
4618
5095
|
};
|
|
4619
5096
|
}
|
|
@@ -4623,23 +5100,37 @@ print(text)`;
|
|
|
4623
5100
|
async sonosPlay(room) {
|
|
4624
5101
|
if (!room) return { success: false, error: "Missing room name" };
|
|
4625
5102
|
const url = this.getSonosApiUrl();
|
|
4626
|
-
if (!url)
|
|
4627
|
-
|
|
5103
|
+
if (!url)
|
|
5104
|
+
return {
|
|
5105
|
+
success: false,
|
|
5106
|
+
error: "Sonos not configured. Set SONOS_API_URL env var."
|
|
5107
|
+
};
|
|
5108
|
+
const res = await this.sonosRequest(
|
|
5109
|
+
url,
|
|
5110
|
+
`${encodeURIComponent(room)}/play`
|
|
5111
|
+
);
|
|
4628
5112
|
return { success: true, data: { room, action: "play", response: res } };
|
|
4629
5113
|
}
|
|
4630
5114
|
async sonosPause(room) {
|
|
4631
5115
|
if (!room) return { success: false, error: "Missing room name" };
|
|
4632
5116
|
const url = this.getSonosApiUrl();
|
|
4633
5117
|
if (!url) return { success: false, error: "Sonos not configured." };
|
|
4634
|
-
const res = await this.sonosRequest(
|
|
5118
|
+
const res = await this.sonosRequest(
|
|
5119
|
+
url,
|
|
5120
|
+
`${encodeURIComponent(room)}/pause`
|
|
5121
|
+
);
|
|
4635
5122
|
return { success: true, data: { room, action: "pause", response: res } };
|
|
4636
5123
|
}
|
|
4637
5124
|
async sonosVolume(room, level) {
|
|
4638
|
-
if (!room || isNaN(level))
|
|
5125
|
+
if (!room || isNaN(level))
|
|
5126
|
+
return { success: false, error: "Missing room or level" };
|
|
4639
5127
|
const url = this.getSonosApiUrl();
|
|
4640
5128
|
if (!url) return { success: false, error: "Sonos not configured." };
|
|
4641
5129
|
const vol = Math.max(0, Math.min(100, level));
|
|
4642
|
-
const res = await this.sonosRequest(
|
|
5130
|
+
const res = await this.sonosRequest(
|
|
5131
|
+
url,
|
|
5132
|
+
`${encodeURIComponent(room)}/volume/${vol}`
|
|
5133
|
+
);
|
|
4643
5134
|
return { success: true, data: { room, volume: vol, response: res } };
|
|
4644
5135
|
}
|
|
4645
5136
|
async sonosPlayUri(room, uri, _title) {
|
|
@@ -4647,10 +5138,19 @@ print(text)`;
|
|
|
4647
5138
|
const url = this.getSonosApiUrl();
|
|
4648
5139
|
if (!url) return { success: false, error: "Sonos not configured." };
|
|
4649
5140
|
if (uri.startsWith("spotify:")) {
|
|
4650
|
-
const res2 = await this.sonosRequest(
|
|
4651
|
-
|
|
5141
|
+
const res2 = await this.sonosRequest(
|
|
5142
|
+
url,
|
|
5143
|
+
`${encodeURIComponent(room)}/spotify/now/${encodeURIComponent(uri)}`
|
|
5144
|
+
);
|
|
5145
|
+
return {
|
|
5146
|
+
success: true,
|
|
5147
|
+
data: { room, uri, type: "spotify", response: res2 }
|
|
5148
|
+
};
|
|
4652
5149
|
}
|
|
4653
|
-
const res = await this.sonosRequest(
|
|
5150
|
+
const res = await this.sonosRequest(
|
|
5151
|
+
url,
|
|
5152
|
+
`${encodeURIComponent(room)}/setavtransporturi/${encodeURIComponent(uri)}`
|
|
5153
|
+
);
|
|
4654
5154
|
return { success: true, data: { room, uri, response: res } };
|
|
4655
5155
|
}
|
|
4656
5156
|
async sonosRooms() {
|
|
@@ -4663,21 +5163,30 @@ print(text)`;
|
|
|
4663
5163
|
if (!room) return { success: false, error: "Missing room name" };
|
|
4664
5164
|
const url = this.getSonosApiUrl();
|
|
4665
5165
|
if (!url) return { success: false, error: "Sonos not configured." };
|
|
4666
|
-
const res = await this.sonosRequest(
|
|
5166
|
+
const res = await this.sonosRequest(
|
|
5167
|
+
url,
|
|
5168
|
+
`${encodeURIComponent(room)}/next`
|
|
5169
|
+
);
|
|
4667
5170
|
return { success: true, data: { room, action: "next", response: res } };
|
|
4668
5171
|
}
|
|
4669
5172
|
async sonosPrevious(room) {
|
|
4670
5173
|
if (!room) return { success: false, error: "Missing room name" };
|
|
4671
5174
|
const url = this.getSonosApiUrl();
|
|
4672
5175
|
if (!url) return { success: false, error: "Sonos not configured." };
|
|
4673
|
-
const res = await this.sonosRequest(
|
|
5176
|
+
const res = await this.sonosRequest(
|
|
5177
|
+
url,
|
|
5178
|
+
`${encodeURIComponent(room)}/previous`
|
|
5179
|
+
);
|
|
4674
5180
|
return { success: true, data: { room, action: "previous", response: res } };
|
|
4675
5181
|
}
|
|
4676
5182
|
async sonosNowPlaying(room) {
|
|
4677
5183
|
if (!room) return { success: false, error: "Missing room name" };
|
|
4678
5184
|
const url = this.getSonosApiUrl();
|
|
4679
5185
|
if (!url) return { success: false, error: "Sonos not configured." };
|
|
4680
|
-
const res = await this.sonosRequest(
|
|
5186
|
+
const res = await this.sonosRequest(
|
|
5187
|
+
url,
|
|
5188
|
+
`${encodeURIComponent(room)}/state`
|
|
5189
|
+
);
|
|
4681
5190
|
return { success: true, data: { room, ...res } };
|
|
4682
5191
|
}
|
|
4683
5192
|
/* ══════════════════════════════════════════════════════════
|
|
@@ -4689,9 +5198,12 @@ print(text)`;
|
|
|
4689
5198
|
const cached = this.searchCache.get(key);
|
|
4690
5199
|
if (cached && Date.now() - cached.ts < CACHE_TTL) return cached;
|
|
4691
5200
|
try {
|
|
4692
|
-
const res = await fetch(
|
|
4693
|
-
|
|
4694
|
-
|
|
5201
|
+
const res = await fetch(
|
|
5202
|
+
`${this.apiUrl}/tools/spotify/search?q=${encodeURIComponent(query)}`,
|
|
5203
|
+
{
|
|
5204
|
+
headers: { Authorization: `Bearer ${this.token}` }
|
|
5205
|
+
}
|
|
5206
|
+
);
|
|
4695
5207
|
if (res.ok) {
|
|
4696
5208
|
const data = await res.json();
|
|
4697
5209
|
if (data.uri) {
|
|
@@ -4706,7 +5218,11 @@ print(text)`;
|
|
|
4706
5218
|
if (looksLikeArtist) {
|
|
4707
5219
|
const artistId = await this.searchWebSpotifyIds(query, "artist");
|
|
4708
5220
|
if (artistId) {
|
|
4709
|
-
const result2 = {
|
|
5221
|
+
const result2 = {
|
|
5222
|
+
uri: `spotify:artist:${artistId}`,
|
|
5223
|
+
name: "Top Songs",
|
|
5224
|
+
artist: query
|
|
5225
|
+
};
|
|
4710
5226
|
this.searchCache.set(key, { ...result2, ts: Date.now() });
|
|
4711
5227
|
this.pushToServerCache(query, result2).catch(() => {
|
|
4712
5228
|
});
|
|
@@ -4716,7 +5232,11 @@ print(text)`;
|
|
|
4716
5232
|
const trackIds = await this.searchWebSpotifyIds(query, "track");
|
|
4717
5233
|
if (!trackIds) return null;
|
|
4718
5234
|
const meta = await this.getTrackMetadata(trackIds);
|
|
4719
|
-
const result = {
|
|
5235
|
+
const result = {
|
|
5236
|
+
uri: `spotify:track:${trackIds}`,
|
|
5237
|
+
name: meta?.name ?? query,
|
|
5238
|
+
artist: meta?.artist ?? "Unknown"
|
|
5239
|
+
};
|
|
4720
5240
|
this.searchCache.set(key, { ...result, ts: Date.now() });
|
|
4721
5241
|
this.pushToServerCache(query, result).catch(() => {
|
|
4722
5242
|
});
|
|
@@ -4727,12 +5247,18 @@ print(text)`;
|
|
|
4727
5247
|
`https://search.brave.com/search?q=${encodeURIComponent(`${query} ${type} site:open.spotify.com`)}&source=web`,
|
|
4728
5248
|
`https://html.duckduckgo.com/html/?q=${encodeURIComponent(`${query} ${type} site:open.spotify.com`)}`
|
|
4729
5249
|
];
|
|
4730
|
-
const pattern = new RegExp(
|
|
5250
|
+
const pattern = new RegExp(
|
|
5251
|
+
`open\\.spotify\\.com(?:/intl-[a-z]+)?/${type}/([a-zA-Z0-9]{22})`,
|
|
5252
|
+
"g"
|
|
5253
|
+
);
|
|
4731
5254
|
for (const url of engines) {
|
|
4732
5255
|
try {
|
|
4733
5256
|
const controller = new AbortController();
|
|
4734
5257
|
const timeout = setTimeout(() => controller.abort(), 8e3);
|
|
4735
|
-
const res = await fetch(url, {
|
|
5258
|
+
const res = await fetch(url, {
|
|
5259
|
+
headers: { "User-Agent": UA, Accept: "text/html" },
|
|
5260
|
+
signal: controller.signal
|
|
5261
|
+
});
|
|
4736
5262
|
clearTimeout(timeout);
|
|
4737
5263
|
if (!res.ok) continue;
|
|
4738
5264
|
const html = await res.text();
|
|
@@ -4746,19 +5272,27 @@ print(text)`;
|
|
|
4746
5272
|
try {
|
|
4747
5273
|
const controller = new AbortController();
|
|
4748
5274
|
const timeout = setTimeout(() => controller.abort(), 5e3);
|
|
4749
|
-
const res = await fetch(
|
|
4750
|
-
|
|
4751
|
-
|
|
4752
|
-
|
|
5275
|
+
const res = await fetch(
|
|
5276
|
+
`https://open.spotify.com/embed/track/${trackId}`,
|
|
5277
|
+
{
|
|
5278
|
+
headers: { "User-Agent": UA, Accept: "text/html" },
|
|
5279
|
+
signal: controller.signal
|
|
5280
|
+
}
|
|
5281
|
+
);
|
|
4753
5282
|
clearTimeout(timeout);
|
|
4754
5283
|
if (!res.ok) return null;
|
|
4755
5284
|
const html = await res.text();
|
|
4756
|
-
const match = html.match(
|
|
5285
|
+
const match = html.match(
|
|
5286
|
+
/<script[^>]*>(\{"props":\{"pageProps".*?\})<\/script>/s
|
|
5287
|
+
);
|
|
4757
5288
|
if (!match) return null;
|
|
4758
5289
|
const data = JSON.parse(match[1]);
|
|
4759
5290
|
const entity = data?.props?.pageProps?.state?.data?.entity;
|
|
4760
5291
|
if (!entity?.name) return null;
|
|
4761
|
-
return {
|
|
5292
|
+
return {
|
|
5293
|
+
name: entity.name,
|
|
5294
|
+
artist: entity.artists?.map((a) => a.name).join(", ") ?? "Unknown"
|
|
5295
|
+
};
|
|
4762
5296
|
} catch {
|
|
4763
5297
|
return null;
|
|
4764
5298
|
}
|
|
@@ -4767,7 +5301,10 @@ print(text)`;
|
|
|
4767
5301
|
try {
|
|
4768
5302
|
await fetch(`${this.apiUrl}/tools/spotify/cache`, {
|
|
4769
5303
|
method: "POST",
|
|
4770
|
-
headers: {
|
|
5304
|
+
headers: {
|
|
5305
|
+
Authorization: `Bearer ${this.token}`,
|
|
5306
|
+
"Content-Type": "application/json"
|
|
5307
|
+
},
|
|
4771
5308
|
body: JSON.stringify({ query, ...result })
|
|
4772
5309
|
});
|
|
4773
5310
|
} catch {
|
|
@@ -4793,21 +5330,31 @@ print(text)`;
|
|
|
4793
5330
|
const controller = new AbortController();
|
|
4794
5331
|
const timeout = setTimeout(() => controller.abort(), 1e4);
|
|
4795
5332
|
try {
|
|
4796
|
-
const opts = {
|
|
5333
|
+
const opts = {
|
|
5334
|
+
method,
|
|
5335
|
+
signal: controller.signal,
|
|
5336
|
+
headers: { "Content-Type": "application/json" }
|
|
5337
|
+
};
|
|
4797
5338
|
if (body && method !== "GET") opts.body = JSON.stringify(body);
|
|
4798
5339
|
const res = await fetch(url, opts);
|
|
4799
5340
|
clearTimeout(timeout);
|
|
4800
5341
|
return await res.json();
|
|
4801
5342
|
} catch (err) {
|
|
4802
5343
|
clearTimeout(timeout);
|
|
4803
|
-
throw new Error(
|
|
5344
|
+
throw new Error(
|
|
5345
|
+
`Hue bridge unreachable at ${config.bridgeIp}: ${err.message}`
|
|
5346
|
+
);
|
|
4804
5347
|
}
|
|
4805
5348
|
}
|
|
4806
5349
|
async resolveHueLight(config, lightRef) {
|
|
4807
5350
|
if (/^\d+$/.test(lightRef)) return lightRef;
|
|
4808
5351
|
if (!this.hueLightCache || Date.now() - this.hueLightCacheTs > 3e5) {
|
|
4809
5352
|
try {
|
|
4810
|
-
const lights = await this.hueRequest(
|
|
5353
|
+
const lights = await this.hueRequest(
|
|
5354
|
+
config,
|
|
5355
|
+
"lights",
|
|
5356
|
+
"GET"
|
|
5357
|
+
);
|
|
4811
5358
|
this.hueLightCache = /* @__PURE__ */ new Map();
|
|
4812
5359
|
for (const [id, light] of Object.entries(lights)) {
|
|
4813
5360
|
this.hueLightCache.set(light.name.toLowerCase(), id);
|
|
@@ -4821,7 +5368,11 @@ print(text)`;
|
|
|
4821
5368
|
}
|
|
4822
5369
|
parseColor(color) {
|
|
4823
5370
|
if (color.startsWith("#") && color.length === 7) {
|
|
4824
|
-
return [
|
|
5371
|
+
return [
|
|
5372
|
+
parseInt(color.slice(1, 3), 16),
|
|
5373
|
+
parseInt(color.slice(3, 5), 16),
|
|
5374
|
+
parseInt(color.slice(5, 7), 16)
|
|
5375
|
+
];
|
|
4825
5376
|
}
|
|
4826
5377
|
return CSS_COLORS2[color.toLowerCase()] ?? null;
|
|
4827
5378
|
}
|
|
@@ -4856,7 +5407,9 @@ print(text)`;
|
|
|
4856
5407
|
}
|
|
4857
5408
|
} catch (err) {
|
|
4858
5409
|
clearTimeout(timeout);
|
|
4859
|
-
throw new Error(
|
|
5410
|
+
throw new Error(
|
|
5411
|
+
`Sonos API unreachable at ${baseUrl}: ${err.message}`
|
|
5412
|
+
);
|
|
4860
5413
|
}
|
|
4861
5414
|
}
|
|
4862
5415
|
};
|
|
@@ -4878,13 +5431,7 @@ import { homedir as homedir3, hostname as hostname2, cpus as cpus2, totalmem as
|
|
|
4878
5431
|
import { join as join4, resolve as resolve3, basename as basename3, extname as extname3 } from "path";
|
|
4879
5432
|
var HOME3 = homedir3();
|
|
4880
5433
|
var NOTES_DIR2 = join4(HOME3, "Documents", "PulsoNotes");
|
|
4881
|
-
var SAFE_DIRS3 = [
|
|
4882
|
-
"Documents",
|
|
4883
|
-
"Desktop",
|
|
4884
|
-
"Downloads",
|
|
4885
|
-
"Projects",
|
|
4886
|
-
"Projetos"
|
|
4887
|
-
];
|
|
5434
|
+
var SAFE_DIRS3 = ["Documents", "Desktop", "Downloads", "Projects", "Projetos"];
|
|
4888
5435
|
var ACCESS_LEVEL2 = process.env.PULSO_ACCESS ?? "sandboxed";
|
|
4889
5436
|
function safePath3(relative) {
|
|
4890
5437
|
const full = resolve3(HOME3, relative);
|
|
@@ -4911,10 +5458,14 @@ function runPowerShell(script, timeout = 15e3) {
|
|
|
4911
5458
|
return new Promise((resolve5, reject) => {
|
|
4912
5459
|
const encoded = Buffer.from(script, "utf16le").toString("base64");
|
|
4913
5460
|
const cmd = `powershell -NoProfile -NonInteractive -EncodedCommand ${encoded}`;
|
|
4914
|
-
exec4(
|
|
4915
|
-
|
|
4916
|
-
|
|
4917
|
-
|
|
5461
|
+
exec4(
|
|
5462
|
+
cmd,
|
|
5463
|
+
{ timeout, maxBuffer: 10 * 1024 * 1024 },
|
|
5464
|
+
(err, stdout, stderr) => {
|
|
5465
|
+
if (err) return reject(new Error(stderr?.trim() || err.message));
|
|
5466
|
+
resolve5(stdout.trim());
|
|
5467
|
+
}
|
|
5468
|
+
);
|
|
4918
5469
|
});
|
|
4919
5470
|
}
|
|
4920
5471
|
function runPowerShellScript(script, timeout = 15e3) {
|
|
@@ -5207,7 +5758,10 @@ var WindowsAdapter = class {
|
|
|
5207
5758
|
try {
|
|
5208
5759
|
const parsed = new URL(url);
|
|
5209
5760
|
if (!["http:", "https:"].includes(parsed.protocol)) {
|
|
5210
|
-
return {
|
|
5761
|
+
return {
|
|
5762
|
+
success: false,
|
|
5763
|
+
error: "Only http and https URLs are allowed"
|
|
5764
|
+
};
|
|
5211
5765
|
}
|
|
5212
5766
|
await runPowerShell(`Start-Process "${url}"`);
|
|
5213
5767
|
return { success: true, data: { url, action: "opened" } };
|
|
@@ -5324,7 +5878,10 @@ public class AudioHelper {
|
|
|
5324
5878
|
`;
|
|
5325
5879
|
const output2 = await runPowerShellScript(script, 1e4);
|
|
5326
5880
|
const volume = parseInt(output2, 10);
|
|
5327
|
-
return {
|
|
5881
|
+
return {
|
|
5882
|
+
success: true,
|
|
5883
|
+
data: { volume: isNaN(volume) ? output2 : volume }
|
|
5884
|
+
};
|
|
5328
5885
|
} catch (err) {
|
|
5329
5886
|
try {
|
|
5330
5887
|
const output2 = await runPowerShell(
|
|
@@ -5535,7 +6092,12 @@ Write-Output "$uptimeStr"
|
|
|
5535
6092
|
data: { shortcut: name, method: "power-automate" }
|
|
5536
6093
|
};
|
|
5537
6094
|
} catch {
|
|
5538
|
-
const scriptPath = join4(
|
|
6095
|
+
const scriptPath = join4(
|
|
6096
|
+
HOME3,
|
|
6097
|
+
"Documents",
|
|
6098
|
+
"PulsoScripts",
|
|
6099
|
+
`${name}.ps1`
|
|
6100
|
+
);
|
|
5539
6101
|
if (existsSync3(scriptPath)) {
|
|
5540
6102
|
const inputArg = input2 ? `-InputData '${input2.replace(/'/g, "''")}'` : "";
|
|
5541
6103
|
const output2 = await runPowerShell(
|
|
@@ -5562,9 +6124,7 @@ Write-Output "$uptimeStr"
|
|
|
5562
6124
|
async dnd(enabled) {
|
|
5563
6125
|
try {
|
|
5564
6126
|
if (enabled === void 0 || enabled) {
|
|
5565
|
-
await runPowerShell(
|
|
5566
|
-
`Start-Process "ms-settings:quiethours"`
|
|
5567
|
-
);
|
|
6127
|
+
await runPowerShell(`Start-Process "ms-settings:quiethours"`);
|
|
5568
6128
|
return {
|
|
5569
6129
|
success: true,
|
|
5570
6130
|
data: {
|
|
@@ -5988,7 +6548,11 @@ $chromeProcs | ForEach-Object { Write-Output "Chrome|$($_.MainWindowTitle)|" }
|
|
|
5988
6548
|
if (!output2) return [];
|
|
5989
6549
|
return output2.split("\n").filter(Boolean).map((line) => {
|
|
5990
6550
|
const [browser, title] = line.split("|");
|
|
5991
|
-
return {
|
|
6551
|
+
return {
|
|
6552
|
+
browser: browser || "Unknown",
|
|
6553
|
+
title: title || "",
|
|
6554
|
+
url: ""
|
|
6555
|
+
};
|
|
5992
6556
|
});
|
|
5993
6557
|
} catch {
|
|
5994
6558
|
return [];
|
|
@@ -6085,7 +6649,11 @@ foreach ($w in $windows) {
|
|
|
6085
6649
|
if (output2.includes("executed")) {
|
|
6086
6650
|
return {
|
|
6087
6651
|
success: true,
|
|
6088
|
-
data: {
|
|
6652
|
+
data: {
|
|
6653
|
+
code,
|
|
6654
|
+
method: "ie-com",
|
|
6655
|
+
note: "Executed via IE COM object. For modern browser JS, use Chrome CDP."
|
|
6656
|
+
}
|
|
6089
6657
|
};
|
|
6090
6658
|
}
|
|
6091
6659
|
return {
|
|
@@ -6099,6 +6667,59 @@ foreach ($w in $windows) {
|
|
|
6099
6667
|
};
|
|
6100
6668
|
}
|
|
6101
6669
|
}
|
|
6670
|
+
async browserListProfiles() {
|
|
6671
|
+
try {
|
|
6672
|
+
const userProfile = process.env.LOCALAPPDATA || process.env.APPDATA || "";
|
|
6673
|
+
const browserPaths = [
|
|
6674
|
+
{
|
|
6675
|
+
browser: "Google Chrome",
|
|
6676
|
+
dir: `${userProfile}\\Google\\Chrome\\User Data`
|
|
6677
|
+
},
|
|
6678
|
+
{
|
|
6679
|
+
browser: "Microsoft Edge",
|
|
6680
|
+
dir: `${userProfile}\\Microsoft\\Edge\\User Data`
|
|
6681
|
+
},
|
|
6682
|
+
{
|
|
6683
|
+
browser: "Brave Browser",
|
|
6684
|
+
dir: `${userProfile}\\BraveSoftware\\Brave-Browser\\User Data`
|
|
6685
|
+
},
|
|
6686
|
+
{ browser: "Vivaldi", dir: `${userProfile}\\Vivaldi\\User Data` },
|
|
6687
|
+
{
|
|
6688
|
+
browser: "Opera",
|
|
6689
|
+
dir: `${userProfile}\\Opera Software\\Opera Stable`
|
|
6690
|
+
}
|
|
6691
|
+
];
|
|
6692
|
+
const profiles = [];
|
|
6693
|
+
for (const { browser, dir } of browserPaths) {
|
|
6694
|
+
if (!existsSync3(dir)) continue;
|
|
6695
|
+
let entries = [];
|
|
6696
|
+
try {
|
|
6697
|
+
entries = readdirSync3(dir);
|
|
6698
|
+
} catch {
|
|
6699
|
+
continue;
|
|
6700
|
+
}
|
|
6701
|
+
for (const entry of entries) {
|
|
6702
|
+
if (entry !== "Default" && !entry.startsWith("Profile ")) continue;
|
|
6703
|
+
const prefsPath = `${dir}\\${entry}\\Preferences`;
|
|
6704
|
+
if (!existsSync3(prefsPath)) continue;
|
|
6705
|
+
try {
|
|
6706
|
+
const prefs = JSON.parse(readFileSync3(prefsPath, "utf-8"));
|
|
6707
|
+
profiles.push({
|
|
6708
|
+
browser,
|
|
6709
|
+
profileDir: `${dir}\\${entry}`,
|
|
6710
|
+
name: prefs.profile?.name || entry,
|
|
6711
|
+
email: prefs.account_info?.[0]?.email,
|
|
6712
|
+
isDefault: entry === "Default"
|
|
6713
|
+
});
|
|
6714
|
+
} catch {
|
|
6715
|
+
}
|
|
6716
|
+
}
|
|
6717
|
+
}
|
|
6718
|
+
return { success: true, data: { profiles, total: profiles.length } };
|
|
6719
|
+
} catch (err) {
|
|
6720
|
+
return { success: false, error: err.message };
|
|
6721
|
+
}
|
|
6722
|
+
}
|
|
6102
6723
|
/* ══════════════════════════════════════════════════════════
|
|
6103
6724
|
* Productivity: Calendar
|
|
6104
6725
|
*
|
|
@@ -6215,7 +6836,11 @@ try {
|
|
|
6215
6836
|
const output2 = await runPowerShellScript(script, 15e3);
|
|
6216
6837
|
const reminders = output2.split("\n").filter(Boolean).map((line) => {
|
|
6217
6838
|
const [name, due, listName] = line.split("|");
|
|
6218
|
-
return {
|
|
6839
|
+
return {
|
|
6840
|
+
name: name || "Untitled",
|
|
6841
|
+
due: due || void 0,
|
|
6842
|
+
list: listName || void 0
|
|
6843
|
+
};
|
|
6219
6844
|
});
|
|
6220
6845
|
return { success: true, data: { reminders, count: reminders.length } };
|
|
6221
6846
|
} catch (err) {
|
|
@@ -6252,7 +6877,10 @@ try {
|
|
|
6252
6877
|
data: { title, dueDate, list, method: "outlook-task" }
|
|
6253
6878
|
};
|
|
6254
6879
|
}
|
|
6255
|
-
return {
|
|
6880
|
+
return {
|
|
6881
|
+
success: false,
|
|
6882
|
+
error: "Failed to create reminder. Is Outlook installed?"
|
|
6883
|
+
};
|
|
6256
6884
|
} catch (err) {
|
|
6257
6885
|
return {
|
|
6258
6886
|
success: false,
|
|
@@ -6369,7 +6997,9 @@ try {
|
|
|
6369
6997
|
name: f.replace(/\.(txt|md)$/, ""),
|
|
6370
6998
|
modified: stat.mtime.toISOString()
|
|
6371
6999
|
};
|
|
6372
|
-
}).sort(
|
|
7000
|
+
}).sort(
|
|
7001
|
+
(a, b) => new Date(b.modified).getTime() - new Date(a.modified).getTime()
|
|
7002
|
+
);
|
|
6373
7003
|
const max = limit || 20;
|
|
6374
7004
|
return {
|
|
6375
7005
|
success: true,
|
|
@@ -6422,7 +7052,12 @@ ${body}`;
|
|
|
6422
7052
|
await runPowerShell(`Start-Process "${gmailUrl}"`);
|
|
6423
7053
|
return {
|
|
6424
7054
|
success: true,
|
|
6425
|
-
data: {
|
|
7055
|
+
data: {
|
|
7056
|
+
to,
|
|
7057
|
+
subject,
|
|
7058
|
+
method: "gmail",
|
|
7059
|
+
note: "Gmail compose window opened"
|
|
7060
|
+
}
|
|
6426
7061
|
};
|
|
6427
7062
|
} catch (err) {
|
|
6428
7063
|
return {
|
|
@@ -6578,7 +7213,10 @@ try {
|
|
|
6578
7213
|
const srcPath = safePath3(source);
|
|
6579
7214
|
const dstPath = safePath3(destination);
|
|
6580
7215
|
if (!srcPath || !dstPath) {
|
|
6581
|
-
return {
|
|
7216
|
+
return {
|
|
7217
|
+
success: false,
|
|
7218
|
+
error: "Access denied to source or destination."
|
|
7219
|
+
};
|
|
6582
7220
|
}
|
|
6583
7221
|
if (!existsSync3(srcPath)) {
|
|
6584
7222
|
return { success: false, error: `Source not found: ${source}` };
|
|
@@ -6600,7 +7238,10 @@ try {
|
|
|
6600
7238
|
const srcPath = safePath3(source);
|
|
6601
7239
|
const dstPath = safePath3(destination);
|
|
6602
7240
|
if (!srcPath || !dstPath) {
|
|
6603
|
-
return {
|
|
7241
|
+
return {
|
|
7242
|
+
success: false,
|
|
7243
|
+
error: "Access denied to source or destination."
|
|
7244
|
+
};
|
|
6604
7245
|
}
|
|
6605
7246
|
if (!existsSync3(srcPath)) {
|
|
6606
7247
|
return { success: false, error: `Source not found: ${source}` };
|
|
@@ -6976,12 +7617,19 @@ $input.u.ki.dwFlags = [WinInput]::KEYEVENTF_KEYUP
|
|
|
6976
7617
|
}
|
|
6977
7618
|
return {
|
|
6978
7619
|
success: true,
|
|
6979
|
-
data: {
|
|
7620
|
+
data: {
|
|
7621
|
+
nowPlaying: output2,
|
|
7622
|
+
state: "playing",
|
|
7623
|
+
source: "window_title"
|
|
7624
|
+
}
|
|
6980
7625
|
};
|
|
6981
7626
|
}
|
|
6982
7627
|
return {
|
|
6983
7628
|
success: true,
|
|
6984
|
-
data: {
|
|
7629
|
+
data: {
|
|
7630
|
+
state: "paused_or_idle",
|
|
7631
|
+
note: "Spotify may be paused or not playing"
|
|
7632
|
+
}
|
|
6985
7633
|
};
|
|
6986
7634
|
} catch {
|
|
6987
7635
|
return {
|
|
@@ -7055,7 +7703,10 @@ $input.u.ki.dwFlags = [WinInput]::KEYEVENTF_KEYUP
|
|
|
7055
7703
|
}
|
|
7056
7704
|
const lightId = await resolveHueLight2(config, light);
|
|
7057
7705
|
if (!lightId) {
|
|
7058
|
-
return {
|
|
7706
|
+
return {
|
|
7707
|
+
success: false,
|
|
7708
|
+
error: `Light '${light}' not found on Hue bridge`
|
|
7709
|
+
};
|
|
7059
7710
|
}
|
|
7060
7711
|
const state = { on: true };
|
|
7061
7712
|
if (brightness !== void 0) {
|
|
@@ -7068,7 +7719,12 @@ $input.u.ki.dwFlags = [WinInput]::KEYEVENTF_KEYUP
|
|
|
7068
7719
|
state.xy = [x, y];
|
|
7069
7720
|
}
|
|
7070
7721
|
}
|
|
7071
|
-
const res = await hueRequest2(
|
|
7722
|
+
const res = await hueRequest2(
|
|
7723
|
+
config,
|
|
7724
|
+
`lights/${lightId}/state`,
|
|
7725
|
+
"PUT",
|
|
7726
|
+
state
|
|
7727
|
+
);
|
|
7072
7728
|
return {
|
|
7073
7729
|
success: true,
|
|
7074
7730
|
data: { light: lightId, action: "on", ...state, response: res }
|
|
@@ -7090,7 +7746,9 @@ $input.u.ki.dwFlags = [WinInput]::KEYEVENTF_KEYUP
|
|
|
7090
7746
|
if (!lightId) {
|
|
7091
7747
|
return { success: false, error: `Light '${light}' not found` };
|
|
7092
7748
|
}
|
|
7093
|
-
const res = await hueRequest2(config, `lights/${lightId}/state`, "PUT", {
|
|
7749
|
+
const res = await hueRequest2(config, `lights/${lightId}/state`, "PUT", {
|
|
7750
|
+
on: false
|
|
7751
|
+
});
|
|
7094
7752
|
return {
|
|
7095
7753
|
success: true,
|
|
7096
7754
|
data: { light: lightId, action: "off", response: res }
|
|
@@ -7360,11 +8018,11 @@ $input.u.ki.dwFlags = [WinInput]::KEYEVENTF_KEYUP
|
|
|
7360
8018
|
try {
|
|
7361
8019
|
const url = getSonosApiUrl2();
|
|
7362
8020
|
if (!url) return { success: false, error: "Sonos not configured." };
|
|
7363
|
-
const res = await sonosRequest2(
|
|
7364
|
-
|
|
7365
|
-
|
|
7366
|
-
|
|
7367
|
-
|
|
8021
|
+
const res = await sonosRequest2(url, `${encodeURIComponent(room)}/state`);
|
|
8022
|
+
return {
|
|
8023
|
+
success: true,
|
|
8024
|
+
data: { room, ...res }
|
|
8025
|
+
};
|
|
7368
8026
|
} catch (err) {
|
|
7369
8027
|
return {
|
|
7370
8028
|
success: false,
|
|
@@ -7426,7 +8084,9 @@ async function loadWhisper(model = "base.en") {
|
|
|
7426
8084
|
try {
|
|
7427
8085
|
const sw = require_dist();
|
|
7428
8086
|
const { Whisper, manager: manager2 } = sw;
|
|
7429
|
-
console.log(
|
|
8087
|
+
console.log(
|
|
8088
|
+
` \u{1F3A4} Loading Whisper ${model} STT (downloading ~74-142MB, first use only)...`
|
|
8089
|
+
);
|
|
7430
8090
|
await manager2.download(model, (p) => {
|
|
7431
8091
|
if (p % 25 === 0) process.stdout.write(`\r \u{1F4E5} ${p}% `);
|
|
7432
8092
|
});
|
|
@@ -7439,7 +8099,10 @@ async function loadWhisper(model = "base.en") {
|
|
|
7439
8099
|
} catch (err) {
|
|
7440
8100
|
whisperState = "failed";
|
|
7441
8101
|
const msg = err.message ?? String(err);
|
|
7442
|
-
console.warn(
|
|
8102
|
+
console.warn(
|
|
8103
|
+
" \u2139\uFE0F smart-whisper STT unavailable (optional):",
|
|
8104
|
+
msg.slice(0, 120)
|
|
8105
|
+
);
|
|
7443
8106
|
}
|
|
7444
8107
|
})();
|
|
7445
8108
|
return whisperLoadPromise;
|
|
@@ -7475,7 +8138,10 @@ function decodeWav(buffer) {
|
|
|
7475
8138
|
const channels = buffer.readUInt16LE(fmtOffset + 10);
|
|
7476
8139
|
const sampleRate = buffer.readUInt32LE(fmtOffset + 12);
|
|
7477
8140
|
const bitsPerSample = buffer.readUInt16LE(fmtOffset + 22);
|
|
7478
|
-
if (audioFmt !== 1)
|
|
8141
|
+
if (audioFmt !== 1)
|
|
8142
|
+
throw new Error(
|
|
8143
|
+
`Unsupported WAV format: ${audioFmt} (only PCM=1 supported)`
|
|
8144
|
+
);
|
|
7479
8145
|
let dataOffset = 36;
|
|
7480
8146
|
let dataSize = 0;
|
|
7481
8147
|
while (dataOffset < buffer.length - 8) {
|
|
@@ -7543,7 +8209,10 @@ async function transcribeWithWhisper(pcmOrWav, inputSampleRate, opts) {
|
|
|
7543
8209
|
durationMs: Date.now() - t0
|
|
7544
8210
|
};
|
|
7545
8211
|
} catch (err) {
|
|
7546
|
-
console.warn(
|
|
8212
|
+
console.warn(
|
|
8213
|
+
" \u26A0\uFE0F Whisper transcription failed:",
|
|
8214
|
+
err.message.slice(0, 100)
|
|
8215
|
+
);
|
|
7547
8216
|
return null;
|
|
7548
8217
|
}
|
|
7549
8218
|
}
|
|
@@ -7704,7 +8373,10 @@ async function streamChatResponse(params, hooks, options) {
|
|
|
7704
8373
|
if (options?.signal?.aborted || err?.name === "AbortError") {
|
|
7705
8374
|
return { ok: false, error: "aborted" };
|
|
7706
8375
|
}
|
|
7707
|
-
return {
|
|
8376
|
+
return {
|
|
8377
|
+
ok: false,
|
|
8378
|
+
error: err.message || "stream read failed"
|
|
8379
|
+
};
|
|
7708
8380
|
}
|
|
7709
8381
|
if (chunk.done) break;
|
|
7710
8382
|
buffer += decoder.decode(chunk.value, { stream: true });
|
|
@@ -7975,7 +8647,11 @@ async function runIdeChatTui(params) {
|
|
|
7975
8647
|
const bars = 4 + Math.round(amp * 12);
|
|
7976
8648
|
return `[${"=".repeat(bars).padEnd(16, " ")}]`;
|
|
7977
8649
|
});
|
|
7978
|
-
const now = () => (/* @__PURE__ */ new Date()).toLocaleTimeString([], {
|
|
8650
|
+
const now = () => (/* @__PURE__ */ new Date()).toLocaleTimeString([], {
|
|
8651
|
+
hour: "2-digit",
|
|
8652
|
+
minute: "2-digit",
|
|
8653
|
+
second: "2-digit"
|
|
8654
|
+
});
|
|
7979
8655
|
const formatTimestamp = (at) => new Date(at).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
|
|
7980
8656
|
const shortValue = (value, max = 28) => value.length > max ? `${value.slice(0, max - 3)}...` : value;
|
|
7981
8657
|
const pushAssistantNote = (message) => {
|
|
@@ -8011,7 +8687,9 @@ async function runIdeChatTui(params) {
|
|
|
8011
8687
|
if (busy) {
|
|
8012
8688
|
const dots = ".".repeat(pulseTick % 3 + 1);
|
|
8013
8689
|
chunks.push(`\u25CF PULSO ${formatTimestamp(Date.now())}`);
|
|
8014
|
-
chunks.push(
|
|
8690
|
+
chunks.push(
|
|
8691
|
+
...(currentAssistant || `thinking${dots}`).split("\n").map((line) => ` ${line}`)
|
|
8692
|
+
);
|
|
8015
8693
|
chunks.push("");
|
|
8016
8694
|
}
|
|
8017
8695
|
return chunks.join("\n");
|
|
@@ -8025,11 +8703,9 @@ async function runIdeChatTui(params) {
|
|
|
8025
8703
|
const pulseFrame = pulseFrames[pulseTick];
|
|
8026
8704
|
const logoPulse = busy ? "\u25C9" : "\u25CE";
|
|
8027
8705
|
logoBox.setContent(
|
|
8028
|
-
[
|
|
8029
|
-
|
|
8030
|
-
|
|
8031
|
-
` ${IDE_VERSION}`
|
|
8032
|
-
].join("\n")
|
|
8706
|
+
[` ${logoPulse} ${pulseFrame}`, " PULSO", ` ${IDE_VERSION}`].join(
|
|
8707
|
+
"\n"
|
|
8708
|
+
)
|
|
8033
8709
|
);
|
|
8034
8710
|
heroBox.setContent(
|
|
8035
8711
|
[
|
|
@@ -8198,12 +8874,16 @@ async function runIdeChatTui(params) {
|
|
|
8198
8874
|
if (cmd === "theme") {
|
|
8199
8875
|
const normalizedTheme = value.toLowerCase();
|
|
8200
8876
|
if (!normalizedTheme) {
|
|
8201
|
-
pushAssistantNote(
|
|
8202
|
-
|
|
8877
|
+
pushAssistantNote(
|
|
8878
|
+
`Current theme: ${currentThemeName}
|
|
8879
|
+
Available themes: pulso, claude`
|
|
8880
|
+
);
|
|
8203
8881
|
pushTimeline("theme unchanged");
|
|
8204
8882
|
} else if (normalizedTheme !== "pulso" && normalizedTheme !== "claude") {
|
|
8205
|
-
pushAssistantNote(
|
|
8206
|
-
|
|
8883
|
+
pushAssistantNote(
|
|
8884
|
+
`Invalid theme: ${value}
|
|
8885
|
+
Use /theme pulso or /theme claude`
|
|
8886
|
+
);
|
|
8207
8887
|
pushTimeline("invalid theme");
|
|
8208
8888
|
} else {
|
|
8209
8889
|
currentThemeName = normalizedTheme;
|
|
@@ -8229,8 +8909,10 @@ Use /theme pulso or /theme claude`);
|
|
|
8229
8909
|
return "handled";
|
|
8230
8910
|
}
|
|
8231
8911
|
if (normalized.startsWith("/")) {
|
|
8232
|
-
pushAssistantNote(
|
|
8233
|
-
|
|
8912
|
+
pushAssistantNote(
|
|
8913
|
+
`Unknown command: ${normalized}
|
|
8914
|
+
Use /help to list available commands.`
|
|
8915
|
+
);
|
|
8234
8916
|
pushTimeline("unknown command");
|
|
8235
8917
|
render();
|
|
8236
8918
|
return "handled";
|
|
@@ -8240,7 +8922,9 @@ Use /help to list available commands.`);
|
|
|
8240
8922
|
const sendPrompt = async (raw) => {
|
|
8241
8923
|
const prompt = raw.trim();
|
|
8242
8924
|
if (!prompt) return;
|
|
8243
|
-
if (prompt.startsWith("/") || ["help", "clear", "quit", "exit", "settings", "stats", "theme"].includes(
|
|
8925
|
+
if (prompt.startsWith("/") || ["help", "clear", "quit", "exit", "settings", "stats", "theme"].includes(
|
|
8926
|
+
prompt.toLowerCase()
|
|
8927
|
+
)) {
|
|
8244
8928
|
const commandResult = handleCommand2(prompt);
|
|
8245
8929
|
if (commandResult === "exit") {
|
|
8246
8930
|
exit();
|
|
@@ -8322,7 +9006,9 @@ Use /help to list available commands.`);
|
|
|
8322
9006
|
at: Date.now(),
|
|
8323
9007
|
content: currentAssistant || "(empty response)"
|
|
8324
9008
|
});
|
|
8325
|
-
pushTimeline(
|
|
9009
|
+
pushTimeline(
|
|
9010
|
+
`usage +$${usageCost.toFixed(4)} \xB7 tok ${usageIn}/${usageOut}`
|
|
9011
|
+
);
|
|
8326
9012
|
}
|
|
8327
9013
|
currentAssistant = "";
|
|
8328
9014
|
render();
|
|
@@ -8426,7 +9112,8 @@ async function runInteractiveChat(params) {
|
|
|
8426
9112
|
const prompt = promptRaw.trim();
|
|
8427
9113
|
const normalized = prompt.toLowerCase();
|
|
8428
9114
|
if (!prompt) continue;
|
|
8429
|
-
if (normalized === "/exit" || normalized === "/quit" || normalized === "exit" || normalized === "quit")
|
|
9115
|
+
if (normalized === "/exit" || normalized === "/quit" || normalized === "exit" || normalized === "quit")
|
|
9116
|
+
break;
|
|
8430
9117
|
if (normalized === "clear" || normalized === "/clear") {
|
|
8431
9118
|
if (output.isTTY) {
|
|
8432
9119
|
output.write("\x1Bc");
|
|
@@ -8491,7 +9178,11 @@ async function runInteractiveChat(params) {
|
|
|
8491
9178
|
}
|
|
8492
9179
|
totalCost += result.costUsd ?? 0;
|
|
8493
9180
|
if (typeof result.costUsd === "number") {
|
|
8494
|
-
console.log(
|
|
9181
|
+
console.log(
|
|
9182
|
+
dimText(
|
|
9183
|
+
` [cost: $${result.costUsd.toFixed(4)} | total: $${totalCost.toFixed(4)}]`
|
|
9184
|
+
)
|
|
9185
|
+
);
|
|
8495
9186
|
console.log("");
|
|
8496
9187
|
} else {
|
|
8497
9188
|
console.log("");
|
|
@@ -8519,12 +9210,7 @@ async function deviceLogin(apiUrl) {
|
|
|
8519
9210
|
console.error(` Failed to request device code: ${err}`);
|
|
8520
9211
|
return null;
|
|
8521
9212
|
}
|
|
8522
|
-
const {
|
|
8523
|
-
device_code,
|
|
8524
|
-
user_code,
|
|
8525
|
-
verification_url,
|
|
8526
|
-
interval
|
|
8527
|
-
} = await codeRes.json();
|
|
9213
|
+
const { device_code, user_code, verification_url, interval } = await codeRes.json();
|
|
8528
9214
|
console.log(" \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510");
|
|
8529
9215
|
console.log(" \u2502 \u2502");
|
|
8530
9216
|
console.log(" \u2502 Open this URL in your browser: \u2502");
|
|
@@ -8605,9 +9291,15 @@ if (helpRequested || subCommand === "help") {
|
|
|
8605
9291
|
console.log(" Pulso Companion Commands");
|
|
8606
9292
|
console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
8607
9293
|
console.log(` ${COMMAND_LOGIN.padEnd(26)} Authenticate device`);
|
|
8608
|
-
console.log(
|
|
8609
|
-
|
|
8610
|
-
|
|
9294
|
+
console.log(
|
|
9295
|
+
` ${COMMAND_LOGOUT.padEnd(26)} Revoke token + clear local creds`
|
|
9296
|
+
);
|
|
9297
|
+
console.log(
|
|
9298
|
+
` ${`${INVOKED_AS_PULSO ? "pulso status" : "pulso-companion status"}`.padEnd(26)} Show companion status`
|
|
9299
|
+
);
|
|
9300
|
+
console.log(
|
|
9301
|
+
` ${COMMAND_CHAT.padEnd(26)} Interactive terminal IDE (default)`
|
|
9302
|
+
);
|
|
8611
9303
|
console.log(` ${`${COMMAND_CHAT} --plain`.padEnd(26)} Plain terminal chat`);
|
|
8612
9304
|
console.log(` ${COMMAND_IDE.padEnd(26)} Full-screen terminal IDE`);
|
|
8613
9305
|
console.log(` ${COMMAND_DAEMON.padEnd(26)} Start companion daemon`);
|
|
@@ -8663,7 +9355,9 @@ if (subCommand === "logout") {
|
|
|
8663
9355
|
if (res.ok) {
|
|
8664
9356
|
console.log("\n Token revoked on server.");
|
|
8665
9357
|
} else {
|
|
8666
|
-
console.log(
|
|
9358
|
+
console.log(
|
|
9359
|
+
"\n Warning: Could not revoke token on server (may already be expired)."
|
|
9360
|
+
);
|
|
8667
9361
|
}
|
|
8668
9362
|
} catch {
|
|
8669
9363
|
console.log("\n Warning: Could not reach server to revoke token.");
|
|
@@ -8730,8 +9424,10 @@ if (!TOKEN) {
|
|
|
8730
9424
|
console.log(" No token found. Starting browser device login...\n");
|
|
8731
9425
|
const creds = await deviceLogin(API_URL);
|
|
8732
9426
|
if (!creds?.token) {
|
|
8733
|
-
console.error(
|
|
8734
|
-
`
|
|
9427
|
+
console.error(
|
|
9428
|
+
` Login failed. Run '${COMMAND_LOGIN}' and approve in browser.
|
|
9429
|
+
`
|
|
9430
|
+
);
|
|
8735
9431
|
process.exit(1);
|
|
8736
9432
|
}
|
|
8737
9433
|
TOKEN = creds.token;
|
|
@@ -8751,10 +9447,73 @@ var WAKE_WORD_SENSITIVITY_RAW = process.env.PULSO_WAKE_WORD_SENSITIVITY ?? proce
|
|
|
8751
9447
|
var WAKE_WORD_VAD_THRESHOLD_RAW = process.env.PULSO_WAKE_WORD_VAD_THRESHOLD ?? process.argv.find((_, i, a) => a[i - 1] === "--wake-word-vad-threshold") ?? "";
|
|
8752
9448
|
var WAKE_WORD_END_SILENCE_MS_RAW = process.env.PULSO_WAKE_WORD_END_SILENCE_MS ?? process.argv.find((_, i, a) => a[i - 1] === "--wake-word-end-silence-ms") ?? "";
|
|
8753
9449
|
var WAKE_WORD_CALIBRATION_MS_RAW = process.env.PULSO_WAKE_WORD_CALIBRATION_MS ?? process.argv.find((_, i, a) => a[i - 1] === "--wake-word-calibration-ms") ?? "";
|
|
8754
|
-
var WAKE_WORD_LOCAL_STT_BUDGET_MS_RAW = process.env.PULSO_WAKE_WORD_LOCAL_STT_BUDGET_MS ?? process.argv.find(
|
|
9450
|
+
var WAKE_WORD_LOCAL_STT_BUDGET_MS_RAW = process.env.PULSO_WAKE_WORD_LOCAL_STT_BUDGET_MS ?? process.argv.find(
|
|
9451
|
+
(_, i, a) => a[i - 1] === "--wake-word-local-stt-budget-ms"
|
|
9452
|
+
) ?? "";
|
|
8755
9453
|
var WS_BASE = API_URL.replace("https://", "wss://").replace("http://", "ws://") + "/ws/companion";
|
|
8756
9454
|
var HOME4 = homedir4();
|
|
8757
9455
|
var RECONNECT_DELAY = 5e3;
|
|
9456
|
+
var COMPANION_LOCK_FILE = join5(CREDENTIALS_DIR, "companion.lock.json");
|
|
9457
|
+
function releaseCompanionLock() {
|
|
9458
|
+
try {
|
|
9459
|
+
if (!existsSync4(COMPANION_LOCK_FILE)) return;
|
|
9460
|
+
const raw = readFileSync4(COMPANION_LOCK_FILE, "utf-8");
|
|
9461
|
+
const parsed = JSON.parse(raw);
|
|
9462
|
+
if (parsed?.pid === process.pid) {
|
|
9463
|
+
unlinkSync5(COMPANION_LOCK_FILE);
|
|
9464
|
+
}
|
|
9465
|
+
} catch {
|
|
9466
|
+
}
|
|
9467
|
+
}
|
|
9468
|
+
function acquireCompanionLock() {
|
|
9469
|
+
try {
|
|
9470
|
+
if (!existsSync4(CREDENTIALS_DIR)) {
|
|
9471
|
+
mkdirSync3(CREDENTIALS_DIR, { recursive: true });
|
|
9472
|
+
}
|
|
9473
|
+
if (existsSync4(COMPANION_LOCK_FILE)) {
|
|
9474
|
+
try {
|
|
9475
|
+
const raw = readFileSync4(COMPANION_LOCK_FILE, "utf-8");
|
|
9476
|
+
const parsed = JSON.parse(raw);
|
|
9477
|
+
const existingPid = Number(parsed?.pid || 0);
|
|
9478
|
+
if (existingPid > 1 && existingPid !== process.pid) {
|
|
9479
|
+
try {
|
|
9480
|
+
process.kill(existingPid, 0);
|
|
9481
|
+
console.log("");
|
|
9482
|
+
console.log(
|
|
9483
|
+
" \u26A0\uFE0F Another Pulso Companion instance is already running."
|
|
9484
|
+
);
|
|
9485
|
+
console.log(
|
|
9486
|
+
` PID: ${existingPid}${parsed?.startedAt ? ` (${parsed.startedAt})` : ""}`
|
|
9487
|
+
);
|
|
9488
|
+
console.log(
|
|
9489
|
+
" Exiting this instance to avoid command collisions.\n"
|
|
9490
|
+
);
|
|
9491
|
+
process.exit(0);
|
|
9492
|
+
} catch {
|
|
9493
|
+
}
|
|
9494
|
+
}
|
|
9495
|
+
} catch {
|
|
9496
|
+
}
|
|
9497
|
+
}
|
|
9498
|
+
writeFileSync5(
|
|
9499
|
+
COMPANION_LOCK_FILE,
|
|
9500
|
+
JSON.stringify(
|
|
9501
|
+
{
|
|
9502
|
+
pid: process.pid,
|
|
9503
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9504
|
+
argv: process.argv.slice(2)
|
|
9505
|
+
},
|
|
9506
|
+
null,
|
|
9507
|
+
2
|
|
9508
|
+
),
|
|
9509
|
+
"utf-8"
|
|
9510
|
+
);
|
|
9511
|
+
if (platform() !== "win32") {
|
|
9512
|
+
chmodSync(COMPANION_LOCK_FILE, 384);
|
|
9513
|
+
}
|
|
9514
|
+
} catch {
|
|
9515
|
+
}
|
|
9516
|
+
}
|
|
8758
9517
|
async function requestWsTicket() {
|
|
8759
9518
|
try {
|
|
8760
9519
|
const res = await fetch(`${API_URL}/ws/ticket`, {
|
|
@@ -8788,27 +9547,27 @@ function runAppleScript2(script) {
|
|
|
8788
9547
|
return new Promise((resolve5, reject) => {
|
|
8789
9548
|
const tmpPath = `/tmp/pulso-as-${Date.now()}-${Math.random().toString(36).slice(2, 6)}.scpt`;
|
|
8790
9549
|
writeFileSync5(tmpPath, script, "utf-8");
|
|
8791
|
-
exec5(
|
|
8792
|
-
|
|
8793
|
-
|
|
8794
|
-
|
|
8795
|
-
try {
|
|
8796
|
-
unlinkSync5(tmpPath);
|
|
8797
|
-
} catch {
|
|
8798
|
-
}
|
|
8799
|
-
if (err) reject(new Error(stderr || err.message));
|
|
8800
|
-
else resolve5(stdout.trim());
|
|
9550
|
+
exec5(`osascript ${tmpPath}`, { timeout: 15e3 }, (err, stdout, stderr) => {
|
|
9551
|
+
try {
|
|
9552
|
+
unlinkSync5(tmpPath);
|
|
9553
|
+
} catch {
|
|
8801
9554
|
}
|
|
8802
|
-
|
|
9555
|
+
if (err) reject(new Error(stderr || err.message));
|
|
9556
|
+
else resolve5(stdout.trim());
|
|
9557
|
+
});
|
|
8803
9558
|
});
|
|
8804
9559
|
}
|
|
8805
9560
|
function runShell4(cmd, timeout = 1e4) {
|
|
8806
9561
|
return new Promise((resolve5, reject) => {
|
|
8807
9562
|
const shell = process.env.SHELL || "/bin/zsh";
|
|
8808
|
-
exec5(
|
|
8809
|
-
|
|
8810
|
-
|
|
8811
|
-
|
|
9563
|
+
exec5(
|
|
9564
|
+
cmd,
|
|
9565
|
+
{ timeout, shell, env: { ...process.env, PATH: augmentedPath2() } },
|
|
9566
|
+
(err, stdout, stderr) => {
|
|
9567
|
+
if (err) reject(new Error(stderr || err.message));
|
|
9568
|
+
else resolve5(stdout.trim());
|
|
9569
|
+
}
|
|
9570
|
+
);
|
|
8812
9571
|
});
|
|
8813
9572
|
}
|
|
8814
9573
|
function augmentedPath2() {
|
|
@@ -8841,6 +9600,21 @@ function runSwift2(code, timeout = 1e4) {
|
|
|
8841
9600
|
child.stdin?.end();
|
|
8842
9601
|
});
|
|
8843
9602
|
}
|
|
9603
|
+
async function hasScreenRecordingPermission2() {
|
|
9604
|
+
try {
|
|
9605
|
+
const out = await runSwift2(
|
|
9606
|
+
`
|
|
9607
|
+
import Cocoa
|
|
9608
|
+
import CoreGraphics
|
|
9609
|
+
print(CGPreflightScreenCaptureAccess() ? "granted" : "denied")
|
|
9610
|
+
`,
|
|
9611
|
+
6e3
|
|
9612
|
+
);
|
|
9613
|
+
return out.trim().toLowerCase() === "granted";
|
|
9614
|
+
} catch {
|
|
9615
|
+
return false;
|
|
9616
|
+
}
|
|
9617
|
+
}
|
|
8844
9618
|
var SETUP_DONE_FILE = join5(HOME4, ".pulso-companion-setup");
|
|
8845
9619
|
async function setupPermissions() {
|
|
8846
9620
|
const isFirstRun = !existsSync4(SETUP_DONE_FILE);
|
|
@@ -8944,12 +9718,59 @@ var ADAPTER_COMMANDS = {
|
|
|
8944
9718
|
sys_open_url: (a, p) => a.openUrl(p.url),
|
|
8945
9719
|
sys_speak: (a, p) => a.speak(p.text, p.voice),
|
|
8946
9720
|
sys_notification: (a, p) => a.notification(p.title, p.message),
|
|
9721
|
+
sys_dialog_action: async (_, p) => {
|
|
9722
|
+
const buttonName = p.button;
|
|
9723
|
+
const procName = p.procName ?? activeDialog?.procName;
|
|
9724
|
+
if (!procName || !buttonName) {
|
|
9725
|
+
return {
|
|
9726
|
+
success: false,
|
|
9727
|
+
error: "No active dialog or button not specified"
|
|
9728
|
+
};
|
|
9729
|
+
}
|
|
9730
|
+
const script = `
|
|
9731
|
+
tell application "System Events"
|
|
9732
|
+
tell process "${procName.replace(/"/g, '\\"')}"
|
|
9733
|
+
set targetWindow to window 1
|
|
9734
|
+
repeat with w in windows
|
|
9735
|
+
try
|
|
9736
|
+
if role of w is "AXSheet" or role of w is "AXDialog" then
|
|
9737
|
+
set targetWindow to w
|
|
9738
|
+
exit repeat
|
|
9739
|
+
end if
|
|
9740
|
+
end try
|
|
9741
|
+
end repeat
|
|
9742
|
+
tell targetWindow
|
|
9743
|
+
click button "${buttonName.replace(/"/g, '\\"')}"
|
|
9744
|
+
end tell
|
|
9745
|
+
end tell
|
|
9746
|
+
end tell`;
|
|
9747
|
+
try {
|
|
9748
|
+
await runAppleScript2(script);
|
|
9749
|
+
console.log(
|
|
9750
|
+
` \u2713 Dialog action: clicked "${buttonName}" in [${procName}]`
|
|
9751
|
+
);
|
|
9752
|
+
activeDialog = null;
|
|
9753
|
+
lastDialogSignature = null;
|
|
9754
|
+
return { success: true, clicked: buttonName };
|
|
9755
|
+
} catch (e) {
|
|
9756
|
+
return { success: false, error: e.message };
|
|
9757
|
+
}
|
|
9758
|
+
},
|
|
8947
9759
|
sys_clipboard_read: (a, _) => a.clipboardRead(),
|
|
8948
9760
|
sys_clipboard_write: (a, p) => a.clipboardWrite(p.text),
|
|
8949
9761
|
sys_screenshot: (a, _) => a.screenshot(),
|
|
8950
|
-
sys_mouse_click: (a, p) => a.mouseClick(
|
|
9762
|
+
sys_mouse_click: (a, p) => a.mouseClick(
|
|
9763
|
+
Number(p.x),
|
|
9764
|
+
Number(p.y),
|
|
9765
|
+
p.button ?? "left"
|
|
9766
|
+
),
|
|
8951
9767
|
sys_mouse_double_click: (a, p) => a.mouseDoubleClick(Number(p.x), Number(p.y)),
|
|
8952
|
-
sys_mouse_scroll: (a, p) => a.mouseScroll(
|
|
9768
|
+
sys_mouse_scroll: (a, p) => a.mouseScroll(
|
|
9769
|
+
Number(p.scrollY),
|
|
9770
|
+
Number(p.scrollX) || 0,
|
|
9771
|
+
Number(p.x) || 0,
|
|
9772
|
+
Number(p.y) || 0
|
|
9773
|
+
),
|
|
8953
9774
|
sys_mouse_move: (a, p) => a.mouseMove(Number(p.x), Number(p.y)),
|
|
8954
9775
|
sys_drag: (a, p) => a.drag(Number(p.fromX), Number(p.fromY), Number(p.toX), Number(p.toY)),
|
|
8955
9776
|
sys_get_cursor_position: (a, _) => a.getCursorPosition(),
|
|
@@ -8962,8 +9783,12 @@ var ADAPTER_COMMANDS = {
|
|
|
8962
9783
|
sys_browser_list_tabs: (a, _) => a.browserListTabs(),
|
|
8963
9784
|
sys_browser_navigate: (a, p) => a.browserNavigate(p.url, p.browser),
|
|
8964
9785
|
sys_browser_new_tab: (a, p) => a.browserNewTab(p.url, p.browser),
|
|
8965
|
-
sys_browser_read_page: (a, p) => a.browserReadPage(
|
|
9786
|
+
sys_browser_read_page: (a, p) => a.browserReadPage(
|
|
9787
|
+
p.browser,
|
|
9788
|
+
Number(p.maxLength) || void 0
|
|
9789
|
+
),
|
|
8966
9790
|
sys_browser_execute_js: (a, p) => a.browserExecuteJs(p.code, p.browser),
|
|
9791
|
+
sys_browser_list_profiles: (a, _) => a.browserListProfiles(),
|
|
8967
9792
|
sys_calendar_list: (a, p) => a.calendarList(Number(p.days) || void 0),
|
|
8968
9793
|
sys_calendar_create: (a, p) => a.calendarCreate(
|
|
8969
9794
|
p.title ?? p.summary,
|
|
@@ -8981,26 +9806,61 @@ var ADAPTER_COMMANDS = {
|
|
|
8981
9806
|
sys_imessage_send: (a, p) => a.sendMessage(p.to, p.message),
|
|
8982
9807
|
sys_system_info: (a, _) => a.getSystemInfo(),
|
|
8983
9808
|
sys_dnd: (a, p) => a.dnd(p.enabled),
|
|
8984
|
-
sys_shell: (a, p) => a.shell(
|
|
9809
|
+
sys_shell: (a, p) => a.shell(
|
|
9810
|
+
p.command,
|
|
9811
|
+
p.cwd,
|
|
9812
|
+
Number(p.timeout) || void 0
|
|
9813
|
+
),
|
|
8985
9814
|
sys_run_shortcut: (a, p) => a.runShortcut(p.name, p.input),
|
|
8986
9815
|
sys_file_read: (a, p) => a.fileRead(p.path),
|
|
8987
9816
|
sys_file_write: (a, p) => a.fileWrite(p.path, p.content),
|
|
8988
|
-
sys_file_list: (a, p) => a.fileList(
|
|
8989
|
-
|
|
8990
|
-
|
|
9817
|
+
sys_file_list: (a, p) => a.fileList(
|
|
9818
|
+
p.path,
|
|
9819
|
+
p.showHidden
|
|
9820
|
+
),
|
|
9821
|
+
sys_file_move: (a, p) => a.fileMove(
|
|
9822
|
+
p.source ?? p.from,
|
|
9823
|
+
p.destination ?? p.to
|
|
9824
|
+
),
|
|
9825
|
+
sys_file_copy: (a, p) => a.fileCopy(
|
|
9826
|
+
p.source ?? p.from,
|
|
9827
|
+
p.destination ?? p.to
|
|
9828
|
+
),
|
|
8991
9829
|
sys_file_delete: (a, p) => a.fileDelete(p.path),
|
|
8992
9830
|
sys_file_info: (a, p) => a.fileInfo(p.path),
|
|
8993
|
-
sys_download: (a, p) => a.download(
|
|
9831
|
+
sys_download: (a, p) => a.download(
|
|
9832
|
+
p.url,
|
|
9833
|
+
p.destination ?? p.path ?? void 0
|
|
9834
|
+
),
|
|
8994
9835
|
sys_window_list: (a, _) => a.windowList(),
|
|
8995
9836
|
sys_window_focus: (a, p) => a.windowFocus(p.app),
|
|
8996
|
-
sys_window_resize: (a, p) => a.windowResize(
|
|
9837
|
+
sys_window_resize: (a, p) => a.windowResize(
|
|
9838
|
+
p.app,
|
|
9839
|
+
Number(p.x) || void 0,
|
|
9840
|
+
Number(p.y) || void 0,
|
|
9841
|
+
Number(p.width) || void 0,
|
|
9842
|
+
Number(p.height) || void 0
|
|
9843
|
+
),
|
|
8997
9844
|
sys_notes_list: (a, p) => a.notesList(Number(p.limit) || void 0),
|
|
8998
|
-
sys_notes_create: (a, p) => a.notesCreate(
|
|
9845
|
+
sys_notes_create: (a, p) => a.notesCreate(
|
|
9846
|
+
p.title,
|
|
9847
|
+
p.body,
|
|
9848
|
+
p.folder
|
|
9849
|
+
),
|
|
8999
9850
|
sys_contacts_search: (a, p) => a.contactsSearch(p.query),
|
|
9000
9851
|
sys_ocr: (a, p) => a.ocr(p.imagePath ?? p.path),
|
|
9001
|
-
sys_email_send: (a, p) => a.emailSend(
|
|
9852
|
+
sys_email_send: (a, p) => a.emailSend(
|
|
9853
|
+
p.to,
|
|
9854
|
+
p.subject,
|
|
9855
|
+
p.body,
|
|
9856
|
+
p.method
|
|
9857
|
+
),
|
|
9002
9858
|
sys_spotify: (a, p) => a.spotify(p.action, p),
|
|
9003
|
-
sys_hue_lights_on: (a, p) => a.hueLightsOn(
|
|
9859
|
+
sys_hue_lights_on: (a, p) => a.hueLightsOn(
|
|
9860
|
+
p.light,
|
|
9861
|
+
Number(p.brightness) || void 0,
|
|
9862
|
+
p.color
|
|
9863
|
+
),
|
|
9004
9864
|
sys_hue_lights_off: (a, p) => a.hueLightsOff(p.light),
|
|
9005
9865
|
sys_hue_lights_color: (a, p) => a.hueLightsColor(p.light, p.color),
|
|
9006
9866
|
sys_hue_lights_brightness: (a, p) => a.hueLightsBrightness(p.light, Number(p.brightness)),
|
|
@@ -9009,7 +9869,11 @@ var ADAPTER_COMMANDS = {
|
|
|
9009
9869
|
sys_sonos_play: (a, p) => a.sonosPlay(p.room),
|
|
9010
9870
|
sys_sonos_pause: (a, p) => a.sonosPause(p.room),
|
|
9011
9871
|
sys_sonos_volume: (a, p) => a.sonosVolume(p.room, Number(p.level)),
|
|
9012
|
-
sys_sonos_play_uri: (a, p) => a.sonosPlayUri(
|
|
9872
|
+
sys_sonos_play_uri: (a, p) => a.sonosPlayUri(
|
|
9873
|
+
p.room,
|
|
9874
|
+
p.uri,
|
|
9875
|
+
p.title
|
|
9876
|
+
),
|
|
9013
9877
|
sys_sonos_rooms: (a, _) => a.sonosRooms(),
|
|
9014
9878
|
sys_sonos_next: (a, p) => a.sonosNext(p.room),
|
|
9015
9879
|
sys_sonos_previous: (a, p) => a.sonosPrevious(p.room),
|
|
@@ -9024,7 +9888,16 @@ var ADAPTER_COMMANDS = {
|
|
|
9024
9888
|
return level !== void 0 ? a.setBrightness(Number(level)) : a.getBrightness();
|
|
9025
9889
|
}
|
|
9026
9890
|
};
|
|
9027
|
-
|
|
9891
|
+
var claudePipeQueue = Promise.resolve();
|
|
9892
|
+
function runClaudePipeSerial(task) {
|
|
9893
|
+
const run = claudePipeQueue.then(task, task);
|
|
9894
|
+
claudePipeQueue = run.then(
|
|
9895
|
+
() => void 0,
|
|
9896
|
+
() => void 0
|
|
9897
|
+
);
|
|
9898
|
+
return run;
|
|
9899
|
+
}
|
|
9900
|
+
async function handleCommand(command, params, streamCb) {
|
|
9028
9901
|
try {
|
|
9029
9902
|
if (command === "ollama_detect") {
|
|
9030
9903
|
try {
|
|
@@ -9035,7 +9908,10 @@ async function handleCommand(command, params) {
|
|
|
9035
9908
|
});
|
|
9036
9909
|
clearTimeout(timeout);
|
|
9037
9910
|
if (!res.ok) {
|
|
9038
|
-
return {
|
|
9911
|
+
return {
|
|
9912
|
+
success: true,
|
|
9913
|
+
data: { running: false, error: `HTTP ${res.status}` }
|
|
9914
|
+
};
|
|
9039
9915
|
}
|
|
9040
9916
|
const data = await res.json();
|
|
9041
9917
|
const models = (data.models ?? []).map((m) => ({
|
|
@@ -9046,7 +9922,12 @@ async function handleCommand(command, params) {
|
|
|
9046
9922
|
}));
|
|
9047
9923
|
return {
|
|
9048
9924
|
success: true,
|
|
9049
|
-
data: {
|
|
9925
|
+
data: {
|
|
9926
|
+
running: true,
|
|
9927
|
+
url: "http://localhost:11434",
|
|
9928
|
+
modelCount: models.length,
|
|
9929
|
+
models
|
|
9930
|
+
}
|
|
9050
9931
|
};
|
|
9051
9932
|
} catch {
|
|
9052
9933
|
return { success: true, data: { running: false } };
|
|
@@ -9061,7 +9942,10 @@ async function handleCommand(command, params) {
|
|
|
9061
9942
|
return { success: true, data: result };
|
|
9062
9943
|
}
|
|
9063
9944
|
if (adapter.platform !== "macos") {
|
|
9064
|
-
return {
|
|
9945
|
+
return {
|
|
9946
|
+
success: false,
|
|
9947
|
+
error: `Command ${command} is not available on ${adapter.platform}. It is currently macOS-only.`
|
|
9948
|
+
};
|
|
9065
9949
|
}
|
|
9066
9950
|
switch (command) {
|
|
9067
9951
|
// ── macOS-only commands (not in the cross-platform adapter interface) ──
|
|
@@ -9072,25 +9956,54 @@ async function handleCommand(command, params) {
|
|
|
9072
9956
|
const rh = params.height;
|
|
9073
9957
|
if (rx == null || ry == null || rw == null || rh == null)
|
|
9074
9958
|
return { success: false, error: "Missing x, y, width, or height" };
|
|
9959
|
+
const screenPermitted = await hasScreenRecordingPermission2();
|
|
9960
|
+
if (!screenPermitted) {
|
|
9961
|
+
return {
|
|
9962
|
+
success: false,
|
|
9963
|
+
error: "Screen Recording permission is not granted. Enable Pulso Companion in System Settings -> Privacy & Security -> Screen Recording, then reopen the app.",
|
|
9964
|
+
errorCode: "SCREEN_PERMISSION_REQUIRED"
|
|
9965
|
+
};
|
|
9966
|
+
}
|
|
9075
9967
|
const ts2 = Date.now();
|
|
9076
9968
|
const regPath = `/tmp/pulso-ss-region-${ts2}.png`;
|
|
9077
9969
|
const regJpg = `/tmp/pulso-ss-region-${ts2}.jpg`;
|
|
9078
9970
|
try {
|
|
9079
|
-
await runShell4(
|
|
9971
|
+
await runShell4(
|
|
9972
|
+
`screencapture -x -R${rx},${ry},${rw},${rh} ${regPath}`,
|
|
9973
|
+
15e3
|
|
9974
|
+
);
|
|
9080
9975
|
} catch (e) {
|
|
9081
|
-
return {
|
|
9976
|
+
return {
|
|
9977
|
+
success: false,
|
|
9978
|
+
error: `Region screenshot failed: ${e.message}`,
|
|
9979
|
+
errorCode: "SCREENSHOT_FAILED"
|
|
9980
|
+
};
|
|
9082
9981
|
}
|
|
9083
9982
|
if (!existsSync4(regPath))
|
|
9084
|
-
return {
|
|
9983
|
+
return {
|
|
9984
|
+
success: false,
|
|
9985
|
+
error: "Region screenshot failed",
|
|
9986
|
+
errorCode: "SCREENSHOT_FAILED"
|
|
9987
|
+
};
|
|
9085
9988
|
try {
|
|
9086
|
-
await runShell4(
|
|
9989
|
+
await runShell4(
|
|
9990
|
+
`sips --setProperty format jpeg --setProperty formatOptions 85 ${regPath} --out ${regJpg}`,
|
|
9991
|
+
1e4
|
|
9992
|
+
);
|
|
9087
9993
|
} catch {
|
|
9088
9994
|
const rb = readFileSync4(regPath);
|
|
9089
9995
|
try {
|
|
9090
9996
|
unlinkSync5(regPath);
|
|
9091
9997
|
} catch {
|
|
9092
9998
|
}
|
|
9093
|
-
return {
|
|
9999
|
+
return {
|
|
10000
|
+
success: true,
|
|
10001
|
+
data: {
|
|
10002
|
+
image: `data:image/png;base64,${rb.toString("base64")}`,
|
|
10003
|
+
format: "png",
|
|
10004
|
+
region: { x: rx, y: ry, width: rw, height: rh }
|
|
10005
|
+
}
|
|
10006
|
+
};
|
|
9094
10007
|
}
|
|
9095
10008
|
const rb2 = readFileSync4(regJpg);
|
|
9096
10009
|
try {
|
|
@@ -9128,7 +10041,16 @@ for (i, screen) in screens.enumerated() {
|
|
|
9128
10041
|
print(result)`;
|
|
9129
10042
|
const raw = await runSwift2(swift, 15e3);
|
|
9130
10043
|
const displays = raw.trim().split("\n").filter(Boolean).map((line) => {
|
|
9131
|
-
const [
|
|
10044
|
+
const [
|
|
10045
|
+
index,
|
|
10046
|
+
name,
|
|
10047
|
+
origin,
|
|
10048
|
+
size,
|
|
10049
|
+
visOrigin,
|
|
10050
|
+
visSize,
|
|
10051
|
+
scale,
|
|
10052
|
+
isMain
|
|
10053
|
+
] = line.split("|");
|
|
9132
10054
|
const [ox, oy] = (origin || "0,0").split(",").map(Number);
|
|
9133
10055
|
const [sw2, sh2] = (size || "0,0").split(",").map(Number);
|
|
9134
10056
|
const [vox, voy] = (visOrigin || "0,0").split(",").map(Number);
|
|
@@ -9157,7 +10079,10 @@ print(result)`;
|
|
|
9157
10079
|
}
|
|
9158
10080
|
};
|
|
9159
10081
|
} catch (e) {
|
|
9160
|
-
return {
|
|
10082
|
+
return {
|
|
10083
|
+
success: false,
|
|
10084
|
+
error: `Failed to list displays: ${e.message}`
|
|
10085
|
+
};
|
|
9161
10086
|
}
|
|
9162
10087
|
}
|
|
9163
10088
|
// ── NEW: System Settings ──────────────────────────────
|
|
@@ -9195,7 +10120,9 @@ print(result)`;
|
|
|
9195
10120
|
`open "x-apple.systempreferences:com.apple.settings.${pane}" 2>/dev/null || open "x-apple.systempreferences:${paneId}" 2>/dev/null || open "System Preferences"`
|
|
9196
10121
|
);
|
|
9197
10122
|
} else {
|
|
9198
|
-
await runShell4(
|
|
10123
|
+
await runShell4(
|
|
10124
|
+
`open "System Preferences" 2>/dev/null || open -a "System Settings"`
|
|
10125
|
+
);
|
|
9199
10126
|
}
|
|
9200
10127
|
return { success: true, data: { pane: pane || "main" } };
|
|
9201
10128
|
}
|
|
@@ -9393,9 +10320,7 @@ print(result)`;
|
|
|
9393
10320
|
const device = params.device;
|
|
9394
10321
|
if (!device) return { success: false, error: "Missing device name" };
|
|
9395
10322
|
try {
|
|
9396
|
-
await runShell4(
|
|
9397
|
-
`SwitchAudioSource -s "${device}" 2>/dev/null`
|
|
9398
|
-
);
|
|
10323
|
+
await runShell4(`SwitchAudioSource -s "${device}" 2>/dev/null`);
|
|
9399
10324
|
return { success: true, data: { switched: device } };
|
|
9400
10325
|
} catch {
|
|
9401
10326
|
return {
|
|
@@ -9417,12 +10342,8 @@ print(result)`;
|
|
|
9417
10342
|
case "sys_trash": {
|
|
9418
10343
|
const trashAction = params.action || "info";
|
|
9419
10344
|
if (trashAction === "info") {
|
|
9420
|
-
const count = await runShell4(
|
|
9421
|
-
|
|
9422
|
-
);
|
|
9423
|
-
const size = await runShell4(
|
|
9424
|
-
`du -sh ~/.Trash 2>/dev/null | cut -f1`
|
|
9425
|
-
);
|
|
10345
|
+
const count = await runShell4(`ls -1 ~/.Trash 2>/dev/null | wc -l`);
|
|
10346
|
+
const size = await runShell4(`du -sh ~/.Trash 2>/dev/null | cut -f1`);
|
|
9426
10347
|
return {
|
|
9427
10348
|
success: true,
|
|
9428
10349
|
data: {
|
|
@@ -9431,9 +10352,7 @@ print(result)`;
|
|
|
9431
10352
|
}
|
|
9432
10353
|
};
|
|
9433
10354
|
} else if (trashAction === "empty") {
|
|
9434
|
-
await runAppleScript2(
|
|
9435
|
-
`tell application "Finder" to empty the trash`
|
|
9436
|
-
);
|
|
10355
|
+
await runAppleScript2(`tell application "Finder" to empty the trash`);
|
|
9437
10356
|
return { success: true, data: { emptied: true } };
|
|
9438
10357
|
}
|
|
9439
10358
|
return { success: false, error: "Use action: info or empty" };
|
|
@@ -9465,9 +10384,7 @@ print(result)`;
|
|
|
9465
10384
|
case "sys_disk_info": {
|
|
9466
10385
|
const df = await runShell4(`df -h / | tail -1`);
|
|
9467
10386
|
const parts = df.trim().split(/\s+/);
|
|
9468
|
-
const volumes = await runShell4(
|
|
9469
|
-
`ls -1 /Volumes 2>/dev/null`
|
|
9470
|
-
);
|
|
10387
|
+
const volumes = await runShell4(`ls -1 /Volumes 2>/dev/null`);
|
|
9471
10388
|
return {
|
|
9472
10389
|
success: true,
|
|
9473
10390
|
data: {
|
|
@@ -9497,9 +10414,7 @@ print(result)`;
|
|
|
9497
10414
|
await runShell4(`shortcuts run "Set ${mode}" 2>/dev/null`);
|
|
9498
10415
|
return { success: true, data: { mode } };
|
|
9499
10416
|
} catch {
|
|
9500
|
-
await runShell4(
|
|
9501
|
-
`shortcuts run "Toggle Do Not Disturb" 2>/dev/null`
|
|
9502
|
-
);
|
|
10417
|
+
await runShell4(`shortcuts run "Toggle Do Not Disturb" 2>/dev/null`);
|
|
9503
10418
|
return {
|
|
9504
10419
|
success: true,
|
|
9505
10420
|
data: {
|
|
@@ -9564,13 +10479,18 @@ print(result)`;
|
|
|
9564
10479
|
await runShell4(`pmset sleepnow`);
|
|
9565
10480
|
return { success: true, data: { sleeping: true } };
|
|
9566
10481
|
}
|
|
9567
|
-
return {
|
|
10482
|
+
return {
|
|
10483
|
+
success: false,
|
|
10484
|
+
error: "Use action: status, caffeinate, sleep"
|
|
10485
|
+
};
|
|
9568
10486
|
}
|
|
9569
10487
|
// ── NEW: Printer Management ─────────────────────────────
|
|
9570
10488
|
case "sys_printer": {
|
|
9571
10489
|
const prAction = params.action || "list";
|
|
9572
10490
|
if (prAction === "list") {
|
|
9573
|
-
const result = await runShell4(
|
|
10491
|
+
const result = await runShell4(
|
|
10492
|
+
`lpstat -p 2>/dev/null || echo "No printers"`
|
|
10493
|
+
);
|
|
9574
10494
|
return { success: true, data: { printers: result.trim() } };
|
|
9575
10495
|
} else if (prAction === "print") {
|
|
9576
10496
|
const file = params.file;
|
|
@@ -9703,7 +10623,9 @@ print(result.stdout[:5000])
|
|
|
9703
10623
|
} else if (svcAction === "start") {
|
|
9704
10624
|
const name = params.name;
|
|
9705
10625
|
if (!name) return { success: false, error: "Missing service name" };
|
|
9706
|
-
await runShell4(
|
|
10626
|
+
await runShell4(
|
|
10627
|
+
`launchctl kickstart gui/$(id -u)/${name} 2>/dev/null`
|
|
10628
|
+
);
|
|
9707
10629
|
return { success: true, data: { started: name } };
|
|
9708
10630
|
} else if (svcAction === "stop") {
|
|
9709
10631
|
const name = params.name;
|
|
@@ -9722,71 +10644,200 @@ print(result.stdout[:5000])
|
|
|
9722
10644
|
const model = params.model;
|
|
9723
10645
|
const maxTurns = params.max_turns;
|
|
9724
10646
|
const systemPrompt = params.system_prompt;
|
|
9725
|
-
const
|
|
9726
|
-
const
|
|
9727
|
-
const
|
|
9728
|
-
|
|
9729
|
-
|
|
9730
|
-
|
|
9731
|
-
|
|
9732
|
-
|
|
9733
|
-
|
|
9734
|
-
|
|
9735
|
-
|
|
9736
|
-
|
|
9737
|
-
|
|
9738
|
-
|
|
9739
|
-
|
|
9740
|
-
|
|
9741
|
-
|
|
10647
|
+
const effort = params.effort || "low";
|
|
10648
|
+
const outputFormat = params.output_format || "text";
|
|
10649
|
+
const timeout = Number(params.timeout) || 18e4;
|
|
10650
|
+
const home = process.env.HOME || "";
|
|
10651
|
+
const claudePaths = [
|
|
10652
|
+
`${home}/.local/bin/claude`,
|
|
10653
|
+
`${home}/.local/share/claude/versions/latest/claude`,
|
|
10654
|
+
"/usr/local/bin/claude",
|
|
10655
|
+
"/opt/homebrew/bin/claude"
|
|
10656
|
+
];
|
|
10657
|
+
let claudeBin = "claude";
|
|
10658
|
+
for (const p of claudePaths) {
|
|
10659
|
+
try {
|
|
10660
|
+
execSync3(`test -x "${p}"`, { stdio: "ignore" });
|
|
10661
|
+
claudeBin = p;
|
|
10662
|
+
break;
|
|
10663
|
+
} catch {
|
|
10664
|
+
}
|
|
10665
|
+
}
|
|
10666
|
+
const useStreamJson = Boolean(streamCb);
|
|
10667
|
+
const effectiveOutputFormat = useStreamJson ? "stream-json" : outputFormat;
|
|
10668
|
+
const args = ["-p", "--output-format", effectiveOutputFormat];
|
|
10669
|
+
if (useStreamJson) {
|
|
10670
|
+
args.push("--verbose", "--include-partial-messages");
|
|
10671
|
+
}
|
|
10672
|
+
args.push("--tools", "");
|
|
10673
|
+
if (systemPrompt && systemPrompt.trim()) {
|
|
10674
|
+
args.push("--system-prompt", systemPrompt.trim());
|
|
10675
|
+
}
|
|
10676
|
+
if (effort) args.push("--effort", effort);
|
|
10677
|
+
if (model) args.push("--model", model);
|
|
10678
|
+
if (maxTurns) args.push("--max-turns", String(maxTurns));
|
|
10679
|
+
const promptInput = prompt;
|
|
10680
|
+
return runClaudePipeSerial(
|
|
10681
|
+
() => new Promise((resolve5) => {
|
|
10682
|
+
let stdout = "";
|
|
10683
|
+
let stderr = "";
|
|
10684
|
+
let streamBuffer = "";
|
|
10685
|
+
let streamedResponse = "";
|
|
10686
|
+
let finalResultText = "";
|
|
10687
|
+
let assistantSnapshotText = "";
|
|
10688
|
+
const processJsonLine = (line) => {
|
|
10689
|
+
const trimmed = line.trim();
|
|
10690
|
+
if (!trimmed) return;
|
|
10691
|
+
let evt = null;
|
|
9742
10692
|
try {
|
|
9743
|
-
|
|
9744
|
-
|
|
10693
|
+
evt = JSON.parse(trimmed);
|
|
10694
|
+
} catch {
|
|
10695
|
+
return;
|
|
10696
|
+
}
|
|
10697
|
+
const deltaType = evt?.event?.delta?.type;
|
|
10698
|
+
if (evt?.type === "stream_event" && deltaType === "text_delta") {
|
|
10699
|
+
const deltaText = String(evt?.event?.delta?.text ?? "");
|
|
10700
|
+
if (deltaText) {
|
|
10701
|
+
streamedResponse += deltaText;
|
|
10702
|
+
if (streamCb) streamCb(deltaText);
|
|
10703
|
+
}
|
|
10704
|
+
return;
|
|
10705
|
+
}
|
|
10706
|
+
if (evt?.type === "result" && typeof evt?.result === "string") {
|
|
10707
|
+
finalResultText = evt.result;
|
|
10708
|
+
return;
|
|
10709
|
+
}
|
|
10710
|
+
if (evt?.type === "assistant" && Array.isArray(evt?.message?.content)) {
|
|
10711
|
+
const textBlocks = evt.message.content.filter(
|
|
10712
|
+
(p) => p?.type === "text" && typeof p?.text === "string"
|
|
10713
|
+
).map((p) => p.text);
|
|
10714
|
+
if (textBlocks.length > 0) {
|
|
10715
|
+
assistantSnapshotText = textBlocks.join("");
|
|
10716
|
+
}
|
|
10717
|
+
}
|
|
10718
|
+
};
|
|
10719
|
+
const childEnv = {
|
|
10720
|
+
...process.env,
|
|
10721
|
+
PATH: `${home}/.local/bin:${process.env.PATH}`
|
|
10722
|
+
};
|
|
10723
|
+
delete childEnv.CLAUDECODE;
|
|
10724
|
+
delete childEnv.CLAUDE_CODE;
|
|
10725
|
+
const child = spawn(claudeBin, args, {
|
|
10726
|
+
env: childEnv,
|
|
10727
|
+
timeout,
|
|
10728
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
10729
|
+
});
|
|
10730
|
+
child.stdout.on("data", (chunk) => {
|
|
10731
|
+
const text = chunk.toString();
|
|
10732
|
+
stdout += text;
|
|
10733
|
+
if (useStreamJson) {
|
|
10734
|
+
streamBuffer += text;
|
|
10735
|
+
const lines = streamBuffer.split(/\r?\n/);
|
|
10736
|
+
streamBuffer = lines.pop() || "";
|
|
10737
|
+
for (const ln of lines) {
|
|
10738
|
+
processJsonLine(ln);
|
|
10739
|
+
}
|
|
10740
|
+
} else if (streamCb) {
|
|
10741
|
+
streamCb(text);
|
|
10742
|
+
}
|
|
10743
|
+
});
|
|
10744
|
+
child.stderr.on("data", (chunk) => {
|
|
10745
|
+
stderr += chunk.toString();
|
|
10746
|
+
});
|
|
10747
|
+
child.on("close", (code) => {
|
|
10748
|
+
if (code !== 0) {
|
|
10749
|
+
const detail = (stderr.trim() || stdout.trim() || "Unknown error").slice(0, 500);
|
|
10750
|
+
const errorCode = detail.toLowerCase().includes("out of extra usage") ? "CLAUDE_USAGE_LIMIT" : "CLAUDE_PIPE_FAILED";
|
|
10751
|
+
resolve5({
|
|
10752
|
+
success: false,
|
|
10753
|
+
error: `Claude pipe error (exit ${code}): ${detail}`,
|
|
10754
|
+
errorCode
|
|
10755
|
+
});
|
|
10756
|
+
return;
|
|
10757
|
+
}
|
|
10758
|
+
try {
|
|
10759
|
+
if (effectiveOutputFormat === "stream-json") {
|
|
10760
|
+
if (streamBuffer.trim()) {
|
|
10761
|
+
processJsonLine(streamBuffer);
|
|
10762
|
+
}
|
|
10763
|
+
const response = (finalResultText || streamedResponse || assistantSnapshotText || "").trim();
|
|
9745
10764
|
resolve5({
|
|
9746
10765
|
success: true,
|
|
9747
10766
|
data: {
|
|
9748
|
-
response
|
|
9749
|
-
session_id: parsed.session_id,
|
|
9750
|
-
cost_usd: parsed.total_cost_usd ?? 0,
|
|
9751
|
-
duration_ms: parsed.duration_ms,
|
|
9752
|
-
num_turns: parsed.num_turns,
|
|
10767
|
+
response,
|
|
9753
10768
|
model: model || "default",
|
|
9754
10769
|
via: "claude-max-subscription"
|
|
9755
10770
|
}
|
|
9756
10771
|
});
|
|
9757
|
-
|
|
10772
|
+
return;
|
|
10773
|
+
}
|
|
10774
|
+
if (outputFormat === "json") {
|
|
10775
|
+
const parsed = JSON.parse(stdout);
|
|
9758
10776
|
resolve5({
|
|
9759
10777
|
success: true,
|
|
9760
10778
|
data: {
|
|
9761
|
-
response: stdout.trim(),
|
|
10779
|
+
response: parsed.result || stdout.trim(),
|
|
10780
|
+
session_id: parsed.session_id,
|
|
10781
|
+
cost_usd: 0,
|
|
10782
|
+
duration_ms: parsed.duration_ms,
|
|
10783
|
+
num_turns: parsed.num_turns,
|
|
9762
10784
|
model: model || "default",
|
|
9763
10785
|
via: "claude-max-subscription"
|
|
9764
10786
|
}
|
|
9765
10787
|
});
|
|
10788
|
+
return;
|
|
9766
10789
|
}
|
|
9767
10790
|
} catch {
|
|
9768
|
-
resolve5({
|
|
9769
|
-
success: true,
|
|
9770
|
-
data: {
|
|
9771
|
-
response: stdout.trim(),
|
|
9772
|
-
model: model || "default",
|
|
9773
|
-
via: "claude-max-subscription"
|
|
9774
|
-
}
|
|
9775
|
-
});
|
|
9776
10791
|
}
|
|
9777
|
-
|
|
9778
|
-
|
|
9779
|
-
|
|
9780
|
-
|
|
9781
|
-
|
|
10792
|
+
resolve5({
|
|
10793
|
+
success: true,
|
|
10794
|
+
data: {
|
|
10795
|
+
response: stdout.trim(),
|
|
10796
|
+
model: model || "default",
|
|
10797
|
+
via: "claude-max-subscription"
|
|
10798
|
+
}
|
|
10799
|
+
});
|
|
10800
|
+
});
|
|
10801
|
+
child.on("error", (err) => {
|
|
10802
|
+
resolve5({
|
|
10803
|
+
success: false,
|
|
10804
|
+
error: `Claude pipe spawn error: ${err.message}`,
|
|
10805
|
+
errorCode: "CLAUDE_PIPE_FAILED"
|
|
10806
|
+
});
|
|
10807
|
+
});
|
|
10808
|
+
child.stdin.write(promptInput);
|
|
10809
|
+
child.stdin.end();
|
|
10810
|
+
})
|
|
10811
|
+
);
|
|
9782
10812
|
}
|
|
9783
10813
|
case "sys_claude_status": {
|
|
9784
10814
|
try {
|
|
9785
10815
|
const version = await runShell4("claude --version 2>/dev/null", 5e3);
|
|
9786
10816
|
let authStatus = "unknown";
|
|
10817
|
+
let authDetails;
|
|
9787
10818
|
try {
|
|
9788
10819
|
const status = await runShell4("claude auth status 2>&1", 1e4);
|
|
9789
|
-
|
|
10820
|
+
const trimmed = status.trim();
|
|
10821
|
+
let parsed = null;
|
|
10822
|
+
try {
|
|
10823
|
+
parsed = JSON.parse(trimmed);
|
|
10824
|
+
} catch {
|
|
10825
|
+
parsed = null;
|
|
10826
|
+
}
|
|
10827
|
+
if (parsed && typeof parsed === "object") {
|
|
10828
|
+
const loggedIn = parsed.loggedIn === true;
|
|
10829
|
+
authStatus = loggedIn ? "authenticated" : "not_authenticated";
|
|
10830
|
+
authDetails = {
|
|
10831
|
+
authMethod: typeof parsed.authMethod === "string" ? parsed.authMethod : void 0,
|
|
10832
|
+
apiProvider: typeof parsed.apiProvider === "string" ? parsed.apiProvider : void 0,
|
|
10833
|
+
email: typeof parsed.email === "string" ? parsed.email : void 0,
|
|
10834
|
+
orgId: typeof parsed.orgId === "string" ? parsed.orgId : void 0,
|
|
10835
|
+
subscriptionType: typeof parsed.subscriptionType === "string" ? parsed.subscriptionType : void 0
|
|
10836
|
+
};
|
|
10837
|
+
} else {
|
|
10838
|
+
const lower = trimmed.toLowerCase();
|
|
10839
|
+
authStatus = lower.includes("authenticated") || lower.includes("logged in") || lower.includes('loggedin":true') ? "authenticated" : "not_authenticated";
|
|
10840
|
+
}
|
|
9790
10841
|
} catch {
|
|
9791
10842
|
authStatus = "not_authenticated";
|
|
9792
10843
|
}
|
|
@@ -9796,7 +10847,8 @@ print(result.stdout[:5000])
|
|
|
9796
10847
|
installed: true,
|
|
9797
10848
|
version: version.trim(),
|
|
9798
10849
|
authenticated: authStatus === "authenticated",
|
|
9799
|
-
status: authStatus
|
|
10850
|
+
status: authStatus,
|
|
10851
|
+
...authDetails ? { details: authDetails } : {}
|
|
9800
10852
|
}
|
|
9801
10853
|
};
|
|
9802
10854
|
} catch {
|
|
@@ -9817,12 +10869,18 @@ print(result.stdout[:5000])
|
|
|
9817
10869
|
const version = await runShell4("codex --version 2>/dev/null", 5e3);
|
|
9818
10870
|
let authStatus = "unknown";
|
|
9819
10871
|
try {
|
|
9820
|
-
const status = await runShell4(
|
|
10872
|
+
const status = await runShell4(
|
|
10873
|
+
"codex auth whoami 2>&1 || codex --help 2>&1 | head -5",
|
|
10874
|
+
1e4
|
|
10875
|
+
);
|
|
9821
10876
|
const lc = status.toLowerCase();
|
|
9822
10877
|
authStatus = lc.includes("not logged in") || lc.includes("not authenticated") || lc.includes("sign in") || lc.includes("no api key") ? "not_authenticated" : "authenticated";
|
|
9823
10878
|
} catch {
|
|
9824
10879
|
try {
|
|
9825
|
-
await runShell4(
|
|
10880
|
+
await runShell4(
|
|
10881
|
+
"security find-generic-password -s 'openai-codex' 2>/dev/null || security find-generic-password -s 'codex' 2>/dev/null",
|
|
10882
|
+
5e3
|
|
10883
|
+
);
|
|
9826
10884
|
authStatus = "authenticated";
|
|
9827
10885
|
} catch {
|
|
9828
10886
|
authStatus = "not_authenticated";
|
|
@@ -9899,11 +10957,260 @@ print(result.stdout[:5000])
|
|
|
9899
10957
|
const testText = params.text || "Hello! This is Pulso voice test. Kokoro TTS is working correctly.";
|
|
9900
10958
|
try {
|
|
9901
10959
|
await speak(testText, { engine: "auto" });
|
|
9902
|
-
return {
|
|
10960
|
+
return {
|
|
10961
|
+
success: true,
|
|
10962
|
+
data: { spoken: testText, engine: getTTSInfo().engine }
|
|
10963
|
+
};
|
|
9903
10964
|
} catch (err) {
|
|
9904
10965
|
return { success: false, error: err.message };
|
|
9905
10966
|
}
|
|
9906
10967
|
}
|
|
10968
|
+
// ── IDE Integration ────────────────────────────────────
|
|
10969
|
+
case "sys_ide_list_open": {
|
|
10970
|
+
return new Promise((resolve5) => {
|
|
10971
|
+
exec5(
|
|
10972
|
+
"ps aux",
|
|
10973
|
+
{ timeout: 5e3 },
|
|
10974
|
+
(err, stdout) => {
|
|
10975
|
+
if (err) {
|
|
10976
|
+
resolve5({ success: false, error: err.message });
|
|
10977
|
+
return;
|
|
10978
|
+
}
|
|
10979
|
+
const IDE_PATTERNS = {
|
|
10980
|
+
"Cursor Helper": "Cursor",
|
|
10981
|
+
"Cursor.app": "Cursor",
|
|
10982
|
+
"Code Helper": "VS Code",
|
|
10983
|
+
"Visual Studio Code": "VS Code",
|
|
10984
|
+
"Windsurf Helper": "Windsurf",
|
|
10985
|
+
"Windsurf.app": "Windsurf",
|
|
10986
|
+
"zed": "Zed",
|
|
10987
|
+
"WebStorm": "WebStorm",
|
|
10988
|
+
"IntelliJ IDEA": "IntelliJ IDEA",
|
|
10989
|
+
"PyCharm": "PyCharm",
|
|
10990
|
+
"GoLand": "GoLand"
|
|
10991
|
+
};
|
|
10992
|
+
const found = {};
|
|
10993
|
+
for (const line of stdout.split("\n")) {
|
|
10994
|
+
for (const [pattern, ideName] of Object.entries(IDE_PATTERNS)) {
|
|
10995
|
+
if (line.includes(pattern) && !line.includes("grep")) {
|
|
10996
|
+
if (!found[ideName]) found[ideName] = { ide: ideName, workspaces: [] };
|
|
10997
|
+
const matches = line.match(/\/(Users|home)\/[^\s]+/g) ?? [];
|
|
10998
|
+
for (const m of matches) {
|
|
10999
|
+
if (!m.includes(".app/") && !found[ideName].workspaces.includes(m)) {
|
|
11000
|
+
try {
|
|
11001
|
+
if (statSync4(m).isDirectory()) {
|
|
11002
|
+
found[ideName].workspaces.push(m);
|
|
11003
|
+
}
|
|
11004
|
+
} catch {
|
|
11005
|
+
}
|
|
11006
|
+
}
|
|
11007
|
+
}
|
|
11008
|
+
}
|
|
11009
|
+
}
|
|
11010
|
+
}
|
|
11011
|
+
const ides = Object.values(found);
|
|
11012
|
+
const home = homedir4();
|
|
11013
|
+
const storagePaths = [
|
|
11014
|
+
{ ide: "VS Code", path: join5(home, "Library/Application Support/Code/User/globalStorage/storage.json") },
|
|
11015
|
+
{ ide: "Cursor", path: join5(home, "Library/Application Support/Cursor/User/globalStorage/storage.json") },
|
|
11016
|
+
{ ide: "Windsurf", path: join5(home, "Library/Application Support/Windsurf/User/globalStorage/storage.json") }
|
|
11017
|
+
];
|
|
11018
|
+
for (const { ide: ideName, path: storagePath } of storagePaths) {
|
|
11019
|
+
if (!existsSync4(storagePath)) continue;
|
|
11020
|
+
try {
|
|
11021
|
+
const storage = JSON.parse(readFileSync4(storagePath, "utf-8"));
|
|
11022
|
+
const recentFolders = (storage["recently.opened"]?.workspaces ?? []).map(
|
|
11023
|
+
(w) => typeof w === "string" ? w : w.folderUri ?? ""
|
|
11024
|
+
).filter(Boolean).map((p) => p.replace(/^file:\/\//, "")).slice(0, 3);
|
|
11025
|
+
if (recentFolders.length > 0) {
|
|
11026
|
+
const existing = found[ideName];
|
|
11027
|
+
if (existing) {
|
|
11028
|
+
for (const folder of recentFolders) {
|
|
11029
|
+
if (!existing.workspaces.includes(folder)) {
|
|
11030
|
+
existing.workspaces.push(folder);
|
|
11031
|
+
}
|
|
11032
|
+
}
|
|
11033
|
+
} else if (ides.find((i) => i.ide === ideName) === void 0) {
|
|
11034
|
+
ides.push({ ide: ideName, workspaces: recentFolders });
|
|
11035
|
+
}
|
|
11036
|
+
}
|
|
11037
|
+
} catch {
|
|
11038
|
+
}
|
|
11039
|
+
}
|
|
11040
|
+
resolve5({
|
|
11041
|
+
success: true,
|
|
11042
|
+
data: {
|
|
11043
|
+
ides: ides.length > 0 ? ides : [],
|
|
11044
|
+
count: ides.length,
|
|
11045
|
+
note: ides.length === 0 ? "No IDEs detected. Open VS Code, Cursor, Windsurf, or Zed." : void 0
|
|
11046
|
+
}
|
|
11047
|
+
});
|
|
11048
|
+
}
|
|
11049
|
+
);
|
|
11050
|
+
});
|
|
11051
|
+
}
|
|
11052
|
+
case "sys_ide_get_context": {
|
|
11053
|
+
const targetIde = params.ide ?? "";
|
|
11054
|
+
const home = homedir4();
|
|
11055
|
+
const storageMap = {
|
|
11056
|
+
vscode: join5(home, "Library/Application Support/Code/User/globalStorage/storage.json"),
|
|
11057
|
+
cursor: join5(home, "Library/Application Support/Cursor/User/globalStorage/storage.json"),
|
|
11058
|
+
windsurf: join5(home, "Library/Application Support/Windsurf/User/globalStorage/storage.json")
|
|
11059
|
+
};
|
|
11060
|
+
const ideKey = targetIde.toLowerCase().replace(/[\s-]/g, "");
|
|
11061
|
+
const pathsToTry = ideKey && storageMap[ideKey] ? [{ ide: ideKey, path: storageMap[ideKey] }] : Object.entries(storageMap).map(([ide, path]) => ({ ide, path }));
|
|
11062
|
+
for (const { ide, path: storagePath } of pathsToTry) {
|
|
11063
|
+
if (!existsSync4(storagePath)) continue;
|
|
11064
|
+
try {
|
|
11065
|
+
const storage = JSON.parse(readFileSync4(storagePath, "utf-8"));
|
|
11066
|
+
const recentWorkspaces = (storage["recently.opened"]?.workspaces ?? []).map(
|
|
11067
|
+
(w) => typeof w === "string" ? w : w.folderUri ?? ""
|
|
11068
|
+
).filter(Boolean).map((p) => p.replace(/^file:\/\//, "")).slice(0, 5);
|
|
11069
|
+
const activeWorkspace = recentWorkspaces[0] ?? null;
|
|
11070
|
+
const recentFiles = (storage["recently.opened"]?.files ?? []).map(
|
|
11071
|
+
(f) => typeof f === "string" ? f : f.fileUri ?? ""
|
|
11072
|
+
).filter(Boolean).map((p) => p.replace(/^file:\/\//, "")).slice(0, 5);
|
|
11073
|
+
return {
|
|
11074
|
+
success: true,
|
|
11075
|
+
data: {
|
|
11076
|
+
ide,
|
|
11077
|
+
activeWorkspace,
|
|
11078
|
+
recentWorkspaces,
|
|
11079
|
+
recentFiles
|
|
11080
|
+
}
|
|
11081
|
+
};
|
|
11082
|
+
} catch {
|
|
11083
|
+
}
|
|
11084
|
+
}
|
|
11085
|
+
return {
|
|
11086
|
+
success: false,
|
|
11087
|
+
error: "No IDE context found. Make sure VS Code, Cursor, or Windsurf has been used."
|
|
11088
|
+
};
|
|
11089
|
+
}
|
|
11090
|
+
case "sys_ide_run_terminal": {
|
|
11091
|
+
const command2 = params.command;
|
|
11092
|
+
if (!command2) return { success: false, error: "Missing command" };
|
|
11093
|
+
let cwd = params.workspace;
|
|
11094
|
+
if (!cwd) {
|
|
11095
|
+
const home = homedir4();
|
|
11096
|
+
for (const storagePath of [
|
|
11097
|
+
join5(home, "Library/Application Support/Cursor/User/globalStorage/storage.json"),
|
|
11098
|
+
join5(home, "Library/Application Support/Code/User/globalStorage/storage.json"),
|
|
11099
|
+
join5(home, "Library/Application Support/Windsurf/User/globalStorage/storage.json")
|
|
11100
|
+
]) {
|
|
11101
|
+
if (!existsSync4(storagePath)) continue;
|
|
11102
|
+
try {
|
|
11103
|
+
const storage = JSON.parse(readFileSync4(storagePath, "utf-8"));
|
|
11104
|
+
const firstWorkspace = (storage["recently.opened"]?.workspaces ?? [])[0];
|
|
11105
|
+
if (firstWorkspace) {
|
|
11106
|
+
const p = typeof firstWorkspace === "string" ? firstWorkspace : firstWorkspace.folderUri ?? "";
|
|
11107
|
+
cwd = p.replace(/^file:\/\//, "");
|
|
11108
|
+
break;
|
|
11109
|
+
}
|
|
11110
|
+
} catch {
|
|
11111
|
+
}
|
|
11112
|
+
}
|
|
11113
|
+
}
|
|
11114
|
+
const timeout = 3e4;
|
|
11115
|
+
return new Promise((resolve5) => {
|
|
11116
|
+
exec5(command2, { cwd: cwd || homedir4(), timeout }, (err, stdout, stderr) => {
|
|
11117
|
+
if (err && !stdout) {
|
|
11118
|
+
resolve5({
|
|
11119
|
+
success: false,
|
|
11120
|
+
error: `Command failed: ${stderr || err.message}`.slice(0, 2e3)
|
|
11121
|
+
});
|
|
11122
|
+
} else {
|
|
11123
|
+
resolve5({
|
|
11124
|
+
success: true,
|
|
11125
|
+
data: {
|
|
11126
|
+
command: command2,
|
|
11127
|
+
cwd: cwd || homedir4(),
|
|
11128
|
+
output: (stdout + (stderr ? `
|
|
11129
|
+
STDERR: ${stderr}` : "")).slice(0, 1e4),
|
|
11130
|
+
truncated: (stdout + stderr).length > 1e4,
|
|
11131
|
+
exitCode: err?.code ?? 0
|
|
11132
|
+
}
|
|
11133
|
+
});
|
|
11134
|
+
}
|
|
11135
|
+
});
|
|
11136
|
+
});
|
|
11137
|
+
}
|
|
11138
|
+
case "sys_ide_read_terminal": {
|
|
11139
|
+
const lines = Number(params.lines) || 50;
|
|
11140
|
+
const home = homedir4();
|
|
11141
|
+
const historyPaths = [
|
|
11142
|
+
join5(home, ".zsh_history"),
|
|
11143
|
+
join5(home, ".bash_history"),
|
|
11144
|
+
join5(home, ".local/share/fish/fish_history")
|
|
11145
|
+
];
|
|
11146
|
+
for (const histPath of historyPaths) {
|
|
11147
|
+
if (!existsSync4(histPath)) continue;
|
|
11148
|
+
try {
|
|
11149
|
+
const content = readFileSync4(histPath, "utf-8");
|
|
11150
|
+
const allLines = content.split("\n").filter(Boolean);
|
|
11151
|
+
const commands = allLines.map((l) => l.startsWith(": ") ? l.replace(/^:\s*\d+:\d+;/, "") : l).filter((l) => !l.startsWith("#")).slice(-lines);
|
|
11152
|
+
return {
|
|
11153
|
+
success: true,
|
|
11154
|
+
data: {
|
|
11155
|
+
source: histPath,
|
|
11156
|
+
lines: commands,
|
|
11157
|
+
count: commands.length
|
|
11158
|
+
}
|
|
11159
|
+
};
|
|
11160
|
+
} catch {
|
|
11161
|
+
}
|
|
11162
|
+
}
|
|
11163
|
+
return {
|
|
11164
|
+
success: false,
|
|
11165
|
+
error: "No shell history found. Make sure zsh, bash, or fish history is enabled."
|
|
11166
|
+
};
|
|
11167
|
+
}
|
|
11168
|
+
case "sys_ide_send_to_claude": {
|
|
11169
|
+
const prompt = params.prompt;
|
|
11170
|
+
if (!prompt) return { success: false, error: "Missing prompt" };
|
|
11171
|
+
let cwd = params.workspace;
|
|
11172
|
+
if (!cwd) {
|
|
11173
|
+
const home = homedir4();
|
|
11174
|
+
for (const storagePath of [
|
|
11175
|
+
join5(home, "Library/Application Support/Cursor/User/globalStorage/storage.json"),
|
|
11176
|
+
join5(home, "Library/Application Support/Code/User/globalStorage/storage.json"),
|
|
11177
|
+
join5(home, "Library/Application Support/Windsurf/User/globalStorage/storage.json")
|
|
11178
|
+
]) {
|
|
11179
|
+
if (!existsSync4(storagePath)) continue;
|
|
11180
|
+
try {
|
|
11181
|
+
const storage = JSON.parse(readFileSync4(storagePath, "utf-8"));
|
|
11182
|
+
const firstWorkspace = (storage["recently.opened"]?.workspaces ?? [])[0];
|
|
11183
|
+
if (firstWorkspace) {
|
|
11184
|
+
const p = typeof firstWorkspace === "string" ? firstWorkspace : firstWorkspace.folderUri ?? "";
|
|
11185
|
+
cwd = p.replace(/^file:\/\//, "");
|
|
11186
|
+
break;
|
|
11187
|
+
}
|
|
11188
|
+
} catch {
|
|
11189
|
+
}
|
|
11190
|
+
}
|
|
11191
|
+
}
|
|
11192
|
+
const claudeCmd = `claude -p ${JSON.stringify(prompt)}`;
|
|
11193
|
+
return new Promise((resolve5) => {
|
|
11194
|
+
exec5(claudeCmd, { cwd: cwd || homedir4(), timeout: 12e4, env: { ...process.env, PATH: augmentedPath2() } }, (err, stdout, stderr) => {
|
|
11195
|
+
if (err && !stdout) {
|
|
11196
|
+
resolve5({
|
|
11197
|
+
success: false,
|
|
11198
|
+
error: err.message.includes("not found") || err.message.includes("ENOENT") ? "Claude Code CLI not found. Install it with: npm install -g @anthropic-ai/claude-code" : `Claude Code error: ${stderr || err.message}`.slice(0, 2e3)
|
|
11199
|
+
});
|
|
11200
|
+
} else {
|
|
11201
|
+
resolve5({
|
|
11202
|
+
success: true,
|
|
11203
|
+
data: {
|
|
11204
|
+
prompt,
|
|
11205
|
+
workspace: cwd || homedir4(),
|
|
11206
|
+
response: stdout.trim().slice(0, 1e4),
|
|
11207
|
+
truncated: stdout.length > 1e4
|
|
11208
|
+
}
|
|
11209
|
+
});
|
|
11210
|
+
}
|
|
11211
|
+
});
|
|
11212
|
+
});
|
|
11213
|
+
}
|
|
9907
11214
|
default:
|
|
9908
11215
|
return { success: false, error: `Unknown command: ${command}` };
|
|
9909
11216
|
}
|
|
@@ -9934,8 +11241,12 @@ function startImessageMonitor() {
|
|
|
9934
11241
|
lastImessageRowId = parseInt(initResult, 10) || 0;
|
|
9935
11242
|
console.log(` \u2713 iMessage: monitoring from ROWID ${lastImessageRowId}`);
|
|
9936
11243
|
} catch (err) {
|
|
9937
|
-
console.log(
|
|
9938
|
-
|
|
11244
|
+
console.log(
|
|
11245
|
+
` \u26A0 iMessage: failed to read chat.db \u2014 ${err.message}`
|
|
11246
|
+
);
|
|
11247
|
+
console.log(
|
|
11248
|
+
" Grant Full Disk Access to Terminal/iTerm in System Settings \u2192 Privacy & Security"
|
|
11249
|
+
);
|
|
9939
11250
|
return;
|
|
9940
11251
|
}
|
|
9941
11252
|
imessageTimer = setInterval(async () => {
|
|
@@ -9969,16 +11280,20 @@ function startImessageMonitor() {
|
|
|
9969
11280
|
if (rowId <= lastImessageRowId) continue;
|
|
9970
11281
|
lastImessageRowId = rowId;
|
|
9971
11282
|
if (!text || text.startsWith("\uFFFC")) continue;
|
|
9972
|
-
console.log(
|
|
9973
|
-
|
|
9974
|
-
|
|
9975
|
-
|
|
9976
|
-
|
|
9977
|
-
|
|
9978
|
-
|
|
9979
|
-
|
|
9980
|
-
|
|
9981
|
-
|
|
11283
|
+
console.log(
|
|
11284
|
+
`
|
|
11285
|
+
\u{1F4AC} iMessage from ${senderName || senderId}: ${text.slice(0, 80)}`
|
|
11286
|
+
);
|
|
11287
|
+
ws.send(
|
|
11288
|
+
JSON.stringify({
|
|
11289
|
+
type: "imessage_incoming",
|
|
11290
|
+
from: senderId || "unknown",
|
|
11291
|
+
fromName: senderName || senderId || "Unknown",
|
|
11292
|
+
chatId: chatId || senderId || "unknown",
|
|
11293
|
+
text,
|
|
11294
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
11295
|
+
})
|
|
11296
|
+
);
|
|
9982
11297
|
}
|
|
9983
11298
|
} catch {
|
|
9984
11299
|
}
|
|
@@ -9990,6 +11305,103 @@ function stopImessageMonitor() {
|
|
|
9990
11305
|
imessageTimer = null;
|
|
9991
11306
|
}
|
|
9992
11307
|
}
|
|
11308
|
+
var permissionDialogTimer = null;
|
|
11309
|
+
var lastDialogSignature = null;
|
|
11310
|
+
var DIALOG_POLL_INTERVAL = 1500;
|
|
11311
|
+
var activeDialog = null;
|
|
11312
|
+
function startPermissionDialogWatcher() {
|
|
11313
|
+
if (adapter.platform !== "macos") return;
|
|
11314
|
+
permissionDialogTimer = setInterval(async () => {
|
|
11315
|
+
if (!ws || ws.readyState !== WebSocket.OPEN) return;
|
|
11316
|
+
try {
|
|
11317
|
+
const script = `
|
|
11318
|
+
tell application "System Events"
|
|
11319
|
+
set procList to every process where visible is true
|
|
11320
|
+
repeat with proc in procList
|
|
11321
|
+
set procName to name of proc as string
|
|
11322
|
+
repeat with w in (windows of proc)
|
|
11323
|
+
try
|
|
11324
|
+
set wRole to role of w as string
|
|
11325
|
+
if wRole is "AXSheet" or wRole is "AXDialog" then
|
|
11326
|
+
set wTitle to ""
|
|
11327
|
+
try
|
|
11328
|
+
set wTitle to title of w as string
|
|
11329
|
+
end try
|
|
11330
|
+
set wDesc to ""
|
|
11331
|
+
try
|
|
11332
|
+
set wDesc to description of w as string
|
|
11333
|
+
end try
|
|
11334
|
+
set btnNames to {}
|
|
11335
|
+
repeat with btn in (buttons of w)
|
|
11336
|
+
try
|
|
11337
|
+
set end of btnNames to (name of btn as string)
|
|
11338
|
+
end try
|
|
11339
|
+
end repeat
|
|
11340
|
+
if (count of btnNames) > 0 then
|
|
11341
|
+
set btnStr to ""
|
|
11342
|
+
repeat with b in btnNames
|
|
11343
|
+
if btnStr is not "" then set btnStr to btnStr & "|||"
|
|
11344
|
+
set btnStr to btnStr & b
|
|
11345
|
+
end repeat
|
|
11346
|
+
return procName & ":::" & wTitle & ":::" & wDesc & ":::" & btnStr
|
|
11347
|
+
end if
|
|
11348
|
+
end if
|
|
11349
|
+
end try
|
|
11350
|
+
end repeat
|
|
11351
|
+
end repeat
|
|
11352
|
+
return ""
|
|
11353
|
+
end tell`;
|
|
11354
|
+
const raw = await runAppleScript2(script);
|
|
11355
|
+
if (!raw) {
|
|
11356
|
+
if (activeDialog) {
|
|
11357
|
+
lastDialogSignature = null;
|
|
11358
|
+
activeDialog = null;
|
|
11359
|
+
}
|
|
11360
|
+
return;
|
|
11361
|
+
}
|
|
11362
|
+
const sig = raw;
|
|
11363
|
+
if (sig === lastDialogSignature) return;
|
|
11364
|
+
lastDialogSignature = sig;
|
|
11365
|
+
const [procName = "", title = "", desc = "", btnStr = ""] = raw.split(":::");
|
|
11366
|
+
const buttons = btnStr.split("|||").map((b) => b.trim()).filter(Boolean);
|
|
11367
|
+
const dialogId = `dlg_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
|
|
11368
|
+
activeDialog = { dialogId, procName, buttons, detectedAt: Date.now() };
|
|
11369
|
+
let screenshot;
|
|
11370
|
+
try {
|
|
11371
|
+
const scrResult = await adapter.screenshot();
|
|
11372
|
+
if (scrResult.image) {
|
|
11373
|
+
screenshot = scrResult.image;
|
|
11374
|
+
}
|
|
11375
|
+
} catch {
|
|
11376
|
+
}
|
|
11377
|
+
console.log(
|
|
11378
|
+
`
|
|
11379
|
+
\u{1F514} Permission dialog: [${procName}] "${title}" \u2014 buttons: ${buttons.join(", ")}`
|
|
11380
|
+
);
|
|
11381
|
+
ws.send(
|
|
11382
|
+
JSON.stringify({
|
|
11383
|
+
type: "permission_dialog",
|
|
11384
|
+
dialogId,
|
|
11385
|
+
procName,
|
|
11386
|
+
title: title || procName,
|
|
11387
|
+
message: desc,
|
|
11388
|
+
buttons,
|
|
11389
|
+
screenshot,
|
|
11390
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
11391
|
+
})
|
|
11392
|
+
);
|
|
11393
|
+
} catch {
|
|
11394
|
+
}
|
|
11395
|
+
}, DIALOG_POLL_INTERVAL);
|
|
11396
|
+
}
|
|
11397
|
+
function stopPermissionDialogWatcher() {
|
|
11398
|
+
if (permissionDialogTimer) {
|
|
11399
|
+
clearInterval(permissionDialogTimer);
|
|
11400
|
+
permissionDialogTimer = null;
|
|
11401
|
+
}
|
|
11402
|
+
lastDialogSignature = null;
|
|
11403
|
+
activeDialog = null;
|
|
11404
|
+
}
|
|
9993
11405
|
var LAZY_CAPABILITY_TOOLS = [
|
|
9994
11406
|
"sys_calendar_list",
|
|
9995
11407
|
"sys_calendar_create",
|
|
@@ -10007,13 +11419,22 @@ var CAPABILITY_PROBES = [
|
|
|
10007
11419
|
name: "spotify",
|
|
10008
11420
|
test: async () => {
|
|
10009
11421
|
try {
|
|
10010
|
-
await runShell4(
|
|
11422
|
+
await runShell4(
|
|
11423
|
+
"pgrep -x Spotify >/dev/null 2>&1 || ls /Applications/Spotify.app >/dev/null 2>&1"
|
|
11424
|
+
);
|
|
10011
11425
|
return true;
|
|
10012
11426
|
} catch {
|
|
10013
11427
|
return false;
|
|
10014
11428
|
}
|
|
10015
11429
|
},
|
|
10016
|
-
tools: [
|
|
11430
|
+
tools: [
|
|
11431
|
+
"sys_spotify_play",
|
|
11432
|
+
"sys_spotify_pause",
|
|
11433
|
+
"sys_spotify_current",
|
|
11434
|
+
"sys_spotify_next",
|
|
11435
|
+
"sys_spotify_previous",
|
|
11436
|
+
"sys_spotify_search"
|
|
11437
|
+
]
|
|
10017
11438
|
},
|
|
10018
11439
|
{
|
|
10019
11440
|
name: "tts",
|
|
@@ -10043,7 +11464,10 @@ var CAPABILITY_PROBES = [
|
|
|
10043
11464
|
name: "claude_cli",
|
|
10044
11465
|
test: async () => {
|
|
10045
11466
|
try {
|
|
10046
|
-
await runShell4(
|
|
11467
|
+
await runShell4(
|
|
11468
|
+
'(which claude >/dev/null 2>&1 || test -x "$HOME/.local/bin/claude" || test -x "$HOME/.local/share/claude/versions/"*/claude) && (claude --version >/dev/null 2>&1 || "$HOME/.local/bin/claude" --version >/dev/null 2>&1)',
|
|
11469
|
+
5e3
|
|
11470
|
+
);
|
|
10047
11471
|
return true;
|
|
10048
11472
|
} catch {
|
|
10049
11473
|
return false;
|
|
@@ -10055,7 +11479,10 @@ var CAPABILITY_PROBES = [
|
|
|
10055
11479
|
name: "codex_cli",
|
|
10056
11480
|
test: async () => {
|
|
10057
11481
|
try {
|
|
10058
|
-
await runShell4(
|
|
11482
|
+
await runShell4(
|
|
11483
|
+
"which codex >/dev/null 2>&1 && codex --version >/dev/null 2>&1",
|
|
11484
|
+
5e3
|
|
11485
|
+
);
|
|
10059
11486
|
return true;
|
|
10060
11487
|
} catch {
|
|
10061
11488
|
return false;
|
|
@@ -10123,8 +11550,11 @@ async function probeCapabilities() {
|
|
|
10123
11550
|
}
|
|
10124
11551
|
}
|
|
10125
11552
|
const cap = { available, unavailable, tools: Array.from(tools) };
|
|
10126
|
-
console.log(
|
|
10127
|
-
|
|
11553
|
+
console.log(
|
|
11554
|
+
` \u2705 Available: ${available.join(", ") || "all adapter tools"}`
|
|
11555
|
+
);
|
|
11556
|
+
if (unavailable.length)
|
|
11557
|
+
console.log(` \u26A0\uFE0F Unavailable: ${unavailable.join(", ")}`);
|
|
10128
11558
|
console.log(` \u{1F4E6} ${cap.tools.length} tools verified`);
|
|
10129
11559
|
return cap;
|
|
10130
11560
|
}
|
|
@@ -10172,18 +11602,20 @@ async function connect() {
|
|
|
10172
11602
|
console.log(" Waiting for commands from Pulso agent...");
|
|
10173
11603
|
probeCapabilities().then((cap) => {
|
|
10174
11604
|
verifiedCapabilities = cap;
|
|
10175
|
-
ws.send(
|
|
10176
|
-
|
|
10177
|
-
|
|
10178
|
-
|
|
10179
|
-
|
|
10180
|
-
|
|
10181
|
-
|
|
10182
|
-
|
|
10183
|
-
|
|
10184
|
-
|
|
10185
|
-
|
|
10186
|
-
|
|
11605
|
+
ws.send(
|
|
11606
|
+
JSON.stringify({
|
|
11607
|
+
type: "extension_ready",
|
|
11608
|
+
platform: adapter.platform,
|
|
11609
|
+
version: "0.4.3",
|
|
11610
|
+
accessLevel: ACCESS_LEVEL3,
|
|
11611
|
+
homeDir: HOME4,
|
|
11612
|
+
hostname: hostname3(),
|
|
11613
|
+
capabilities: cap.available,
|
|
11614
|
+
unavailable: cap.unavailable,
|
|
11615
|
+
tools: cap.tools,
|
|
11616
|
+
totalTools: cap.tools.length
|
|
11617
|
+
})
|
|
11618
|
+
);
|
|
10187
11619
|
});
|
|
10188
11620
|
if (heartbeatTimer) clearInterval(heartbeatTimer);
|
|
10189
11621
|
heartbeatTimer = setInterval(() => {
|
|
@@ -10193,6 +11625,8 @@ async function connect() {
|
|
|
10193
11625
|
}, HEARTBEAT_INTERVAL);
|
|
10194
11626
|
stopImessageMonitor();
|
|
10195
11627
|
startImessageMonitor();
|
|
11628
|
+
stopPermissionDialogWatcher();
|
|
11629
|
+
startPermissionDialogWatcher();
|
|
10196
11630
|
});
|
|
10197
11631
|
ws.on("message", async (raw) => {
|
|
10198
11632
|
try {
|
|
@@ -10210,7 +11644,17 @@ async function connect() {
|
|
|
10210
11644
|
\u26A1 Command: ${msg.command}`,
|
|
10211
11645
|
msg.params ? JSON.stringify(msg.params).slice(0, 200) : ""
|
|
10212
11646
|
);
|
|
10213
|
-
const
|
|
11647
|
+
const streamCb = (chunk) => {
|
|
11648
|
+
try {
|
|
11649
|
+
ws.send(JSON.stringify({ id: msg.id, type: "stream", chunk }));
|
|
11650
|
+
} catch {
|
|
11651
|
+
}
|
|
11652
|
+
};
|
|
11653
|
+
const result = await handleCommand(
|
|
11654
|
+
msg.command,
|
|
11655
|
+
msg.params ?? {},
|
|
11656
|
+
streamCb
|
|
11657
|
+
);
|
|
10214
11658
|
console.log(
|
|
10215
11659
|
` \u2192 ${result.success ? "\u2705" : "\u274C"}`,
|
|
10216
11660
|
result.success ? JSON.stringify(result.data).slice(0, 200) : result.error
|
|
@@ -10228,6 +11672,7 @@ async function connect() {
|
|
|
10228
11672
|
console.log(`
|
|
10229
11673
|
\u{1F50C} Disconnected (${code}: ${reasonStr})`);
|
|
10230
11674
|
stopImessageMonitor();
|
|
11675
|
+
stopPermissionDialogWatcher();
|
|
10231
11676
|
if (heartbeatTimer) {
|
|
10232
11677
|
clearInterval(heartbeatTimer);
|
|
10233
11678
|
heartbeatTimer = null;
|
|
@@ -10341,7 +11786,10 @@ function discoverWakeWordKeywordPath() {
|
|
|
10341
11786
|
const explicit = WAKE_WORD_KEYWORD_PATH.trim();
|
|
10342
11787
|
if (explicit) {
|
|
10343
11788
|
if (existsSync4(explicit)) {
|
|
10344
|
-
return {
|
|
11789
|
+
return {
|
|
11790
|
+
path: explicit,
|
|
11791
|
+
source: "PULSO_WAKE_WORD_PATH / --wake-word-path"
|
|
11792
|
+
};
|
|
10345
11793
|
}
|
|
10346
11794
|
console.log(
|
|
10347
11795
|
` \u26A0\uFE0F Wake word file not found at configured path: ${explicit}`
|
|
@@ -10400,10 +11848,15 @@ function discoverWakeWordKeywordPath() {
|
|
|
10400
11848
|
function discoverWakeWordLanguageModelPath(keywordPath, fallbackModelPath) {
|
|
10401
11849
|
const explicit = WAKE_WORD_MODEL_PATH.trim();
|
|
10402
11850
|
if (explicit && existsSync4(explicit)) {
|
|
10403
|
-
return {
|
|
11851
|
+
return {
|
|
11852
|
+
path: explicit,
|
|
11853
|
+
source: "PULSO_WAKE_WORD_MODEL_PATH / --wake-word-model-path"
|
|
11854
|
+
};
|
|
10404
11855
|
}
|
|
10405
11856
|
if (explicit && !existsSync4(explicit)) {
|
|
10406
|
-
console.log(
|
|
11857
|
+
console.log(
|
|
11858
|
+
` \u26A0\uFE0F Language model file not found at configured path: ${explicit}`
|
|
11859
|
+
);
|
|
10407
11860
|
}
|
|
10408
11861
|
const direct = join5(HOME4, ".pulso-wake-word-model.pv");
|
|
10409
11862
|
if (existsSync4(direct)) {
|
|
@@ -10458,7 +11911,10 @@ function buildWakeWordDeviceCandidates(devices, explicitDeviceIndex) {
|
|
|
10458
11911
|
}
|
|
10459
11912
|
function startWakeWordRecorder(PvRecorder, frameLength, devices) {
|
|
10460
11913
|
const explicitDeviceIndex = parseWakeWordDeviceIndex(WAKE_WORD_DEVICE_INDEX);
|
|
10461
|
-
const candidates = buildWakeWordDeviceCandidates(
|
|
11914
|
+
const candidates = buildWakeWordDeviceCandidates(
|
|
11915
|
+
devices,
|
|
11916
|
+
explicitDeviceIndex
|
|
11917
|
+
);
|
|
10462
11918
|
const errors = [];
|
|
10463
11919
|
for (const candidate of candidates) {
|
|
10464
11920
|
let recorder = null;
|
|
@@ -10481,7 +11937,9 @@ ${errors.map((e) => ` - ${e}`).join("\n")}`
|
|
|
10481
11937
|
}
|
|
10482
11938
|
function loadWakeRecorderEngine() {
|
|
10483
11939
|
if (process.platform !== "darwin") {
|
|
10484
|
-
throw new Error(
|
|
11940
|
+
throw new Error(
|
|
11941
|
+
"Wake word runtime assets are currently packaged for macOS only."
|
|
11942
|
+
);
|
|
10485
11943
|
}
|
|
10486
11944
|
const archDir = process.arch === "arm64" ? "arm64" : "x86_64";
|
|
10487
11945
|
const recorderLibraryPath = resolvePicovoiceAsset(
|
|
@@ -10502,7 +11960,9 @@ function loadWakeRecorderEngine() {
|
|
|
10502
11960
|
}
|
|
10503
11961
|
function loadWakeWordEngines() {
|
|
10504
11962
|
if (process.platform !== "darwin") {
|
|
10505
|
-
throw new Error(
|
|
11963
|
+
throw new Error(
|
|
11964
|
+
"Wake word runtime assets are currently packaged for macOS only."
|
|
11965
|
+
);
|
|
10506
11966
|
}
|
|
10507
11967
|
const archDir = process.arch === "arm64" ? "arm64" : "x86_64";
|
|
10508
11968
|
const porcupineModelPath = resolvePicovoiceAsset(
|
|
@@ -10557,21 +12017,16 @@ async function startPicovoiceWakeWordDetection() {
|
|
|
10557
12017
|
return;
|
|
10558
12018
|
}
|
|
10559
12019
|
try {
|
|
10560
|
-
const {
|
|
10561
|
-
Porcupine,
|
|
10562
|
-
PvRecorder,
|
|
10563
|
-
porcupineModelPath,
|
|
10564
|
-
porcupineLibraryPath
|
|
10565
|
-
} = loadWakeWordEngines();
|
|
12020
|
+
const { Porcupine, PvRecorder, porcupineModelPath, porcupineLibraryPath } = loadWakeWordEngines();
|
|
10566
12021
|
const keyword = discoverWakeWordKeywordPath();
|
|
10567
12022
|
if (!keyword) {
|
|
12023
|
+
console.log(" \u26A0\uFE0F Wake word model not found at ~/.pulso-wake-word.ppn");
|
|
10568
12024
|
console.log(
|
|
10569
|
-
"
|
|
12025
|
+
' "Hey Pulso" requires a Picovoice keyword model file (.ppn).'
|
|
10570
12026
|
);
|
|
10571
12027
|
console.log(
|
|
10572
|
-
|
|
12028
|
+
" Create it at https://console.picovoice.ai/ and save it to:"
|
|
10573
12029
|
);
|
|
10574
|
-
console.log(" Create it at https://console.picovoice.ai/ and save it to:");
|
|
10575
12030
|
console.log(" ~/.pulso-wake-word.ppn");
|
|
10576
12031
|
console.log(" (or set PULSO_WAKE_WORD_PATH / --wake-word-path)\n");
|
|
10577
12032
|
return;
|
|
@@ -10724,12 +12179,16 @@ async function maybeGetWakeLocalTranscript(chunks, totalSamples, sampleRate, bud
|
|
|
10724
12179
|
const timedOut = /* @__PURE__ */ Symbol("wake-stt-timeout");
|
|
10725
12180
|
const sttResult = await Promise.race([
|
|
10726
12181
|
transcribe(merged, sampleRate, { model: "tiny.en", language: "auto" }),
|
|
10727
|
-
new Promise(
|
|
12182
|
+
new Promise(
|
|
12183
|
+
(resolve5) => setTimeout(() => resolve5(timedOut), budgetMs)
|
|
12184
|
+
)
|
|
10728
12185
|
]);
|
|
10729
12186
|
if (sttResult === timedOut || !sttResult?.text) return void 0;
|
|
10730
12187
|
const transcript = sttResult.text.trim();
|
|
10731
12188
|
if (!transcript) return void 0;
|
|
10732
|
-
console.log(
|
|
12189
|
+
console.log(
|
|
12190
|
+
` \u{1F9E0} Local STT (${sttResult.durationMs ?? budgetMs}ms): "${transcript}"`
|
|
12191
|
+
);
|
|
10733
12192
|
return transcript;
|
|
10734
12193
|
} catch {
|
|
10735
12194
|
return void 0;
|
|
@@ -10764,7 +12223,9 @@ async function startSemanticWakeWordDetection() {
|
|
|
10764
12223
|
devices
|
|
10765
12224
|
);
|
|
10766
12225
|
const selectedDeviceName = selectedDeviceIndex >= 0 ? devices[selectedDeviceIndex] || `device ${selectedDeviceIndex}` : "OS default device";
|
|
10767
|
-
const calibrationFrames = Math.ceil(
|
|
12226
|
+
const calibrationFrames = Math.ceil(
|
|
12227
|
+
wakeCalibrationMs / 1e3 * sampleRate / frameLength
|
|
12228
|
+
);
|
|
10768
12229
|
let noiseFloor = 0;
|
|
10769
12230
|
if (calibrationFrames > 0) {
|
|
10770
12231
|
const samples = [];
|
|
@@ -10794,9 +12255,14 @@ async function startSemanticWakeWordDetection() {
|
|
|
10794
12255
|
" \u{1F9E0} Trigger phrase: say 'Hey Pulso' (or 'Ok Pulso', 'Ola Pulso', 'Pulso')\n"
|
|
10795
12256
|
);
|
|
10796
12257
|
const minSpeechFrames = Math.ceil(0.22 * sampleRate / frameLength);
|
|
10797
|
-
const maxSilenceFrames = Math.ceil(
|
|
12258
|
+
const maxSilenceFrames = Math.ceil(
|
|
12259
|
+
wakeEndSilenceMs / 1e3 * sampleRate / frameLength
|
|
12260
|
+
);
|
|
10798
12261
|
const maxRecordFrames = Math.ceil(10 * sampleRate / frameLength);
|
|
10799
|
-
const preRollFrames = Math.max(
|
|
12262
|
+
const preRollFrames = Math.max(
|
|
12263
|
+
1,
|
|
12264
|
+
Math.ceil(0.35 * sampleRate / frameLength)
|
|
12265
|
+
);
|
|
10800
12266
|
const sendCooldownMs = 700;
|
|
10801
12267
|
let speaking = false;
|
|
10802
12268
|
let framesCaptured = 0;
|
|
@@ -10854,7 +12320,9 @@ async function startSemanticWakeWordDetection() {
|
|
|
10854
12320
|
...localTranscript ? { localTranscript } : {}
|
|
10855
12321
|
})
|
|
10856
12322
|
);
|
|
10857
|
-
console.log(
|
|
12323
|
+
console.log(
|
|
12324
|
+
` \u{1F4E4} Semantic wake probe sent (${(durationMs / 1e3).toFixed(1)}s)`
|
|
12325
|
+
);
|
|
10858
12326
|
exec5("afplay /System/Library/Sounds/Pop.aiff");
|
|
10859
12327
|
cooldownUntil = Date.now() + sendCooldownMs;
|
|
10860
12328
|
}
|
|
@@ -10933,6 +12401,10 @@ function writeString(view, offset, str) {
|
|
|
10933
12401
|
}
|
|
10934
12402
|
var currentPlatform = detectPlatform();
|
|
10935
12403
|
var platformName = { macos: "macOS", windows: "Windows", linux: "Linux" }[currentPlatform] || "Unknown";
|
|
12404
|
+
acquireCompanionLock();
|
|
12405
|
+
process.on("exit", () => {
|
|
12406
|
+
releaseCompanionLock();
|
|
12407
|
+
});
|
|
10936
12408
|
console.log("");
|
|
10937
12409
|
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");
|
|
10938
12410
|
console.log(` \u2551 Pulso ${platformName} Companion v0.4.3 \u2551`);
|
|
@@ -10958,10 +12430,12 @@ process.on("SIGINT", () => {
|
|
|
10958
12430
|
console.log("\n\u{1F44B} Shutting down Pulso Companion...");
|
|
10959
12431
|
wakeWordActive = false;
|
|
10960
12432
|
ws?.close(1e3, "User shutdown");
|
|
12433
|
+
releaseCompanionLock();
|
|
10961
12434
|
process.exit(0);
|
|
10962
12435
|
});
|
|
10963
12436
|
process.on("SIGTERM", () => {
|
|
10964
12437
|
wakeWordActive = false;
|
|
10965
12438
|
ws?.close(1e3, "Process terminated");
|
|
12439
|
+
releaseCompanionLock();
|
|
10966
12440
|
process.exit(0);
|
|
10967
12441
|
});
|