@pulso/companion 0.4.4 → 0.4.6
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 +1494 -525
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -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" }
|
|
@@ -1841,7 +1821,10 @@ var LinuxAdapter = class {
|
|
|
1841
1821
|
{ browser: "Google Chrome", dir: `${home}/.config/google-chrome` },
|
|
1842
1822
|
{ browser: "Chromium", dir: `${home}/.config/chromium` },
|
|
1843
1823
|
{ browser: "Microsoft Edge", dir: `${home}/.config/microsoft-edge` },
|
|
1844
|
-
{
|
|
1824
|
+
{
|
|
1825
|
+
browser: "Brave Browser",
|
|
1826
|
+
dir: `${home}/.config/BraveSoftware/Brave-Browser`
|
|
1827
|
+
}
|
|
1845
1828
|
];
|
|
1846
1829
|
const profiles = [];
|
|
1847
1830
|
for (const { browser, dir } of browserPaths) {
|
|
@@ -2045,9 +2028,7 @@ ${body}`;
|
|
|
2045
2028
|
if (method === "gmail") {
|
|
2046
2029
|
try {
|
|
2047
2030
|
const gmailUrl = `https://mail.google.com/mail/?view=cm&to=${encodeURIComponent(to)}&su=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`;
|
|
2048
|
-
await runShell(
|
|
2049
|
-
`xdg-open '${gmailUrl.replace(/'/g, "'\\''")}'`
|
|
2050
|
-
);
|
|
2031
|
+
await runShell(`xdg-open '${gmailUrl.replace(/'/g, "'\\''")}'`);
|
|
2051
2032
|
return {
|
|
2052
2033
|
success: true,
|
|
2053
2034
|
data: {
|
|
@@ -2066,9 +2047,7 @@ ${body}`;
|
|
|
2066
2047
|
}
|
|
2067
2048
|
try {
|
|
2068
2049
|
const mailtoUrl = `mailto:${encodeURIComponent(to)}?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`;
|
|
2069
|
-
await runShell(
|
|
2070
|
-
`xdg-open '${mailtoUrl.replace(/'/g, "'\\''")}'`
|
|
2071
|
-
);
|
|
2050
|
+
await runShell(`xdg-open '${mailtoUrl.replace(/'/g, "'\\''")}'`);
|
|
2072
2051
|
return {
|
|
2073
2052
|
success: true,
|
|
2074
2053
|
data: {
|
|
@@ -2241,9 +2220,7 @@ ${body}`;
|
|
|
2241
2220
|
}
|
|
2242
2221
|
if (await commandExists("gio")) {
|
|
2243
2222
|
try {
|
|
2244
|
-
await runShell(
|
|
2245
|
-
`gio trash '${fullPath.replace(/'/g, "'\\''")}'`
|
|
2246
|
-
);
|
|
2223
|
+
await runShell(`gio trash '${fullPath.replace(/'/g, "'\\''")}'`);
|
|
2247
2224
|
return {
|
|
2248
2225
|
success: true,
|
|
2249
2226
|
data: { path, action: "moved_to_trash" }
|
|
@@ -2265,9 +2242,7 @@ ${body}`;
|
|
|
2265
2242
|
}
|
|
2266
2243
|
if (await commandExists("trash-put")) {
|
|
2267
2244
|
try {
|
|
2268
|
-
await runShell(
|
|
2269
|
-
`trash-put '${fullPath.replace(/'/g, "'\\''")}'`
|
|
2270
|
-
);
|
|
2245
|
+
await runShell(`trash-put '${fullPath.replace(/'/g, "'\\''")}'`);
|
|
2271
2246
|
return {
|
|
2272
2247
|
success: true,
|
|
2273
2248
|
data: { path, action: "moved_to_trash" }
|
|
@@ -2500,9 +2475,7 @@ ${body}`;
|
|
|
2500
2475
|
const gy = y !== void 0 ? y : -1;
|
|
2501
2476
|
const gw = width !== void 0 ? width : -1;
|
|
2502
2477
|
const gh = height !== void 0 ? height : -1;
|
|
2503
|
-
await runShell(
|
|
2504
|
-
`wmctrl -r '${safeApp}' -e 0,${gx},${gy},${gw},${gh}`
|
|
2505
|
-
);
|
|
2478
|
+
await runShell(`wmctrl -r '${safeApp}' -e 0,${gx},${gy},${gw},${gh}`);
|
|
2506
2479
|
return {
|
|
2507
2480
|
success: true,
|
|
2508
2481
|
data: { app, x, y, width, height }
|
|
@@ -2670,9 +2643,7 @@ ${body}`;
|
|
|
2670
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'`,
|
|
2671
2644
|
5e3
|
|
2672
2645
|
);
|
|
2673
|
-
const titleMatch = output2.match(
|
|
2674
|
-
/xesam:title.*?string "([^"]+)"/
|
|
2675
|
-
);
|
|
2646
|
+
const titleMatch = output2.match(/xesam:title.*?string "([^"]+)"/);
|
|
2676
2647
|
const artistMatch = output2.match(
|
|
2677
2648
|
/xesam:artist.*?string "([^"]+)"/
|
|
2678
2649
|
);
|
|
@@ -2702,9 +2673,7 @@ ${body}`;
|
|
|
2702
2673
|
return { success: false, error: "Missing search query" };
|
|
2703
2674
|
}
|
|
2704
2675
|
const uri = `spotify:search:${encodeURIComponent(query)}`;
|
|
2705
|
-
await runShell(
|
|
2706
|
-
`xdg-open '${uri}' 2>/dev/null &`
|
|
2707
|
-
);
|
|
2676
|
+
await runShell(`xdg-open '${uri}' 2>/dev/null &`);
|
|
2708
2677
|
return {
|
|
2709
2678
|
success: true,
|
|
2710
2679
|
data: {
|
|
@@ -2828,12 +2797,9 @@ ${body}`;
|
|
|
2828
2797
|
if (!lightId) {
|
|
2829
2798
|
return { success: false, error: `Light '${light}' not found` };
|
|
2830
2799
|
}
|
|
2831
|
-
const res = await hueRequest(
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
"PUT",
|
|
2835
|
-
{ on: false }
|
|
2836
|
-
);
|
|
2800
|
+
const res = await hueRequest(config, `lights/${lightId}/state`, "PUT", {
|
|
2801
|
+
on: false
|
|
2802
|
+
});
|
|
2837
2803
|
return {
|
|
2838
2804
|
success: true,
|
|
2839
2805
|
data: { light: lightId, action: "off", response: res }
|
|
@@ -2860,15 +2826,10 @@ ${body}`;
|
|
|
2860
2826
|
return { success: false, error: `Unrecognized color: ${color}` };
|
|
2861
2827
|
}
|
|
2862
2828
|
const [x, y] = rgbToXy(...rgb);
|
|
2863
|
-
const res = await hueRequest(
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
{
|
|
2868
|
-
on: true,
|
|
2869
|
-
xy: [x, y]
|
|
2870
|
-
}
|
|
2871
|
-
);
|
|
2829
|
+
const res = await hueRequest(config, `lights/${lightId}/state`, "PUT", {
|
|
2830
|
+
on: true,
|
|
2831
|
+
xy: [x, y]
|
|
2832
|
+
});
|
|
2872
2833
|
return {
|
|
2873
2834
|
success: true,
|
|
2874
2835
|
data: { light: lightId, color, xy: [x, y], response: res }
|
|
@@ -2891,15 +2852,10 @@ ${body}`;
|
|
|
2891
2852
|
return { success: false, error: `Light '${light}' not found` };
|
|
2892
2853
|
}
|
|
2893
2854
|
const bri = Math.max(1, Math.min(254, brightness));
|
|
2894
|
-
const res = await hueRequest(
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
{
|
|
2899
|
-
on: true,
|
|
2900
|
-
bri
|
|
2901
|
-
}
|
|
2902
|
-
);
|
|
2855
|
+
const res = await hueRequest(config, `lights/${lightId}/state`, "PUT", {
|
|
2856
|
+
on: true,
|
|
2857
|
+
bri
|
|
2858
|
+
});
|
|
2903
2859
|
return {
|
|
2904
2860
|
success: true,
|
|
2905
2861
|
data: { light: lightId, brightness: bri, response: res }
|
|
@@ -2932,14 +2888,9 @@ ${body}`;
|
|
|
2932
2888
|
error: `Scene '${scene}' not found. Available: ${Object.values(scenes).map((s) => s.name).join(", ")}`
|
|
2933
2889
|
};
|
|
2934
2890
|
}
|
|
2935
|
-
const res = await hueRequest(
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
"PUT",
|
|
2939
|
-
{
|
|
2940
|
-
scene: sceneId
|
|
2941
|
-
}
|
|
2942
|
-
);
|
|
2891
|
+
const res = await hueRequest(config, `groups/${groupId}/action`, "PUT", {
|
|
2892
|
+
scene: sceneId
|
|
2893
|
+
});
|
|
2943
2894
|
return {
|
|
2944
2895
|
success: true,
|
|
2945
2896
|
data: { scene, sceneId, group: groupId, response: res }
|
|
@@ -3004,10 +2955,7 @@ ${body}`;
|
|
|
3004
2955
|
error: "Sonos not configured. Set SONOS_API_URL environment variable (e.g., http://localhost:5005)."
|
|
3005
2956
|
};
|
|
3006
2957
|
}
|
|
3007
|
-
const res = await sonosRequest(
|
|
3008
|
-
url,
|
|
3009
|
-
`${encodeURIComponent(room)}/play`
|
|
3010
|
-
);
|
|
2958
|
+
const res = await sonosRequest(url, `${encodeURIComponent(room)}/play`);
|
|
3011
2959
|
return {
|
|
3012
2960
|
success: true,
|
|
3013
2961
|
data: { room, action: "play", response: res }
|
|
@@ -3022,12 +2970,8 @@ ${body}`;
|
|
|
3022
2970
|
async sonosPause(room) {
|
|
3023
2971
|
try {
|
|
3024
2972
|
const url = getSonosApiUrl();
|
|
3025
|
-
if (!url)
|
|
3026
|
-
|
|
3027
|
-
const res = await sonosRequest(
|
|
3028
|
-
url,
|
|
3029
|
-
`${encodeURIComponent(room)}/pause`
|
|
3030
|
-
);
|
|
2973
|
+
if (!url) return { success: false, error: "Sonos not configured." };
|
|
2974
|
+
const res = await sonosRequest(url, `${encodeURIComponent(room)}/pause`);
|
|
3031
2975
|
return {
|
|
3032
2976
|
success: true,
|
|
3033
2977
|
data: { room, action: "pause", response: res }
|
|
@@ -3042,8 +2986,7 @@ ${body}`;
|
|
|
3042
2986
|
async sonosVolume(room, level) {
|
|
3043
2987
|
try {
|
|
3044
2988
|
const url = getSonosApiUrl();
|
|
3045
|
-
if (!url)
|
|
3046
|
-
return { success: false, error: "Sonos not configured." };
|
|
2989
|
+
if (!url) return { success: false, error: "Sonos not configured." };
|
|
3047
2990
|
const vol = Math.max(0, Math.min(100, level));
|
|
3048
2991
|
const res = await sonosRequest(
|
|
3049
2992
|
url,
|
|
@@ -3063,8 +3006,7 @@ ${body}`;
|
|
|
3063
3006
|
async sonosPlayUri(room, uri, title) {
|
|
3064
3007
|
try {
|
|
3065
3008
|
const url = getSonosApiUrl();
|
|
3066
|
-
if (!url)
|
|
3067
|
-
return { success: false, error: "Sonos not configured." };
|
|
3009
|
+
if (!url) return { success: false, error: "Sonos not configured." };
|
|
3068
3010
|
if (uri.startsWith("spotify:")) {
|
|
3069
3011
|
const res2 = await sonosRequest(
|
|
3070
3012
|
url,
|
|
@@ -3090,8 +3032,7 @@ ${body}`;
|
|
|
3090
3032
|
async sonosRooms() {
|
|
3091
3033
|
try {
|
|
3092
3034
|
const url = getSonosApiUrl();
|
|
3093
|
-
if (!url)
|
|
3094
|
-
return { success: false, error: "Sonos not configured." };
|
|
3035
|
+
if (!url) return { success: false, error: "Sonos not configured." };
|
|
3095
3036
|
const res = await sonosRequest(url, "zones");
|
|
3096
3037
|
return { success: true, data: res };
|
|
3097
3038
|
} catch (err) {
|
|
@@ -3104,12 +3045,8 @@ ${body}`;
|
|
|
3104
3045
|
async sonosNext(room) {
|
|
3105
3046
|
try {
|
|
3106
3047
|
const url = getSonosApiUrl();
|
|
3107
|
-
if (!url)
|
|
3108
|
-
|
|
3109
|
-
const res = await sonosRequest(
|
|
3110
|
-
url,
|
|
3111
|
-
`${encodeURIComponent(room)}/next`
|
|
3112
|
-
);
|
|
3048
|
+
if (!url) return { success: false, error: "Sonos not configured." };
|
|
3049
|
+
const res = await sonosRequest(url, `${encodeURIComponent(room)}/next`);
|
|
3113
3050
|
return {
|
|
3114
3051
|
success: true,
|
|
3115
3052
|
data: { room, action: "next", response: res }
|
|
@@ -3124,8 +3061,7 @@ ${body}`;
|
|
|
3124
3061
|
async sonosPrevious(room) {
|
|
3125
3062
|
try {
|
|
3126
3063
|
const url = getSonosApiUrl();
|
|
3127
|
-
if (!url)
|
|
3128
|
-
return { success: false, error: "Sonos not configured." };
|
|
3064
|
+
if (!url) return { success: false, error: "Sonos not configured." };
|
|
3129
3065
|
const res = await sonosRequest(
|
|
3130
3066
|
url,
|
|
3131
3067
|
`${encodeURIComponent(room)}/previous`
|
|
@@ -3144,12 +3080,8 @@ ${body}`;
|
|
|
3144
3080
|
async sonosNowPlaying(room) {
|
|
3145
3081
|
try {
|
|
3146
3082
|
const url = getSonosApiUrl();
|
|
3147
|
-
if (!url)
|
|
3148
|
-
|
|
3149
|
-
const res = await sonosRequest(
|
|
3150
|
-
url,
|
|
3151
|
-
`${encodeURIComponent(room)}/state`
|
|
3152
|
-
);
|
|
3083
|
+
if (!url) return { success: false, error: "Sonos not configured." };
|
|
3084
|
+
const res = await sonosRequest(url, `${encodeURIComponent(room)}/state`);
|
|
3153
3085
|
return {
|
|
3154
3086
|
success: true,
|
|
3155
3087
|
data: { room, ...res }
|
|
@@ -3199,7 +3131,8 @@ async function loadKokoro() {
|
|
|
3199
3131
|
console.log(" \u{1F399}\uFE0F Loading Kokoro TTS (~83MB, first use only)...");
|
|
3200
3132
|
const mod = await import("./kokoro-UIHMLMG3.js");
|
|
3201
3133
|
const KokoroTTS = mod.KokoroTTS ?? mod.default?.KokoroTTS;
|
|
3202
|
-
if (!KokoroTTS)
|
|
3134
|
+
if (!KokoroTTS)
|
|
3135
|
+
throw new Error("KokoroTTS class not found in kokoro-js export");
|
|
3203
3136
|
kokoroTts = await KokoroTTS.from_pretrained(KOKORO_MODEL, {
|
|
3204
3137
|
dtype: KOKORO_DTYPE,
|
|
3205
3138
|
device: "cpu"
|
|
@@ -3209,7 +3142,10 @@ async function loadKokoro() {
|
|
|
3209
3142
|
} catch (err) {
|
|
3210
3143
|
kokoroState = "failed";
|
|
3211
3144
|
const msg = err.message ?? String(err);
|
|
3212
|
-
console.warn(
|
|
3145
|
+
console.warn(
|
|
3146
|
+
" \u2139\uFE0F Kokoro TTS unavailable (optional):",
|
|
3147
|
+
msg.slice(0, 120)
|
|
3148
|
+
);
|
|
3213
3149
|
}
|
|
3214
3150
|
})();
|
|
3215
3151
|
return kokoroLoadPromise;
|
|
@@ -3342,12 +3278,18 @@ async function speakKokoro(text, voice) {
|
|
|
3342
3278
|
const tmpFile = join2(tmpdir(), `pulso-tts-${Date.now()}.wav`);
|
|
3343
3279
|
try {
|
|
3344
3280
|
const result = await kokoroTts.generate(text.slice(0, 500), { voice });
|
|
3345
|
-
const wav = float32ToWav(
|
|
3281
|
+
const wav = float32ToWav(
|
|
3282
|
+
result.audio,
|
|
3283
|
+
result.sampling_rate
|
|
3284
|
+
);
|
|
3346
3285
|
writeFileSync2(tmpFile, wav);
|
|
3347
3286
|
await playWavFile(tmpFile);
|
|
3348
3287
|
return true;
|
|
3349
3288
|
} catch (err) {
|
|
3350
|
-
console.warn(
|
|
3289
|
+
console.warn(
|
|
3290
|
+
" \u26A0\uFE0F Kokoro speak failed:",
|
|
3291
|
+
err.message.slice(0, 100)
|
|
3292
|
+
);
|
|
3351
3293
|
return false;
|
|
3352
3294
|
} finally {
|
|
3353
3295
|
try {
|
|
@@ -3368,7 +3310,8 @@ function playWavFile(filePath) {
|
|
|
3368
3310
|
cmd = `aplay "${filePath}" 2>/dev/null || paplay "${filePath}" 2>/dev/null || ffplay -nodisp -autoexit "${filePath}" 2>/dev/null || true`;
|
|
3369
3311
|
}
|
|
3370
3312
|
exec2(cmd, (err) => {
|
|
3371
|
-
if (err)
|
|
3313
|
+
if (err)
|
|
3314
|
+
console.warn(" \u26A0\uFE0F Audio playback error:", err.message.slice(0, 80));
|
|
3372
3315
|
resolve5();
|
|
3373
3316
|
});
|
|
3374
3317
|
});
|
|
@@ -3394,7 +3337,11 @@ async function speak(text, opts = {}) {
|
|
|
3394
3337
|
}
|
|
3395
3338
|
function getTTSInfo() {
|
|
3396
3339
|
if (isKokoroReady()) {
|
|
3397
|
-
return {
|
|
3340
|
+
return {
|
|
3341
|
+
engine: "kokoro",
|
|
3342
|
+
voice: DEFAULT_KOKORO_VOICE,
|
|
3343
|
+
model: KOKORO_MODEL
|
|
3344
|
+
};
|
|
3398
3345
|
}
|
|
3399
3346
|
if (process.platform === "darwin") {
|
|
3400
3347
|
return { engine: "native", voice: getBestMacVoice() };
|
|
@@ -3461,27 +3408,27 @@ function runAppleScript(script) {
|
|
|
3461
3408
|
return new Promise((resolve5, reject) => {
|
|
3462
3409
|
const tmpPath = `/tmp/pulso-as-${Date.now()}-${Math.random().toString(36).slice(2, 6)}.scpt`;
|
|
3463
3410
|
writeFileSync3(tmpPath, script, "utf-8");
|
|
3464
|
-
exec3(
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
try {
|
|
3469
|
-
unlinkSync3(tmpPath);
|
|
3470
|
-
} catch {
|
|
3471
|
-
}
|
|
3472
|
-
if (err) reject(new Error(stderr || err.message));
|
|
3473
|
-
else resolve5(stdout.trim());
|
|
3411
|
+
exec3(`osascript ${tmpPath}`, { timeout: 15e3 }, (err, stdout, stderr) => {
|
|
3412
|
+
try {
|
|
3413
|
+
unlinkSync3(tmpPath);
|
|
3414
|
+
} catch {
|
|
3474
3415
|
}
|
|
3475
|
-
|
|
3416
|
+
if (err) reject(new Error(stderr || err.message));
|
|
3417
|
+
else resolve5(stdout.trim());
|
|
3418
|
+
});
|
|
3476
3419
|
});
|
|
3477
3420
|
}
|
|
3478
3421
|
function runShell2(cmd, timeout = 1e4) {
|
|
3479
3422
|
return new Promise((resolve5, reject) => {
|
|
3480
3423
|
const shell = process.env.SHELL || "/bin/zsh";
|
|
3481
|
-
exec3(
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
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
|
+
);
|
|
3485
3432
|
});
|
|
3486
3433
|
}
|
|
3487
3434
|
function runSwift(code, timeout = 1e4) {
|
|
@@ -3496,11 +3443,14 @@ function runSwift(code, timeout = 1e4) {
|
|
|
3496
3443
|
}
|
|
3497
3444
|
async function hasScreenRecordingPermission() {
|
|
3498
3445
|
try {
|
|
3499
|
-
const out = await runSwift(
|
|
3446
|
+
const out = await runSwift(
|
|
3447
|
+
`
|
|
3500
3448
|
import Cocoa
|
|
3501
3449
|
import CoreGraphics
|
|
3502
3450
|
print(CGPreflightScreenCaptureAccess() ? "granted" : "denied")
|
|
3503
|
-
`,
|
|
3451
|
+
`,
|
|
3452
|
+
6e3
|
|
3453
|
+
);
|
|
3504
3454
|
return out.trim().toLowerCase() === "granted";
|
|
3505
3455
|
} catch {
|
|
3506
3456
|
return false;
|
|
@@ -3513,7 +3463,8 @@ function safePath2(relative) {
|
|
|
3513
3463
|
if (accessLevel === "full") return full;
|
|
3514
3464
|
const relFromHome = full.slice(HOME2.length + 1);
|
|
3515
3465
|
const topDir = relFromHome.split("/")[0];
|
|
3516
|
-
if (!topDir || !SAFE_DIRS2.some((d) => topDir.toLowerCase() === d.toLowerCase()))
|
|
3466
|
+
if (!topDir || !SAFE_DIRS2.some((d) => topDir.toLowerCase() === d.toLowerCase()))
|
|
3467
|
+
return null;
|
|
3517
3468
|
return full;
|
|
3518
3469
|
}
|
|
3519
3470
|
var MacOSAdapter = class {
|
|
@@ -3534,7 +3485,10 @@ var MacOSAdapter = class {
|
|
|
3534
3485
|
try {
|
|
3535
3486
|
await runShell2(`open -a "${app}"`);
|
|
3536
3487
|
} catch (e) {
|
|
3537
|
-
return {
|
|
3488
|
+
return {
|
|
3489
|
+
success: false,
|
|
3490
|
+
error: `Failed to open "${app}": ${e.message}`
|
|
3491
|
+
};
|
|
3538
3492
|
}
|
|
3539
3493
|
let launched = false;
|
|
3540
3494
|
for (let i = 0; i < 10; i++) {
|
|
@@ -3604,7 +3558,8 @@ var MacOSAdapter = class {
|
|
|
3604
3558
|
}
|
|
3605
3559
|
}
|
|
3606
3560
|
async notification(title, message) {
|
|
3607
|
-
if (!title || !message)
|
|
3561
|
+
if (!title || !message)
|
|
3562
|
+
return { success: false, error: "Missing title or message" };
|
|
3608
3563
|
await runAppleScript(
|
|
3609
3564
|
`display notification "${message.replace(/"/g, '\\"')}" with title "${title.replace(/"/g, '\\"')}"`
|
|
3610
3565
|
);
|
|
@@ -3621,10 +3576,18 @@ var MacOSAdapter = class {
|
|
|
3621
3576
|
}
|
|
3622
3577
|
async getBrightness() {
|
|
3623
3578
|
try {
|
|
3624
|
-
const raw = await runShell2(
|
|
3579
|
+
const raw = await runShell2(
|
|
3580
|
+
"brightness -l 2>/dev/null | grep brightness | head -1 | awk '{print $NF}'"
|
|
3581
|
+
);
|
|
3625
3582
|
return { success: true, data: { brightness: parseFloat(raw) || 0.5 } };
|
|
3626
3583
|
} catch {
|
|
3627
|
-
return {
|
|
3584
|
+
return {
|
|
3585
|
+
success: true,
|
|
3586
|
+
data: {
|
|
3587
|
+
brightness: "unknown",
|
|
3588
|
+
note: "Install 'brightness' via brew for control"
|
|
3589
|
+
}
|
|
3590
|
+
};
|
|
3628
3591
|
}
|
|
3629
3592
|
}
|
|
3630
3593
|
async setBrightness(level) {
|
|
@@ -3641,7 +3604,10 @@ var MacOSAdapter = class {
|
|
|
3641
3604
|
["os", "sw_vers -productVersion"],
|
|
3642
3605
|
["cpu", "sysctl -n machdep.cpu.brand_string"],
|
|
3643
3606
|
["memory", "vm_stat | head -5"],
|
|
3644
|
-
[
|
|
3607
|
+
[
|
|
3608
|
+
"disk",
|
|
3609
|
+
`df -h / | tail -1 | awk '{print $3 " used / " $2 " total (" $5 " used)"}'`
|
|
3610
|
+
],
|
|
3645
3611
|
["uptime", "uptime | sed 's/.*up /up /' | sed 's/,.*//'"],
|
|
3646
3612
|
["battery", "pmset -g batt | grep -Eo '\\d+%'"],
|
|
3647
3613
|
["wifi", "networksetup -getairportnetwork en0 2>/dev/null | cut -d: -f2"],
|
|
@@ -3676,27 +3642,51 @@ var MacOSAdapter = class {
|
|
|
3676
3642
|
const t = Number(timeout) || 15e3;
|
|
3677
3643
|
try {
|
|
3678
3644
|
const output2 = await runShell2(command, t);
|
|
3679
|
-
return {
|
|
3645
|
+
return {
|
|
3646
|
+
success: true,
|
|
3647
|
+
data: {
|
|
3648
|
+
command,
|
|
3649
|
+
output: output2.slice(0, 1e4),
|
|
3650
|
+
truncated: output2.length > 1e4
|
|
3651
|
+
}
|
|
3652
|
+
};
|
|
3680
3653
|
} catch (err) {
|
|
3681
|
-
return {
|
|
3654
|
+
return {
|
|
3655
|
+
success: false,
|
|
3656
|
+
error: `Shell error: ${err.message.slice(0, 2e3)}`
|
|
3657
|
+
};
|
|
3682
3658
|
}
|
|
3683
3659
|
}
|
|
3684
3660
|
async runShortcut(name, input2) {
|
|
3685
3661
|
if (!name) return { success: false, error: "Missing shortcut name" };
|
|
3686
3662
|
const inputFlag = input2 ? `--input-type text --input "${input2.replace(/"/g, '\\"')}"` : "";
|
|
3687
|
-
const result = await runShell2(
|
|
3688
|
-
|
|
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
|
+
};
|
|
3689
3671
|
}
|
|
3690
3672
|
async dnd(enabled) {
|
|
3691
3673
|
if (enabled !== void 0) {
|
|
3692
3674
|
try {
|
|
3693
|
-
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
|
+
);
|
|
3694
3678
|
return { success: true, data: { dnd: enabled, note: "DND toggled" } };
|
|
3695
3679
|
} catch {
|
|
3696
|
-
return {
|
|
3680
|
+
return {
|
|
3681
|
+
success: true,
|
|
3682
|
+
data: { dnd: enabled, note: "Set DND manually in Control Center" }
|
|
3683
|
+
};
|
|
3697
3684
|
}
|
|
3698
3685
|
}
|
|
3699
|
-
return {
|
|
3686
|
+
return {
|
|
3687
|
+
success: true,
|
|
3688
|
+
data: { note: "Pass enabled: true/false to toggle DND" }
|
|
3689
|
+
};
|
|
3700
3690
|
}
|
|
3701
3691
|
/* ══════════════════════════════════════════════════════════
|
|
3702
3692
|
* Clipboard
|
|
@@ -3730,22 +3720,37 @@ var MacOSAdapter = class {
|
|
|
3730
3720
|
} catch (ssErr) {
|
|
3731
3721
|
const msg = ssErr.message || "";
|
|
3732
3722
|
if (msg.includes("could not create image") || msg.includes("display")) {
|
|
3733
|
-
return {
|
|
3723
|
+
return {
|
|
3724
|
+
image: "",
|
|
3725
|
+
format: "jpeg",
|
|
3726
|
+
note: "Screen Recording permission required."
|
|
3727
|
+
};
|
|
3734
3728
|
}
|
|
3735
3729
|
return { image: "", format: "jpeg", note: `Screenshot failed: ${msg}` };
|
|
3736
3730
|
}
|
|
3737
3731
|
if (!existsSync2(pngPath)) {
|
|
3738
|
-
return {
|
|
3732
|
+
return {
|
|
3733
|
+
image: "",
|
|
3734
|
+
format: "jpeg",
|
|
3735
|
+
note: "Screenshot failed \u2014 Screen Recording permission needed."
|
|
3736
|
+
};
|
|
3739
3737
|
}
|
|
3740
3738
|
try {
|
|
3741
|
-
await runShell2(
|
|
3739
|
+
await runShell2(
|
|
3740
|
+
`sips --resampleWidth 1600 --setProperty format jpeg --setProperty formatOptions 75 ${pngPath} --out ${jpgPath}`,
|
|
3741
|
+
1e4
|
|
3742
|
+
);
|
|
3742
3743
|
} catch {
|
|
3743
3744
|
const buf2 = readFileSync2(pngPath);
|
|
3744
3745
|
try {
|
|
3745
3746
|
unlinkSync3(pngPath);
|
|
3746
3747
|
} catch {
|
|
3747
3748
|
}
|
|
3748
|
-
return {
|
|
3749
|
+
return {
|
|
3750
|
+
image: `data:image/png;base64,${buf2.toString("base64")}`,
|
|
3751
|
+
format: "png",
|
|
3752
|
+
note: "Full screen screenshot (PNG fallback)"
|
|
3753
|
+
};
|
|
3749
3754
|
}
|
|
3750
3755
|
const buf = readFileSync2(jpgPath);
|
|
3751
3756
|
const base64 = buf.toString("base64");
|
|
@@ -3780,7 +3785,8 @@ print("\\(Int(main.frame.width)),\\(Int(main.frame.height))")`);
|
|
|
3780
3785
|
}
|
|
3781
3786
|
async mouseClick(x, y, button) {
|
|
3782
3787
|
const btn = button || "left";
|
|
3783
|
-
if (isNaN(x) || isNaN(y))
|
|
3788
|
+
if (isNaN(x) || isNaN(y))
|
|
3789
|
+
return { success: false, error: "Missing x, y coordinates" };
|
|
3784
3790
|
const mouseType = btn === "right" ? "rightMouseDown" : "leftMouseDown";
|
|
3785
3791
|
const mouseTypeUp = btn === "right" ? "rightMouseUp" : "leftMouseUp";
|
|
3786
3792
|
const mouseButton = btn === "right" ? ".right" : ".left";
|
|
@@ -3797,7 +3803,8 @@ print("clicked")`;
|
|
|
3797
3803
|
return { success: true, data: { clicked: { x, y }, button: btn } };
|
|
3798
3804
|
}
|
|
3799
3805
|
async mouseDoubleClick(x, y) {
|
|
3800
|
-
if (isNaN(x) || isNaN(y))
|
|
3806
|
+
if (isNaN(x) || isNaN(y))
|
|
3807
|
+
return { success: false, error: "Missing x, y coordinates" };
|
|
3801
3808
|
const swift = `
|
|
3802
3809
|
import Cocoa
|
|
3803
3810
|
let p = CGPoint(x: ${x}, y: ${y})
|
|
@@ -3823,7 +3830,8 @@ print("double-clicked")`;
|
|
|
3823
3830
|
async mouseScroll(scrollY, scrollX, x, y) {
|
|
3824
3831
|
const sx = scrollX || 0;
|
|
3825
3832
|
const sy = scrollY || 0;
|
|
3826
|
-
if (!sy && !sx)
|
|
3833
|
+
if (!sy && !sx)
|
|
3834
|
+
return { success: false, error: "Missing scrollY or scrollX" };
|
|
3827
3835
|
const swift = `
|
|
3828
3836
|
import Cocoa
|
|
3829
3837
|
let p = CGPoint(x: ${x || 0}, y: ${y || 0})
|
|
@@ -3834,10 +3842,14 @@ let scroll = CGEvent(scrollWheelEvent2Source: nil, units: .pixel, wheelCount: 2,
|
|
|
3834
3842
|
scroll.post(tap: .cghidEventTap)
|
|
3835
3843
|
print("scrolled")`;
|
|
3836
3844
|
await runSwift(swift);
|
|
3837
|
-
return {
|
|
3845
|
+
return {
|
|
3846
|
+
success: true,
|
|
3847
|
+
data: { scrolled: { x: x || 0, y: y || 0, scrollY: sy, scrollX: sx } }
|
|
3848
|
+
};
|
|
3838
3849
|
}
|
|
3839
3850
|
async mouseMove(x, y) {
|
|
3840
|
-
if (isNaN(x) || isNaN(y))
|
|
3851
|
+
if (isNaN(x) || isNaN(y))
|
|
3852
|
+
return { success: false, error: "Missing x, y coordinates" };
|
|
3841
3853
|
const swift = `
|
|
3842
3854
|
import Cocoa
|
|
3843
3855
|
let p = CGPoint(x: ${x}, y: ${y})
|
|
@@ -3869,7 +3881,12 @@ let u = CGEvent(mouseEventSource: nil, mouseType: .leftMouseUp, mouseCursorPosit
|
|
|
3869
3881
|
u.post(tap: .cghidEventTap)
|
|
3870
3882
|
print("dragged")`;
|
|
3871
3883
|
await runSwift(swift);
|
|
3872
|
-
return {
|
|
3884
|
+
return {
|
|
3885
|
+
success: true,
|
|
3886
|
+
data: {
|
|
3887
|
+
dragged: { from: { x: fromX, y: fromY }, to: { x: toX, y: toY } }
|
|
3888
|
+
}
|
|
3889
|
+
};
|
|
3873
3890
|
}
|
|
3874
3891
|
async getCursorPosition() {
|
|
3875
3892
|
const swift = `
|
|
@@ -3937,12 +3954,19 @@ print("\\(x),\\(y)")`;
|
|
|
3937
3954
|
const keyCode = keyCodeMap[key.toLowerCase()];
|
|
3938
3955
|
if (keyCode !== void 0) {
|
|
3939
3956
|
const using = modStr.length > 0 ? ` using {${modStr.join(", ")}}` : "";
|
|
3940
|
-
await runAppleScript(
|
|
3957
|
+
await runAppleScript(
|
|
3958
|
+
`tell application "System Events" to key code ${keyCode}${using}`
|
|
3959
|
+
);
|
|
3941
3960
|
} else if (key.length === 1) {
|
|
3942
3961
|
const using = modStr.length > 0 ? ` using {${modStr.join(", ")}}` : "";
|
|
3943
|
-
await runAppleScript(
|
|
3962
|
+
await runAppleScript(
|
|
3963
|
+
`tell application "System Events" to keystroke "${key}"${using}`
|
|
3964
|
+
);
|
|
3944
3965
|
} else {
|
|
3945
|
-
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
|
+
};
|
|
3946
3970
|
}
|
|
3947
3971
|
return { success: true, data: { pressed: key, modifiers: mods } };
|
|
3948
3972
|
}
|
|
@@ -3950,7 +3974,13 @@ print("\\(x),\\(y)")`;
|
|
|
3950
3974
|
* Browser Automation
|
|
3951
3975
|
* ══════════════════════════════════════════════════════════ */
|
|
3952
3976
|
async browserListTabs() {
|
|
3953
|
-
const browsers = [
|
|
3977
|
+
const browsers = [
|
|
3978
|
+
"Google Chrome",
|
|
3979
|
+
"Safari",
|
|
3980
|
+
"Arc",
|
|
3981
|
+
"Firefox",
|
|
3982
|
+
"Microsoft Edge"
|
|
3983
|
+
];
|
|
3954
3984
|
const allTabs = [];
|
|
3955
3985
|
for (const browser of browsers) {
|
|
3956
3986
|
try {
|
|
@@ -3974,7 +4004,12 @@ print("\\(x),\\(y)")`;
|
|
|
3974
4004
|
const tabStr = rest.join("~~~");
|
|
3975
4005
|
const pairs = tabStr.split("|||").filter(Boolean);
|
|
3976
4006
|
for (let i = 0; i < pairs.length - 1; i += 2) {
|
|
3977
|
-
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
|
+
});
|
|
3978
4013
|
}
|
|
3979
4014
|
} else {
|
|
3980
4015
|
const tabData = await runAppleScript(`
|
|
@@ -3992,7 +4027,12 @@ print("\\(x),\\(y)")`;
|
|
|
3992
4027
|
const tabStr = rest.join("~~~");
|
|
3993
4028
|
const pairs = tabStr.split("|||").filter(Boolean);
|
|
3994
4029
|
for (let i = 0; i < pairs.length - 1; i += 2) {
|
|
3995
|
-
allTabs.push({
|
|
4030
|
+
allTabs.push({
|
|
4031
|
+
browser,
|
|
4032
|
+
title: pairs[i],
|
|
4033
|
+
url: pairs[i + 1],
|
|
4034
|
+
active: pairs[i + 1] === activeURL.trim()
|
|
4035
|
+
});
|
|
3996
4036
|
}
|
|
3997
4037
|
}
|
|
3998
4038
|
} catch {
|
|
@@ -4020,7 +4060,10 @@ print("\\(x),\\(y)")`;
|
|
|
4020
4060
|
}
|
|
4021
4061
|
return { success: true, data: { navigated: url, browser: b } };
|
|
4022
4062
|
} catch (err) {
|
|
4023
|
-
return {
|
|
4063
|
+
return {
|
|
4064
|
+
success: false,
|
|
4065
|
+
error: `Failed to navigate: ${err.message}`
|
|
4066
|
+
};
|
|
4024
4067
|
}
|
|
4025
4068
|
}
|
|
4026
4069
|
async browserNewTab(url, browser) {
|
|
@@ -4043,7 +4086,10 @@ print("\\(x),\\(y)")`;
|
|
|
4043
4086
|
}
|
|
4044
4087
|
return { success: true, data: { opened: url, browser: b } };
|
|
4045
4088
|
} catch (err) {
|
|
4046
|
-
return {
|
|
4089
|
+
return {
|
|
4090
|
+
success: false,
|
|
4091
|
+
error: `Failed to open window: ${err.message}`
|
|
4092
|
+
};
|
|
4047
4093
|
}
|
|
4048
4094
|
}
|
|
4049
4095
|
async browserReadPage(browser, maxLength) {
|
|
@@ -4069,11 +4115,17 @@ print("\\(x),\\(y)")`;
|
|
|
4069
4115
|
} catch {
|
|
4070
4116
|
try {
|
|
4071
4117
|
const savedClipboard = await runShell2("pbpaste 2>/dev/null || true");
|
|
4072
|
-
await runAppleScript(
|
|
4118
|
+
await runAppleScript(
|
|
4119
|
+
`tell application "${b.replace(/"/g, '\\"')}" to activate`
|
|
4120
|
+
);
|
|
4073
4121
|
await new Promise((r) => setTimeout(r, 300));
|
|
4074
|
-
await runAppleScript(
|
|
4122
|
+
await runAppleScript(
|
|
4123
|
+
'tell application "System Events" to keystroke "a" using command down'
|
|
4124
|
+
);
|
|
4075
4125
|
await new Promise((r) => setTimeout(r, 200));
|
|
4076
|
-
await runAppleScript(
|
|
4126
|
+
await runAppleScript(
|
|
4127
|
+
'tell application "System Events" to keystroke "c" using command down'
|
|
4128
|
+
);
|
|
4077
4129
|
await new Promise((r) => setTimeout(r, 300));
|
|
4078
4130
|
content = await runShell2("pbpaste");
|
|
4079
4131
|
method = "clipboard";
|
|
@@ -4082,18 +4134,29 @@ print("\\(x),\\(y)")`;
|
|
|
4082
4134
|
execSync2(`echo ${JSON.stringify(savedClipboard)} | pbcopy`);
|
|
4083
4135
|
}
|
|
4084
4136
|
} catch (clipErr) {
|
|
4085
|
-
return {
|
|
4137
|
+
return {
|
|
4138
|
+
success: false,
|
|
4139
|
+
error: `Could not read page: ${clipErr.message}`
|
|
4140
|
+
};
|
|
4086
4141
|
}
|
|
4087
4142
|
}
|
|
4088
4143
|
let pageUrl = "";
|
|
4089
4144
|
let pageTitle = "";
|
|
4090
4145
|
try {
|
|
4091
4146
|
if (b === "Safari") {
|
|
4092
|
-
pageUrl = await runAppleScript(
|
|
4093
|
-
|
|
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
|
+
);
|
|
4094
4153
|
} else {
|
|
4095
|
-
pageUrl = await runAppleScript(
|
|
4096
|
-
|
|
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
|
+
);
|
|
4097
4160
|
}
|
|
4098
4161
|
} catch {
|
|
4099
4162
|
}
|
|
@@ -4133,21 +4196,48 @@ end tell`);
|
|
|
4133
4196
|
}
|
|
4134
4197
|
return { success: true, data: { result: (result || "").slice(0, 5e3) } };
|
|
4135
4198
|
} catch (err) {
|
|
4136
|
-
return {
|
|
4199
|
+
return {
|
|
4200
|
+
success: false,
|
|
4201
|
+
error: `JS execution failed: ${err.message}`
|
|
4202
|
+
};
|
|
4137
4203
|
}
|
|
4138
4204
|
}
|
|
4139
4205
|
async browserListProfiles() {
|
|
4140
4206
|
const os = homedir2();
|
|
4141
4207
|
const browserPaths = [
|
|
4142
|
-
{
|
|
4143
|
-
|
|
4144
|
-
|
|
4145
|
-
|
|
4146
|
-
{
|
|
4147
|
-
|
|
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
|
+
},
|
|
4148
4232
|
{ browser: "Vivaldi", dir: `${os}/Library/Application Support/Vivaldi` },
|
|
4149
|
-
{
|
|
4150
|
-
|
|
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
|
+
}
|
|
4151
4241
|
];
|
|
4152
4242
|
const profiles = [];
|
|
4153
4243
|
for (const { browser, dir } of browserPaths) {
|
|
@@ -4204,7 +4294,9 @@ end tell`);
|
|
|
4204
4294
|
}
|
|
4205
4295
|
let running = [];
|
|
4206
4296
|
try {
|
|
4207
|
-
const ps = await runShell2(
|
|
4297
|
+
const ps = await runShell2(
|
|
4298
|
+
"ps aux | grep -E '(Chrome|Edge|Brave|Firefox|Opera|Vivaldi|Arc)' | grep -v grep | awk '{print $11}'"
|
|
4299
|
+
);
|
|
4208
4300
|
running = ps.split("\n").filter(Boolean);
|
|
4209
4301
|
} catch {
|
|
4210
4302
|
}
|
|
@@ -4213,7 +4305,9 @@ end tell`);
|
|
|
4213
4305
|
data: {
|
|
4214
4306
|
profiles: profiles.map((p) => ({
|
|
4215
4307
|
...p,
|
|
4216
|
-
isRunning: running.some(
|
|
4308
|
+
isRunning: running.some(
|
|
4309
|
+
(r) => r.toLowerCase().includes(p.browser.toLowerCase().replace(/ /g, ""))
|
|
4310
|
+
)
|
|
4217
4311
|
})),
|
|
4218
4312
|
total: profiles.length
|
|
4219
4313
|
}
|
|
@@ -4244,15 +4338,27 @@ end tell`);
|
|
|
4244
4338
|
const raw = await runAppleScript(script);
|
|
4245
4339
|
return raw.split("\n").filter(Boolean).map((line) => {
|
|
4246
4340
|
const [cal, summary, start, end] = line.split(" | ");
|
|
4247
|
-
return {
|
|
4341
|
+
return {
|
|
4342
|
+
calendar: cal?.trim(),
|
|
4343
|
+
title: summary?.trim() || "",
|
|
4344
|
+
startDate: start?.trim() || "",
|
|
4345
|
+
endDate: end?.trim()
|
|
4346
|
+
};
|
|
4248
4347
|
});
|
|
4249
4348
|
}
|
|
4250
4349
|
async calendarCreate(title, startDate, endDate, calendar, notes) {
|
|
4251
|
-
if (!title || !startDate)
|
|
4350
|
+
if (!title || !startDate)
|
|
4351
|
+
return { success: false, error: "Missing title or start date" };
|
|
4252
4352
|
const parseDate = (iso) => {
|
|
4253
4353
|
const d = new Date(iso);
|
|
4254
4354
|
if (isNaN(d.getTime())) return null;
|
|
4255
|
-
return {
|
|
4355
|
+
return {
|
|
4356
|
+
y: d.getFullYear(),
|
|
4357
|
+
mo: d.getMonth() + 1,
|
|
4358
|
+
d: d.getDate(),
|
|
4359
|
+
h: d.getHours(),
|
|
4360
|
+
mi: d.getMinutes()
|
|
4361
|
+
};
|
|
4256
4362
|
};
|
|
4257
4363
|
const buildDateScript = (varName, iso) => {
|
|
4258
4364
|
const p = parseDate(iso);
|
|
@@ -4268,7 +4374,8 @@ tell ${varName}
|
|
|
4268
4374
|
end tell`;
|
|
4269
4375
|
};
|
|
4270
4376
|
const startDateScript = buildDateScript("startD", startDate);
|
|
4271
|
-
if (!startDateScript)
|
|
4377
|
+
if (!startDateScript)
|
|
4378
|
+
return { success: false, error: `Invalid start date: ${startDate}` };
|
|
4272
4379
|
const endDateScript = endDate ? buildDateScript("endD", endDate) : "";
|
|
4273
4380
|
const calTarget = calendar ? `calendar "${calendar.replace(/"/g, '\\"')}"` : "default calendar";
|
|
4274
4381
|
const notesPart = notes ? `
|
|
@@ -4287,7 +4394,10 @@ tell application "Calendar"
|
|
|
4287
4394
|
set newEvent to make new event with properties {summary:"${title.replace(/"/g, '\\"')}", start date:startD}${endPart}${notesPart}
|
|
4288
4395
|
end tell
|
|
4289
4396
|
end tell`);
|
|
4290
|
-
return {
|
|
4397
|
+
return {
|
|
4398
|
+
success: true,
|
|
4399
|
+
data: { created: title, start: startDate, end: endDate || "1 hour" }
|
|
4400
|
+
};
|
|
4291
4401
|
}
|
|
4292
4402
|
/* ══════════════════════════════════════════════════════════
|
|
4293
4403
|
* Productivity: Reminders
|
|
@@ -4312,11 +4422,22 @@ end tell`);
|
|
|
4312
4422
|
const raw = await runAppleScript(script);
|
|
4313
4423
|
const reminders = raw.split("\n").filter(Boolean).map((line) => {
|
|
4314
4424
|
const parts = line.split(" | ");
|
|
4315
|
-
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() };
|
|
4316
4430
|
});
|
|
4317
4431
|
return { success: true, data: { reminders, count: reminders.length } };
|
|
4318
4432
|
} catch {
|
|
4319
|
-
return {
|
|
4433
|
+
return {
|
|
4434
|
+
success: true,
|
|
4435
|
+
data: {
|
|
4436
|
+
reminders: [],
|
|
4437
|
+
count: 0,
|
|
4438
|
+
note: "No reminders or Reminders app not accessible"
|
|
4439
|
+
}
|
|
4440
|
+
};
|
|
4320
4441
|
}
|
|
4321
4442
|
}
|
|
4322
4443
|
async reminderCreate(title, list, dueDate) {
|
|
@@ -4324,7 +4445,9 @@ end tell`);
|
|
|
4324
4445
|
let listName = list || "";
|
|
4325
4446
|
if (!listName) {
|
|
4326
4447
|
try {
|
|
4327
|
-
listName = (await runAppleScript(
|
|
4448
|
+
listName = (await runAppleScript(
|
|
4449
|
+
'tell application "Reminders" to return name of default list'
|
|
4450
|
+
)).trim();
|
|
4328
4451
|
} catch {
|
|
4329
4452
|
listName = "Reminders";
|
|
4330
4453
|
}
|
|
@@ -4350,20 +4473,27 @@ tell application "Reminders"
|
|
|
4350
4473
|
make new reminder with properties {name:"${title.replace(/"/g, '\\"')}"${dueProperty}}
|
|
4351
4474
|
end tell
|
|
4352
4475
|
end tell`);
|
|
4353
|
-
return {
|
|
4476
|
+
return {
|
|
4477
|
+
success: true,
|
|
4478
|
+
data: { created: title, due: dueDate || "none", list: listName }
|
|
4479
|
+
};
|
|
4354
4480
|
}
|
|
4355
4481
|
/* ══════════════════════════════════════════════════════════
|
|
4356
4482
|
* Productivity: Messages
|
|
4357
4483
|
* ══════════════════════════════════════════════════════════ */
|
|
4358
4484
|
async sendMessage(to, message) {
|
|
4359
|
-
if (!to || !message)
|
|
4485
|
+
if (!to || !message)
|
|
4486
|
+
return { success: false, error: "Missing 'to' or 'message'" };
|
|
4360
4487
|
await runAppleScript(`
|
|
4361
4488
|
tell application "Messages"
|
|
4362
4489
|
set targetService to 1st account whose service type = iMessage
|
|
4363
4490
|
set targetBuddy to participant "${to.replace(/"/g, '\\"')}" of targetService
|
|
4364
4491
|
send "${message.replace(/"/g, '\\"')}" to targetBuddy
|
|
4365
4492
|
end tell`);
|
|
4366
|
-
return {
|
|
4493
|
+
return {
|
|
4494
|
+
success: true,
|
|
4495
|
+
data: { sent: true, to, message: message.slice(0, 100) }
|
|
4496
|
+
};
|
|
4367
4497
|
}
|
|
4368
4498
|
/* ══════════════════════════════════════════════════════════
|
|
4369
4499
|
* Productivity: Contacts
|
|
@@ -4390,7 +4520,11 @@ end tell`);
|
|
|
4390
4520
|
end tell`);
|
|
4391
4521
|
return raw.split("\n").filter(Boolean).map((line) => {
|
|
4392
4522
|
const [name, email, phone] = line.split(" | ");
|
|
4393
|
-
return {
|
|
4523
|
+
return {
|
|
4524
|
+
name: name?.trim() || "",
|
|
4525
|
+
email: email?.trim(),
|
|
4526
|
+
phone: phone?.trim()
|
|
4527
|
+
};
|
|
4394
4528
|
});
|
|
4395
4529
|
}
|
|
4396
4530
|
/* ══════════════════════════════════════════════════════════
|
|
@@ -4427,11 +4561,15 @@ end tell`);
|
|
|
4427
4561
|
* Email
|
|
4428
4562
|
* ══════════════════════════════════════════════════════════ */
|
|
4429
4563
|
async emailSend(to, subject, body, method) {
|
|
4430
|
-
if (!to || !subject || !body)
|
|
4564
|
+
if (!to || !subject || !body)
|
|
4565
|
+
return { success: false, error: "Missing to, subject, or body" };
|
|
4431
4566
|
if (method === "gmail") {
|
|
4432
4567
|
const gmailUrl = `https://mail.google.com/mail/u/0/?view=cm&fs=1&to=${encodeURIComponent(to)}&su=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`;
|
|
4433
4568
|
await runShell2(`open "${gmailUrl}"`);
|
|
4434
|
-
return {
|
|
4569
|
+
return {
|
|
4570
|
+
success: true,
|
|
4571
|
+
data: { method: "gmail", to, subject, note: "Gmail compose opened." }
|
|
4572
|
+
};
|
|
4435
4573
|
}
|
|
4436
4574
|
try {
|
|
4437
4575
|
await runAppleScript(`
|
|
@@ -4442,11 +4580,22 @@ end tell`);
|
|
|
4442
4580
|
end tell
|
|
4443
4581
|
send newMessage
|
|
4444
4582
|
end tell`);
|
|
4445
|
-
return {
|
|
4583
|
+
return {
|
|
4584
|
+
success: true,
|
|
4585
|
+
data: { method: "mail", to, subject, sent: true }
|
|
4586
|
+
};
|
|
4446
4587
|
} catch (err) {
|
|
4447
4588
|
const gmailUrl = `https://mail.google.com/mail/u/0/?view=cm&fs=1&to=${encodeURIComponent(to)}&su=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`;
|
|
4448
4589
|
await runShell2(`open "${gmailUrl}"`);
|
|
4449
|
-
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
|
+
};
|
|
4450
4599
|
}
|
|
4451
4600
|
}
|
|
4452
4601
|
/* ══════════════════════════════════════════════════════════
|
|
@@ -4455,15 +4604,33 @@ end tell`);
|
|
|
4455
4604
|
async fileRead(path) {
|
|
4456
4605
|
if (!path) return { success: false, error: "Missing file path" };
|
|
4457
4606
|
const fullPath = safePath2(path);
|
|
4458
|
-
if (!fullPath)
|
|
4459
|
-
|
|
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}` };
|
|
4460
4614
|
const content = readFileSync2(fullPath, "utf-8");
|
|
4461
|
-
return {
|
|
4462
|
-
|
|
4463
|
-
|
|
4464
|
-
|
|
4465
|
-
|
|
4466
|
-
|
|
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
|
+
};
|
|
4624
|
+
}
|
|
4625
|
+
async fileWrite(path, content) {
|
|
4626
|
+
if (!path || !content)
|
|
4627
|
+
return { success: false, error: "Missing path or content" };
|
|
4628
|
+
const fullPath = safePath2(path);
|
|
4629
|
+
if (!fullPath)
|
|
4630
|
+
return {
|
|
4631
|
+
success: false,
|
|
4632
|
+
error: `Access denied. Only files in ${SAFE_DIRS2.join(", ")} are allowed.`
|
|
4633
|
+
};
|
|
4467
4634
|
writeFileSync3(fullPath, content, "utf-8");
|
|
4468
4635
|
return { success: true, data: { path, written: content.length } };
|
|
4469
4636
|
}
|
|
@@ -4471,32 +4638,45 @@ end tell`);
|
|
|
4471
4638
|
const dirPath = path || "Desktop";
|
|
4472
4639
|
const fullDir = safePath2(dirPath);
|
|
4473
4640
|
if (!fullDir) return { success: false, error: `Access denied: ${dirPath}` };
|
|
4474
|
-
if (!existsSync2(fullDir))
|
|
4641
|
+
if (!existsSync2(fullDir))
|
|
4642
|
+
return { success: false, error: `Directory not found: ${dirPath}` };
|
|
4475
4643
|
const entries = readdirSync2(fullDir).map((name) => {
|
|
4476
4644
|
try {
|
|
4477
4645
|
const st = statSync2(join3(fullDir, name));
|
|
4478
|
-
return {
|
|
4646
|
+
return {
|
|
4647
|
+
name,
|
|
4648
|
+
type: st.isDirectory() ? "dir" : "file",
|
|
4649
|
+
size: st.size,
|
|
4650
|
+
modified: st.mtime.toISOString()
|
|
4651
|
+
};
|
|
4479
4652
|
} catch {
|
|
4480
4653
|
return { name, type: "unknown", size: 0, modified: "" };
|
|
4481
4654
|
}
|
|
4482
4655
|
});
|
|
4483
|
-
return {
|
|
4656
|
+
return {
|
|
4657
|
+
success: true,
|
|
4658
|
+
data: { path: dirPath, entries, count: entries.length }
|
|
4659
|
+
};
|
|
4484
4660
|
}
|
|
4485
4661
|
async fileMove(source, destination) {
|
|
4486
|
-
if (!source || !destination)
|
|
4662
|
+
if (!source || !destination)
|
|
4663
|
+
return { success: false, error: "Missing from/to paths" };
|
|
4487
4664
|
const fullSrc = safePath2(source);
|
|
4488
4665
|
const fullDst = safePath2(destination);
|
|
4489
4666
|
if (!fullSrc || !fullDst) return { success: false, error: "Access denied" };
|
|
4490
|
-
if (!existsSync2(fullSrc))
|
|
4667
|
+
if (!existsSync2(fullSrc))
|
|
4668
|
+
return { success: false, error: `Source not found: ${source}` };
|
|
4491
4669
|
renameSync2(fullSrc, fullDst);
|
|
4492
4670
|
return { success: true, data: { moved: source, to: destination } };
|
|
4493
4671
|
}
|
|
4494
4672
|
async fileCopy(source, destination) {
|
|
4495
|
-
if (!source || !destination)
|
|
4673
|
+
if (!source || !destination)
|
|
4674
|
+
return { success: false, error: "Missing from/to paths" };
|
|
4496
4675
|
const fullSrc = safePath2(source);
|
|
4497
4676
|
const fullDst = safePath2(destination);
|
|
4498
4677
|
if (!fullSrc || !fullDst) return { success: false, error: "Access denied" };
|
|
4499
|
-
if (!existsSync2(fullSrc))
|
|
4678
|
+
if (!existsSync2(fullSrc))
|
|
4679
|
+
return { success: false, error: `Source not found: ${source}` };
|
|
4500
4680
|
copyFileSync2(fullSrc, fullDst);
|
|
4501
4681
|
return { success: true, data: { copied: source, to: destination } };
|
|
4502
4682
|
}
|
|
@@ -4504,15 +4684,19 @@ end tell`);
|
|
|
4504
4684
|
if (!path) return { success: false, error: "Missing path" };
|
|
4505
4685
|
const fullTarget = safePath2(path);
|
|
4506
4686
|
if (!fullTarget) return { success: false, error: "Access denied" };
|
|
4507
|
-
if (!existsSync2(fullTarget))
|
|
4508
|
-
|
|
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
|
+
);
|
|
4509
4692
|
return { success: true, data: { deleted: path, method: "moved_to_trash" } };
|
|
4510
4693
|
}
|
|
4511
4694
|
async fileInfo(path) {
|
|
4512
4695
|
if (!path) return { success: false, error: "Missing path" };
|
|
4513
4696
|
const fullF = safePath2(path);
|
|
4514
4697
|
if (!fullF) return { success: false, error: "Access denied" };
|
|
4515
|
-
if (!existsSync2(fullF))
|
|
4698
|
+
if (!existsSync2(fullF))
|
|
4699
|
+
return { success: false, error: `Not found: ${path}` };
|
|
4516
4700
|
const st = statSync2(fullF);
|
|
4517
4701
|
return {
|
|
4518
4702
|
success: true,
|
|
@@ -4533,7 +4717,10 @@ end tell`);
|
|
|
4533
4717
|
const dlDest = destination || `Downloads/${basename2(new URL(url).pathname) || "download"}`;
|
|
4534
4718
|
const fullDl = safePath2(dlDest);
|
|
4535
4719
|
if (!fullDl) return { success: false, error: "Access denied" };
|
|
4536
|
-
await runShell2(
|
|
4720
|
+
await runShell2(
|
|
4721
|
+
`curl -sL -o "${fullDl}" "${url.replace(/"/g, '\\"')}"`,
|
|
4722
|
+
6e4
|
|
4723
|
+
);
|
|
4537
4724
|
const size = existsSync2(fullDl) ? statSync2(fullDl).size : 0;
|
|
4538
4725
|
return { success: true, data: { downloaded: url, saved: dlDest, size } };
|
|
4539
4726
|
}
|
|
@@ -4570,14 +4757,20 @@ end tell`);
|
|
|
4570
4757
|
}
|
|
4571
4758
|
async windowFocus(app) {
|
|
4572
4759
|
if (!app) return { success: false, error: "Missing app name" };
|
|
4573
|
-
await runAppleScript(
|
|
4760
|
+
await runAppleScript(
|
|
4761
|
+
`tell application "${app.replace(/"/g, '\\"')}" to activate`
|
|
4762
|
+
);
|
|
4574
4763
|
return { success: true, data: { focused: app } };
|
|
4575
4764
|
}
|
|
4576
4765
|
async windowResize(app, x, y, width, height) {
|
|
4577
4766
|
if (!app) return { success: false, error: "Missing app name" };
|
|
4578
4767
|
const posPart = x !== void 0 && y !== void 0 ? `set position of window 1 to {${x}, ${y}}` : "";
|
|
4579
4768
|
const sizePart = width !== void 0 && height !== void 0 ? `set size of window 1 to {${width}, ${height}}` : "";
|
|
4580
|
-
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
|
+
};
|
|
4581
4774
|
await runAppleScript(`
|
|
4582
4775
|
tell application "System Events"
|
|
4583
4776
|
tell process "${app.replace(/"/g, '\\"')}"
|
|
@@ -4585,7 +4778,14 @@ end tell`);
|
|
|
4585
4778
|
${sizePart}
|
|
4586
4779
|
end tell
|
|
4587
4780
|
end tell`);
|
|
4588
|
-
return {
|
|
4781
|
+
return {
|
|
4782
|
+
success: true,
|
|
4783
|
+
data: {
|
|
4784
|
+
app,
|
|
4785
|
+
position: posPart ? { x, y } : "unchanged",
|
|
4786
|
+
size: sizePart ? { width, height } : "unchanged"
|
|
4787
|
+
}
|
|
4788
|
+
};
|
|
4589
4789
|
}
|
|
4590
4790
|
/* ══════════════════════════════════════════════════════════
|
|
4591
4791
|
* OCR
|
|
@@ -4594,7 +4794,8 @@ end tell`);
|
|
|
4594
4794
|
if (!imagePath) return { success: false, error: "Missing image path" };
|
|
4595
4795
|
const fullImg = imagePath.startsWith("/tmp/") ? imagePath : safePath2(imagePath);
|
|
4596
4796
|
if (!fullImg) return { success: false, error: "Access denied" };
|
|
4597
|
-
if (!existsSync2(fullImg))
|
|
4797
|
+
if (!existsSync2(fullImg))
|
|
4798
|
+
return { success: false, error: `Image not found: ${imagePath}` };
|
|
4598
4799
|
const swiftOcr = `
|
|
4599
4800
|
import Foundation
|
|
4600
4801
|
import Vision
|
|
@@ -4616,7 +4817,14 @@ let text = results.compactMap { $0.topCandidates(1).first?.string }.joined(separ
|
|
|
4616
4817
|
print(text)`;
|
|
4617
4818
|
try {
|
|
4618
4819
|
const ocrText = await runSwift(swiftOcr, 3e4);
|
|
4619
|
-
return {
|
|
4820
|
+
return {
|
|
4821
|
+
success: true,
|
|
4822
|
+
data: {
|
|
4823
|
+
text: ocrText.slice(0, 1e4),
|
|
4824
|
+
length: ocrText.length,
|
|
4825
|
+
path: imagePath
|
|
4826
|
+
}
|
|
4827
|
+
};
|
|
4620
4828
|
} catch (err) {
|
|
4621
4829
|
return { success: false, error: `OCR failed: ${err.message}` };
|
|
4622
4830
|
}
|
|
@@ -4640,10 +4848,18 @@ print(text)`;
|
|
|
4640
4848
|
await runAppleScript('tell application "Spotify" to previous track');
|
|
4641
4849
|
return { success: true, data: { action: "previous" } };
|
|
4642
4850
|
case "now_playing": {
|
|
4643
|
-
const name = await runAppleScript(
|
|
4644
|
-
|
|
4645
|
-
|
|
4646
|
-
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
|
+
);
|
|
4647
4863
|
return { success: true, data: { track: name, artist, album, state } };
|
|
4648
4864
|
}
|
|
4649
4865
|
case "search_play": {
|
|
@@ -4651,34 +4867,68 @@ print(text)`;
|
|
|
4651
4867
|
if (!query) return { success: false, error: "Missing search query" };
|
|
4652
4868
|
const result = await this.spotifySearch(query);
|
|
4653
4869
|
if (result) {
|
|
4654
|
-
await runAppleScript(
|
|
4870
|
+
await runAppleScript(
|
|
4871
|
+
`tell application "Spotify" to play track "${result.uri}"`
|
|
4872
|
+
);
|
|
4655
4873
|
await new Promise((r) => setTimeout(r, 1500));
|
|
4656
4874
|
try {
|
|
4657
|
-
const track = await runAppleScript(
|
|
4658
|
-
|
|
4659
|
-
|
|
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
|
+
};
|
|
4660
4889
|
} catch {
|
|
4661
|
-
return {
|
|
4890
|
+
return {
|
|
4891
|
+
success: true,
|
|
4892
|
+
data: {
|
|
4893
|
+
searched: query,
|
|
4894
|
+
resolved: `${result.name} - ${result.artist}`,
|
|
4895
|
+
note: "Playing track"
|
|
4896
|
+
}
|
|
4897
|
+
};
|
|
4662
4898
|
}
|
|
4663
4899
|
}
|
|
4664
4900
|
await runShell2(`open "spotify:search:${encodeURIComponent(query)}"`);
|
|
4665
|
-
return {
|
|
4901
|
+
return {
|
|
4902
|
+
success: true,
|
|
4903
|
+
data: { searched: query, note: "Opened Spotify search." }
|
|
4904
|
+
};
|
|
4666
4905
|
}
|
|
4667
4906
|
case "volume": {
|
|
4668
4907
|
const level = p.level;
|
|
4669
|
-
if (level === void 0 || level < 0 || level > 100)
|
|
4670
|
-
|
|
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
|
+
);
|
|
4671
4913
|
return { success: true, data: { volume: level } };
|
|
4672
4914
|
}
|
|
4673
4915
|
case "shuffle": {
|
|
4674
4916
|
const enabled = p.enabled;
|
|
4675
|
-
await runAppleScript(
|
|
4917
|
+
await runAppleScript(
|
|
4918
|
+
`tell application "Spotify" to set shuffling to ${enabled ? "true" : "false"}`
|
|
4919
|
+
);
|
|
4676
4920
|
return { success: true, data: { shuffling: enabled } };
|
|
4677
4921
|
}
|
|
4678
4922
|
case "repeat": {
|
|
4679
4923
|
const mode = p.mode;
|
|
4680
|
-
if (!mode)
|
|
4681
|
-
|
|
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
|
+
);
|
|
4682
4932
|
return { success: true, data: { repeating: mode } };
|
|
4683
4933
|
}
|
|
4684
4934
|
default:
|
|
@@ -4691,11 +4941,17 @@ print(text)`;
|
|
|
4691
4941
|
async hueLightsOn(light, brightness, color) {
|
|
4692
4942
|
if (!light) return { success: false, error: "Missing light ID or name" };
|
|
4693
4943
|
const hueConfig = this.getHueConfig();
|
|
4694
|
-
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
|
+
};
|
|
4695
4949
|
const lightId = await this.resolveHueLight(hueConfig, light);
|
|
4696
|
-
if (!lightId)
|
|
4950
|
+
if (!lightId)
|
|
4951
|
+
return { success: false, error: `Light '${light}' not found` };
|
|
4697
4952
|
const state = { on: true };
|
|
4698
|
-
if (brightness !== void 0)
|
|
4953
|
+
if (brightness !== void 0)
|
|
4954
|
+
state.bri = Math.max(1, Math.min(254, Number(brightness)));
|
|
4699
4955
|
if (color) {
|
|
4700
4956
|
const rgb = this.parseColor(color);
|
|
4701
4957
|
if (rgb) {
|
|
@@ -4703,46 +4959,91 @@ print(text)`;
|
|
|
4703
4959
|
state.xy = [x, y];
|
|
4704
4960
|
}
|
|
4705
4961
|
}
|
|
4706
|
-
const res = await this.hueRequest(
|
|
4707
|
-
|
|
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
|
+
};
|
|
4708
4972
|
}
|
|
4709
4973
|
async hueLightsOff(light) {
|
|
4710
4974
|
if (!light) return { success: false, error: "Missing light ID or name" };
|
|
4711
4975
|
const hueConfig = this.getHueConfig();
|
|
4712
|
-
if (!hueConfig)
|
|
4976
|
+
if (!hueConfig)
|
|
4977
|
+
return { success: false, error: "Philips Hue not configured." };
|
|
4713
4978
|
const lightId = await this.resolveHueLight(hueConfig, light);
|
|
4714
|
-
if (!lightId)
|
|
4715
|
-
|
|
4716
|
-
|
|
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
|
+
};
|
|
4717
4991
|
}
|
|
4718
4992
|
async hueLightsColor(light, color) {
|
|
4719
|
-
if (!light || !color)
|
|
4993
|
+
if (!light || !color)
|
|
4994
|
+
return { success: false, error: "Missing light or color" };
|
|
4720
4995
|
const hueConfig = this.getHueConfig();
|
|
4721
|
-
if (!hueConfig)
|
|
4996
|
+
if (!hueConfig)
|
|
4997
|
+
return { success: false, error: "Philips Hue not configured." };
|
|
4722
4998
|
const lightId = await this.resolveHueLight(hueConfig, light);
|
|
4723
|
-
if (!lightId)
|
|
4999
|
+
if (!lightId)
|
|
5000
|
+
return { success: false, error: `Light '${light}' not found` };
|
|
4724
5001
|
const rgb = this.parseColor(color);
|
|
4725
5002
|
if (!rgb) return { success: false, error: `Unrecognized color: ${color}` };
|
|
4726
5003
|
const [x, y] = this.rgbToXy(...rgb);
|
|
4727
|
-
const res = await this.hueRequest(
|
|
4728
|
-
|
|
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
|
+
};
|
|
4729
5014
|
}
|
|
4730
5015
|
async hueLightsBrightness(light, brightness) {
|
|
4731
|
-
if (!light || isNaN(brightness))
|
|
5016
|
+
if (!light || isNaN(brightness))
|
|
5017
|
+
return { success: false, error: "Missing light or brightness" };
|
|
4732
5018
|
const hueConfig = this.getHueConfig();
|
|
4733
|
-
if (!hueConfig)
|
|
5019
|
+
if (!hueConfig)
|
|
5020
|
+
return { success: false, error: "Philips Hue not configured." };
|
|
4734
5021
|
const lightId = await this.resolveHueLight(hueConfig, light);
|
|
4735
|
-
if (!lightId)
|
|
5022
|
+
if (!lightId)
|
|
5023
|
+
return { success: false, error: `Light '${light}' not found` };
|
|
4736
5024
|
const bri = Math.max(1, Math.min(254, brightness));
|
|
4737
|
-
const res = await this.hueRequest(
|
|
4738
|
-
|
|
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
|
+
};
|
|
4739
5035
|
}
|
|
4740
5036
|
async hueLightsScene(scene, group) {
|
|
4741
5037
|
if (!scene) return { success: false, error: "Missing scene name" };
|
|
4742
5038
|
const hueConfig = this.getHueConfig();
|
|
4743
|
-
if (!hueConfig)
|
|
5039
|
+
if (!hueConfig)
|
|
5040
|
+
return { success: false, error: "Philips Hue not configured." };
|
|
4744
5041
|
const g = group || "0";
|
|
4745
|
-
const scenes = await this.hueRequest(
|
|
5042
|
+
const scenes = await this.hueRequest(
|
|
5043
|
+
hueConfig,
|
|
5044
|
+
"scenes",
|
|
5045
|
+
"GET"
|
|
5046
|
+
);
|
|
4746
5047
|
let sceneId = null;
|
|
4747
5048
|
for (const [id, s] of Object.entries(scenes)) {
|
|
4748
5049
|
if (s.name?.toLowerCase() === scene.toLowerCase()) {
|
|
@@ -4750,13 +5051,20 @@ print(text)`;
|
|
|
4750
5051
|
break;
|
|
4751
5052
|
}
|
|
4752
5053
|
}
|
|
4753
|
-
if (!sceneId)
|
|
4754
|
-
|
|
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
|
+
});
|
|
4755
5062
|
return { success: true, data: { scene, sceneId, group: g, response: res } };
|
|
4756
5063
|
}
|
|
4757
5064
|
async hueLightsList() {
|
|
4758
5065
|
const hueConfig = this.getHueConfig();
|
|
4759
|
-
if (!hueConfig)
|
|
5066
|
+
if (!hueConfig)
|
|
5067
|
+
return { success: false, error: "Philips Hue not configured." };
|
|
4760
5068
|
const [lights, groups, scenes] = await Promise.all([
|
|
4761
5069
|
this.hueRequest(hueConfig, "lights", "GET"),
|
|
4762
5070
|
this.hueRequest(hueConfig, "groups", "GET"),
|
|
@@ -4765,9 +5073,24 @@ print(text)`;
|
|
|
4765
5073
|
return {
|
|
4766
5074
|
success: true,
|
|
4767
5075
|
data: {
|
|
4768
|
-
lights: Object.entries(lights).map(([id, l]) => ({
|
|
4769
|
-
|
|
4770
|
-
|
|
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
|
+
}))
|
|
4771
5094
|
}
|
|
4772
5095
|
};
|
|
4773
5096
|
}
|
|
@@ -4777,23 +5100,37 @@ print(text)`;
|
|
|
4777
5100
|
async sonosPlay(room) {
|
|
4778
5101
|
if (!room) return { success: false, error: "Missing room name" };
|
|
4779
5102
|
const url = this.getSonosApiUrl();
|
|
4780
|
-
if (!url)
|
|
4781
|
-
|
|
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
|
+
);
|
|
4782
5112
|
return { success: true, data: { room, action: "play", response: res } };
|
|
4783
5113
|
}
|
|
4784
5114
|
async sonosPause(room) {
|
|
4785
5115
|
if (!room) return { success: false, error: "Missing room name" };
|
|
4786
5116
|
const url = this.getSonosApiUrl();
|
|
4787
5117
|
if (!url) return { success: false, error: "Sonos not configured." };
|
|
4788
|
-
const res = await this.sonosRequest(
|
|
5118
|
+
const res = await this.sonosRequest(
|
|
5119
|
+
url,
|
|
5120
|
+
`${encodeURIComponent(room)}/pause`
|
|
5121
|
+
);
|
|
4789
5122
|
return { success: true, data: { room, action: "pause", response: res } };
|
|
4790
5123
|
}
|
|
4791
5124
|
async sonosVolume(room, level) {
|
|
4792
|
-
if (!room || isNaN(level))
|
|
5125
|
+
if (!room || isNaN(level))
|
|
5126
|
+
return { success: false, error: "Missing room or level" };
|
|
4793
5127
|
const url = this.getSonosApiUrl();
|
|
4794
5128
|
if (!url) return { success: false, error: "Sonos not configured." };
|
|
4795
5129
|
const vol = Math.max(0, Math.min(100, level));
|
|
4796
|
-
const res = await this.sonosRequest(
|
|
5130
|
+
const res = await this.sonosRequest(
|
|
5131
|
+
url,
|
|
5132
|
+
`${encodeURIComponent(room)}/volume/${vol}`
|
|
5133
|
+
);
|
|
4797
5134
|
return { success: true, data: { room, volume: vol, response: res } };
|
|
4798
5135
|
}
|
|
4799
5136
|
async sonosPlayUri(room, uri, _title) {
|
|
@@ -4801,10 +5138,19 @@ print(text)`;
|
|
|
4801
5138
|
const url = this.getSonosApiUrl();
|
|
4802
5139
|
if (!url) return { success: false, error: "Sonos not configured." };
|
|
4803
5140
|
if (uri.startsWith("spotify:")) {
|
|
4804
|
-
const res2 = await this.sonosRequest(
|
|
4805
|
-
|
|
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
|
+
};
|
|
4806
5149
|
}
|
|
4807
|
-
const res = await this.sonosRequest(
|
|
5150
|
+
const res = await this.sonosRequest(
|
|
5151
|
+
url,
|
|
5152
|
+
`${encodeURIComponent(room)}/setavtransporturi/${encodeURIComponent(uri)}`
|
|
5153
|
+
);
|
|
4808
5154
|
return { success: true, data: { room, uri, response: res } };
|
|
4809
5155
|
}
|
|
4810
5156
|
async sonosRooms() {
|
|
@@ -4817,21 +5163,30 @@ print(text)`;
|
|
|
4817
5163
|
if (!room) return { success: false, error: "Missing room name" };
|
|
4818
5164
|
const url = this.getSonosApiUrl();
|
|
4819
5165
|
if (!url) return { success: false, error: "Sonos not configured." };
|
|
4820
|
-
const res = await this.sonosRequest(
|
|
5166
|
+
const res = await this.sonosRequest(
|
|
5167
|
+
url,
|
|
5168
|
+
`${encodeURIComponent(room)}/next`
|
|
5169
|
+
);
|
|
4821
5170
|
return { success: true, data: { room, action: "next", response: res } };
|
|
4822
5171
|
}
|
|
4823
5172
|
async sonosPrevious(room) {
|
|
4824
5173
|
if (!room) return { success: false, error: "Missing room name" };
|
|
4825
5174
|
const url = this.getSonosApiUrl();
|
|
4826
5175
|
if (!url) return { success: false, error: "Sonos not configured." };
|
|
4827
|
-
const res = await this.sonosRequest(
|
|
5176
|
+
const res = await this.sonosRequest(
|
|
5177
|
+
url,
|
|
5178
|
+
`${encodeURIComponent(room)}/previous`
|
|
5179
|
+
);
|
|
4828
5180
|
return { success: true, data: { room, action: "previous", response: res } };
|
|
4829
5181
|
}
|
|
4830
5182
|
async sonosNowPlaying(room) {
|
|
4831
5183
|
if (!room) return { success: false, error: "Missing room name" };
|
|
4832
5184
|
const url = this.getSonosApiUrl();
|
|
4833
5185
|
if (!url) return { success: false, error: "Sonos not configured." };
|
|
4834
|
-
const res = await this.sonosRequest(
|
|
5186
|
+
const res = await this.sonosRequest(
|
|
5187
|
+
url,
|
|
5188
|
+
`${encodeURIComponent(room)}/state`
|
|
5189
|
+
);
|
|
4835
5190
|
return { success: true, data: { room, ...res } };
|
|
4836
5191
|
}
|
|
4837
5192
|
/* ══════════════════════════════════════════════════════════
|
|
@@ -4843,9 +5198,12 @@ print(text)`;
|
|
|
4843
5198
|
const cached = this.searchCache.get(key);
|
|
4844
5199
|
if (cached && Date.now() - cached.ts < CACHE_TTL) return cached;
|
|
4845
5200
|
try {
|
|
4846
|
-
const res = await fetch(
|
|
4847
|
-
|
|
4848
|
-
|
|
5201
|
+
const res = await fetch(
|
|
5202
|
+
`${this.apiUrl}/tools/spotify/search?q=${encodeURIComponent(query)}`,
|
|
5203
|
+
{
|
|
5204
|
+
headers: { Authorization: `Bearer ${this.token}` }
|
|
5205
|
+
}
|
|
5206
|
+
);
|
|
4849
5207
|
if (res.ok) {
|
|
4850
5208
|
const data = await res.json();
|
|
4851
5209
|
if (data.uri) {
|
|
@@ -4860,7 +5218,11 @@ print(text)`;
|
|
|
4860
5218
|
if (looksLikeArtist) {
|
|
4861
5219
|
const artistId = await this.searchWebSpotifyIds(query, "artist");
|
|
4862
5220
|
if (artistId) {
|
|
4863
|
-
const result2 = {
|
|
5221
|
+
const result2 = {
|
|
5222
|
+
uri: `spotify:artist:${artistId}`,
|
|
5223
|
+
name: "Top Songs",
|
|
5224
|
+
artist: query
|
|
5225
|
+
};
|
|
4864
5226
|
this.searchCache.set(key, { ...result2, ts: Date.now() });
|
|
4865
5227
|
this.pushToServerCache(query, result2).catch(() => {
|
|
4866
5228
|
});
|
|
@@ -4870,7 +5232,11 @@ print(text)`;
|
|
|
4870
5232
|
const trackIds = await this.searchWebSpotifyIds(query, "track");
|
|
4871
5233
|
if (!trackIds) return null;
|
|
4872
5234
|
const meta = await this.getTrackMetadata(trackIds);
|
|
4873
|
-
const result = {
|
|
5235
|
+
const result = {
|
|
5236
|
+
uri: `spotify:track:${trackIds}`,
|
|
5237
|
+
name: meta?.name ?? query,
|
|
5238
|
+
artist: meta?.artist ?? "Unknown"
|
|
5239
|
+
};
|
|
4874
5240
|
this.searchCache.set(key, { ...result, ts: Date.now() });
|
|
4875
5241
|
this.pushToServerCache(query, result).catch(() => {
|
|
4876
5242
|
});
|
|
@@ -4881,12 +5247,18 @@ print(text)`;
|
|
|
4881
5247
|
`https://search.brave.com/search?q=${encodeURIComponent(`${query} ${type} site:open.spotify.com`)}&source=web`,
|
|
4882
5248
|
`https://html.duckduckgo.com/html/?q=${encodeURIComponent(`${query} ${type} site:open.spotify.com`)}`
|
|
4883
5249
|
];
|
|
4884
|
-
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
|
+
);
|
|
4885
5254
|
for (const url of engines) {
|
|
4886
5255
|
try {
|
|
4887
5256
|
const controller = new AbortController();
|
|
4888
5257
|
const timeout = setTimeout(() => controller.abort(), 8e3);
|
|
4889
|
-
const res = await fetch(url, {
|
|
5258
|
+
const res = await fetch(url, {
|
|
5259
|
+
headers: { "User-Agent": UA, Accept: "text/html" },
|
|
5260
|
+
signal: controller.signal
|
|
5261
|
+
});
|
|
4890
5262
|
clearTimeout(timeout);
|
|
4891
5263
|
if (!res.ok) continue;
|
|
4892
5264
|
const html = await res.text();
|
|
@@ -4900,19 +5272,27 @@ print(text)`;
|
|
|
4900
5272
|
try {
|
|
4901
5273
|
const controller = new AbortController();
|
|
4902
5274
|
const timeout = setTimeout(() => controller.abort(), 5e3);
|
|
4903
|
-
const res = await fetch(
|
|
4904
|
-
|
|
4905
|
-
|
|
4906
|
-
|
|
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
|
+
);
|
|
4907
5282
|
clearTimeout(timeout);
|
|
4908
5283
|
if (!res.ok) return null;
|
|
4909
5284
|
const html = await res.text();
|
|
4910
|
-
const match = html.match(
|
|
5285
|
+
const match = html.match(
|
|
5286
|
+
/<script[^>]*>(\{"props":\{"pageProps".*?\})<\/script>/s
|
|
5287
|
+
);
|
|
4911
5288
|
if (!match) return null;
|
|
4912
5289
|
const data = JSON.parse(match[1]);
|
|
4913
5290
|
const entity = data?.props?.pageProps?.state?.data?.entity;
|
|
4914
5291
|
if (!entity?.name) return null;
|
|
4915
|
-
return {
|
|
5292
|
+
return {
|
|
5293
|
+
name: entity.name,
|
|
5294
|
+
artist: entity.artists?.map((a) => a.name).join(", ") ?? "Unknown"
|
|
5295
|
+
};
|
|
4916
5296
|
} catch {
|
|
4917
5297
|
return null;
|
|
4918
5298
|
}
|
|
@@ -4921,7 +5301,10 @@ print(text)`;
|
|
|
4921
5301
|
try {
|
|
4922
5302
|
await fetch(`${this.apiUrl}/tools/spotify/cache`, {
|
|
4923
5303
|
method: "POST",
|
|
4924
|
-
headers: {
|
|
5304
|
+
headers: {
|
|
5305
|
+
Authorization: `Bearer ${this.token}`,
|
|
5306
|
+
"Content-Type": "application/json"
|
|
5307
|
+
},
|
|
4925
5308
|
body: JSON.stringify({ query, ...result })
|
|
4926
5309
|
});
|
|
4927
5310
|
} catch {
|
|
@@ -4947,21 +5330,31 @@ print(text)`;
|
|
|
4947
5330
|
const controller = new AbortController();
|
|
4948
5331
|
const timeout = setTimeout(() => controller.abort(), 1e4);
|
|
4949
5332
|
try {
|
|
4950
|
-
const opts = {
|
|
5333
|
+
const opts = {
|
|
5334
|
+
method,
|
|
5335
|
+
signal: controller.signal,
|
|
5336
|
+
headers: { "Content-Type": "application/json" }
|
|
5337
|
+
};
|
|
4951
5338
|
if (body && method !== "GET") opts.body = JSON.stringify(body);
|
|
4952
5339
|
const res = await fetch(url, opts);
|
|
4953
5340
|
clearTimeout(timeout);
|
|
4954
5341
|
return await res.json();
|
|
4955
5342
|
} catch (err) {
|
|
4956
5343
|
clearTimeout(timeout);
|
|
4957
|
-
throw new Error(
|
|
5344
|
+
throw new Error(
|
|
5345
|
+
`Hue bridge unreachable at ${config.bridgeIp}: ${err.message}`
|
|
5346
|
+
);
|
|
4958
5347
|
}
|
|
4959
5348
|
}
|
|
4960
5349
|
async resolveHueLight(config, lightRef) {
|
|
4961
5350
|
if (/^\d+$/.test(lightRef)) return lightRef;
|
|
4962
5351
|
if (!this.hueLightCache || Date.now() - this.hueLightCacheTs > 3e5) {
|
|
4963
5352
|
try {
|
|
4964
|
-
const lights = await this.hueRequest(
|
|
5353
|
+
const lights = await this.hueRequest(
|
|
5354
|
+
config,
|
|
5355
|
+
"lights",
|
|
5356
|
+
"GET"
|
|
5357
|
+
);
|
|
4965
5358
|
this.hueLightCache = /* @__PURE__ */ new Map();
|
|
4966
5359
|
for (const [id, light] of Object.entries(lights)) {
|
|
4967
5360
|
this.hueLightCache.set(light.name.toLowerCase(), id);
|
|
@@ -4975,7 +5368,11 @@ print(text)`;
|
|
|
4975
5368
|
}
|
|
4976
5369
|
parseColor(color) {
|
|
4977
5370
|
if (color.startsWith("#") && color.length === 7) {
|
|
4978
|
-
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
|
+
];
|
|
4979
5376
|
}
|
|
4980
5377
|
return CSS_COLORS2[color.toLowerCase()] ?? null;
|
|
4981
5378
|
}
|
|
@@ -5010,7 +5407,9 @@ print(text)`;
|
|
|
5010
5407
|
}
|
|
5011
5408
|
} catch (err) {
|
|
5012
5409
|
clearTimeout(timeout);
|
|
5013
|
-
throw new Error(
|
|
5410
|
+
throw new Error(
|
|
5411
|
+
`Sonos API unreachable at ${baseUrl}: ${err.message}`
|
|
5412
|
+
);
|
|
5014
5413
|
}
|
|
5015
5414
|
}
|
|
5016
5415
|
};
|
|
@@ -5032,13 +5431,7 @@ import { homedir as homedir3, hostname as hostname2, cpus as cpus2, totalmem as
|
|
|
5032
5431
|
import { join as join4, resolve as resolve3, basename as basename3, extname as extname3 } from "path";
|
|
5033
5432
|
var HOME3 = homedir3();
|
|
5034
5433
|
var NOTES_DIR2 = join4(HOME3, "Documents", "PulsoNotes");
|
|
5035
|
-
var SAFE_DIRS3 = [
|
|
5036
|
-
"Documents",
|
|
5037
|
-
"Desktop",
|
|
5038
|
-
"Downloads",
|
|
5039
|
-
"Projects",
|
|
5040
|
-
"Projetos"
|
|
5041
|
-
];
|
|
5434
|
+
var SAFE_DIRS3 = ["Documents", "Desktop", "Downloads", "Projects", "Projetos"];
|
|
5042
5435
|
var ACCESS_LEVEL2 = process.env.PULSO_ACCESS ?? "sandboxed";
|
|
5043
5436
|
function safePath3(relative) {
|
|
5044
5437
|
const full = resolve3(HOME3, relative);
|
|
@@ -5065,10 +5458,14 @@ function runPowerShell(script, timeout = 15e3) {
|
|
|
5065
5458
|
return new Promise((resolve5, reject) => {
|
|
5066
5459
|
const encoded = Buffer.from(script, "utf16le").toString("base64");
|
|
5067
5460
|
const cmd = `powershell -NoProfile -NonInteractive -EncodedCommand ${encoded}`;
|
|
5068
|
-
exec4(
|
|
5069
|
-
|
|
5070
|
-
|
|
5071
|
-
|
|
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
|
+
);
|
|
5072
5469
|
});
|
|
5073
5470
|
}
|
|
5074
5471
|
function runPowerShellScript(script, timeout = 15e3) {
|
|
@@ -5361,7 +5758,10 @@ var WindowsAdapter = class {
|
|
|
5361
5758
|
try {
|
|
5362
5759
|
const parsed = new URL(url);
|
|
5363
5760
|
if (!["http:", "https:"].includes(parsed.protocol)) {
|
|
5364
|
-
return {
|
|
5761
|
+
return {
|
|
5762
|
+
success: false,
|
|
5763
|
+
error: "Only http and https URLs are allowed"
|
|
5764
|
+
};
|
|
5365
5765
|
}
|
|
5366
5766
|
await runPowerShell(`Start-Process "${url}"`);
|
|
5367
5767
|
return { success: true, data: { url, action: "opened" } };
|
|
@@ -5478,7 +5878,10 @@ public class AudioHelper {
|
|
|
5478
5878
|
`;
|
|
5479
5879
|
const output2 = await runPowerShellScript(script, 1e4);
|
|
5480
5880
|
const volume = parseInt(output2, 10);
|
|
5481
|
-
return {
|
|
5881
|
+
return {
|
|
5882
|
+
success: true,
|
|
5883
|
+
data: { volume: isNaN(volume) ? output2 : volume }
|
|
5884
|
+
};
|
|
5482
5885
|
} catch (err) {
|
|
5483
5886
|
try {
|
|
5484
5887
|
const output2 = await runPowerShell(
|
|
@@ -5689,7 +6092,12 @@ Write-Output "$uptimeStr"
|
|
|
5689
6092
|
data: { shortcut: name, method: "power-automate" }
|
|
5690
6093
|
};
|
|
5691
6094
|
} catch {
|
|
5692
|
-
const scriptPath = join4(
|
|
6095
|
+
const scriptPath = join4(
|
|
6096
|
+
HOME3,
|
|
6097
|
+
"Documents",
|
|
6098
|
+
"PulsoScripts",
|
|
6099
|
+
`${name}.ps1`
|
|
6100
|
+
);
|
|
5693
6101
|
if (existsSync3(scriptPath)) {
|
|
5694
6102
|
const inputArg = input2 ? `-InputData '${input2.replace(/'/g, "''")}'` : "";
|
|
5695
6103
|
const output2 = await runPowerShell(
|
|
@@ -5716,9 +6124,7 @@ Write-Output "$uptimeStr"
|
|
|
5716
6124
|
async dnd(enabled) {
|
|
5717
6125
|
try {
|
|
5718
6126
|
if (enabled === void 0 || enabled) {
|
|
5719
|
-
await runPowerShell(
|
|
5720
|
-
`Start-Process "ms-settings:quiethours"`
|
|
5721
|
-
);
|
|
6127
|
+
await runPowerShell(`Start-Process "ms-settings:quiethours"`);
|
|
5722
6128
|
return {
|
|
5723
6129
|
success: true,
|
|
5724
6130
|
data: {
|
|
@@ -6142,7 +6548,11 @@ $chromeProcs | ForEach-Object { Write-Output "Chrome|$($_.MainWindowTitle)|" }
|
|
|
6142
6548
|
if (!output2) return [];
|
|
6143
6549
|
return output2.split("\n").filter(Boolean).map((line) => {
|
|
6144
6550
|
const [browser, title] = line.split("|");
|
|
6145
|
-
return {
|
|
6551
|
+
return {
|
|
6552
|
+
browser: browser || "Unknown",
|
|
6553
|
+
title: title || "",
|
|
6554
|
+
url: ""
|
|
6555
|
+
};
|
|
6146
6556
|
});
|
|
6147
6557
|
} catch {
|
|
6148
6558
|
return [];
|
|
@@ -6239,7 +6649,11 @@ foreach ($w in $windows) {
|
|
|
6239
6649
|
if (output2.includes("executed")) {
|
|
6240
6650
|
return {
|
|
6241
6651
|
success: true,
|
|
6242
|
-
data: {
|
|
6652
|
+
data: {
|
|
6653
|
+
code,
|
|
6654
|
+
method: "ie-com",
|
|
6655
|
+
note: "Executed via IE COM object. For modern browser JS, use Chrome CDP."
|
|
6656
|
+
}
|
|
6243
6657
|
};
|
|
6244
6658
|
}
|
|
6245
6659
|
return {
|
|
@@ -6257,11 +6671,23 @@ foreach ($w in $windows) {
|
|
|
6257
6671
|
try {
|
|
6258
6672
|
const userProfile = process.env.LOCALAPPDATA || process.env.APPDATA || "";
|
|
6259
6673
|
const browserPaths = [
|
|
6260
|
-
{
|
|
6261
|
-
|
|
6262
|
-
|
|
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
|
+
},
|
|
6263
6686
|
{ browser: "Vivaldi", dir: `${userProfile}\\Vivaldi\\User Data` },
|
|
6264
|
-
{
|
|
6687
|
+
{
|
|
6688
|
+
browser: "Opera",
|
|
6689
|
+
dir: `${userProfile}\\Opera Software\\Opera Stable`
|
|
6690
|
+
}
|
|
6265
6691
|
];
|
|
6266
6692
|
const profiles = [];
|
|
6267
6693
|
for (const { browser, dir } of browserPaths) {
|
|
@@ -6410,7 +6836,11 @@ try {
|
|
|
6410
6836
|
const output2 = await runPowerShellScript(script, 15e3);
|
|
6411
6837
|
const reminders = output2.split("\n").filter(Boolean).map((line) => {
|
|
6412
6838
|
const [name, due, listName] = line.split("|");
|
|
6413
|
-
return {
|
|
6839
|
+
return {
|
|
6840
|
+
name: name || "Untitled",
|
|
6841
|
+
due: due || void 0,
|
|
6842
|
+
list: listName || void 0
|
|
6843
|
+
};
|
|
6414
6844
|
});
|
|
6415
6845
|
return { success: true, data: { reminders, count: reminders.length } };
|
|
6416
6846
|
} catch (err) {
|
|
@@ -6447,7 +6877,10 @@ try {
|
|
|
6447
6877
|
data: { title, dueDate, list, method: "outlook-task" }
|
|
6448
6878
|
};
|
|
6449
6879
|
}
|
|
6450
|
-
return {
|
|
6880
|
+
return {
|
|
6881
|
+
success: false,
|
|
6882
|
+
error: "Failed to create reminder. Is Outlook installed?"
|
|
6883
|
+
};
|
|
6451
6884
|
} catch (err) {
|
|
6452
6885
|
return {
|
|
6453
6886
|
success: false,
|
|
@@ -6564,7 +6997,9 @@ try {
|
|
|
6564
6997
|
name: f.replace(/\.(txt|md)$/, ""),
|
|
6565
6998
|
modified: stat.mtime.toISOString()
|
|
6566
6999
|
};
|
|
6567
|
-
}).sort(
|
|
7000
|
+
}).sort(
|
|
7001
|
+
(a, b) => new Date(b.modified).getTime() - new Date(a.modified).getTime()
|
|
7002
|
+
);
|
|
6568
7003
|
const max = limit || 20;
|
|
6569
7004
|
return {
|
|
6570
7005
|
success: true,
|
|
@@ -6617,7 +7052,12 @@ ${body}`;
|
|
|
6617
7052
|
await runPowerShell(`Start-Process "${gmailUrl}"`);
|
|
6618
7053
|
return {
|
|
6619
7054
|
success: true,
|
|
6620
|
-
data: {
|
|
7055
|
+
data: {
|
|
7056
|
+
to,
|
|
7057
|
+
subject,
|
|
7058
|
+
method: "gmail",
|
|
7059
|
+
note: "Gmail compose window opened"
|
|
7060
|
+
}
|
|
6621
7061
|
};
|
|
6622
7062
|
} catch (err) {
|
|
6623
7063
|
return {
|
|
@@ -6773,7 +7213,10 @@ try {
|
|
|
6773
7213
|
const srcPath = safePath3(source);
|
|
6774
7214
|
const dstPath = safePath3(destination);
|
|
6775
7215
|
if (!srcPath || !dstPath) {
|
|
6776
|
-
return {
|
|
7216
|
+
return {
|
|
7217
|
+
success: false,
|
|
7218
|
+
error: "Access denied to source or destination."
|
|
7219
|
+
};
|
|
6777
7220
|
}
|
|
6778
7221
|
if (!existsSync3(srcPath)) {
|
|
6779
7222
|
return { success: false, error: `Source not found: ${source}` };
|
|
@@ -6795,7 +7238,10 @@ try {
|
|
|
6795
7238
|
const srcPath = safePath3(source);
|
|
6796
7239
|
const dstPath = safePath3(destination);
|
|
6797
7240
|
if (!srcPath || !dstPath) {
|
|
6798
|
-
return {
|
|
7241
|
+
return {
|
|
7242
|
+
success: false,
|
|
7243
|
+
error: "Access denied to source or destination."
|
|
7244
|
+
};
|
|
6799
7245
|
}
|
|
6800
7246
|
if (!existsSync3(srcPath)) {
|
|
6801
7247
|
return { success: false, error: `Source not found: ${source}` };
|
|
@@ -7171,12 +7617,19 @@ $input.u.ki.dwFlags = [WinInput]::KEYEVENTF_KEYUP
|
|
|
7171
7617
|
}
|
|
7172
7618
|
return {
|
|
7173
7619
|
success: true,
|
|
7174
|
-
data: {
|
|
7620
|
+
data: {
|
|
7621
|
+
nowPlaying: output2,
|
|
7622
|
+
state: "playing",
|
|
7623
|
+
source: "window_title"
|
|
7624
|
+
}
|
|
7175
7625
|
};
|
|
7176
7626
|
}
|
|
7177
7627
|
return {
|
|
7178
7628
|
success: true,
|
|
7179
|
-
data: {
|
|
7629
|
+
data: {
|
|
7630
|
+
state: "paused_or_idle",
|
|
7631
|
+
note: "Spotify may be paused or not playing"
|
|
7632
|
+
}
|
|
7180
7633
|
};
|
|
7181
7634
|
} catch {
|
|
7182
7635
|
return {
|
|
@@ -7250,7 +7703,10 @@ $input.u.ki.dwFlags = [WinInput]::KEYEVENTF_KEYUP
|
|
|
7250
7703
|
}
|
|
7251
7704
|
const lightId = await resolveHueLight2(config, light);
|
|
7252
7705
|
if (!lightId) {
|
|
7253
|
-
return {
|
|
7706
|
+
return {
|
|
7707
|
+
success: false,
|
|
7708
|
+
error: `Light '${light}' not found on Hue bridge`
|
|
7709
|
+
};
|
|
7254
7710
|
}
|
|
7255
7711
|
const state = { on: true };
|
|
7256
7712
|
if (brightness !== void 0) {
|
|
@@ -7263,7 +7719,12 @@ $input.u.ki.dwFlags = [WinInput]::KEYEVENTF_KEYUP
|
|
|
7263
7719
|
state.xy = [x, y];
|
|
7264
7720
|
}
|
|
7265
7721
|
}
|
|
7266
|
-
const res = await hueRequest2(
|
|
7722
|
+
const res = await hueRequest2(
|
|
7723
|
+
config,
|
|
7724
|
+
`lights/${lightId}/state`,
|
|
7725
|
+
"PUT",
|
|
7726
|
+
state
|
|
7727
|
+
);
|
|
7267
7728
|
return {
|
|
7268
7729
|
success: true,
|
|
7269
7730
|
data: { light: lightId, action: "on", ...state, response: res }
|
|
@@ -7285,7 +7746,9 @@ $input.u.ki.dwFlags = [WinInput]::KEYEVENTF_KEYUP
|
|
|
7285
7746
|
if (!lightId) {
|
|
7286
7747
|
return { success: false, error: `Light '${light}' not found` };
|
|
7287
7748
|
}
|
|
7288
|
-
const res = await hueRequest2(config, `lights/${lightId}/state`, "PUT", {
|
|
7749
|
+
const res = await hueRequest2(config, `lights/${lightId}/state`, "PUT", {
|
|
7750
|
+
on: false
|
|
7751
|
+
});
|
|
7289
7752
|
return {
|
|
7290
7753
|
success: true,
|
|
7291
7754
|
data: { light: lightId, action: "off", response: res }
|
|
@@ -7555,11 +8018,11 @@ $input.u.ki.dwFlags = [WinInput]::KEYEVENTF_KEYUP
|
|
|
7555
8018
|
try {
|
|
7556
8019
|
const url = getSonosApiUrl2();
|
|
7557
8020
|
if (!url) return { success: false, error: "Sonos not configured." };
|
|
7558
|
-
const res = await sonosRequest2(
|
|
7559
|
-
|
|
7560
|
-
|
|
7561
|
-
|
|
7562
|
-
|
|
8021
|
+
const res = await sonosRequest2(url, `${encodeURIComponent(room)}/state`);
|
|
8022
|
+
return {
|
|
8023
|
+
success: true,
|
|
8024
|
+
data: { room, ...res }
|
|
8025
|
+
};
|
|
7563
8026
|
} catch (err) {
|
|
7564
8027
|
return {
|
|
7565
8028
|
success: false,
|
|
@@ -7621,7 +8084,9 @@ async function loadWhisper(model = "base.en") {
|
|
|
7621
8084
|
try {
|
|
7622
8085
|
const sw = require_dist();
|
|
7623
8086
|
const { Whisper, manager: manager2 } = sw;
|
|
7624
|
-
console.log(
|
|
8087
|
+
console.log(
|
|
8088
|
+
` \u{1F3A4} Loading Whisper ${model} STT (downloading ~74-142MB, first use only)...`
|
|
8089
|
+
);
|
|
7625
8090
|
await manager2.download(model, (p) => {
|
|
7626
8091
|
if (p % 25 === 0) process.stdout.write(`\r \u{1F4E5} ${p}% `);
|
|
7627
8092
|
});
|
|
@@ -7634,7 +8099,10 @@ async function loadWhisper(model = "base.en") {
|
|
|
7634
8099
|
} catch (err) {
|
|
7635
8100
|
whisperState = "failed";
|
|
7636
8101
|
const msg = err.message ?? String(err);
|
|
7637
|
-
console.warn(
|
|
8102
|
+
console.warn(
|
|
8103
|
+
" \u2139\uFE0F smart-whisper STT unavailable (optional):",
|
|
8104
|
+
msg.slice(0, 120)
|
|
8105
|
+
);
|
|
7638
8106
|
}
|
|
7639
8107
|
})();
|
|
7640
8108
|
return whisperLoadPromise;
|
|
@@ -7670,7 +8138,10 @@ function decodeWav(buffer) {
|
|
|
7670
8138
|
const channels = buffer.readUInt16LE(fmtOffset + 10);
|
|
7671
8139
|
const sampleRate = buffer.readUInt32LE(fmtOffset + 12);
|
|
7672
8140
|
const bitsPerSample = buffer.readUInt16LE(fmtOffset + 22);
|
|
7673
|
-
if (audioFmt !== 1)
|
|
8141
|
+
if (audioFmt !== 1)
|
|
8142
|
+
throw new Error(
|
|
8143
|
+
`Unsupported WAV format: ${audioFmt} (only PCM=1 supported)`
|
|
8144
|
+
);
|
|
7674
8145
|
let dataOffset = 36;
|
|
7675
8146
|
let dataSize = 0;
|
|
7676
8147
|
while (dataOffset < buffer.length - 8) {
|
|
@@ -7738,7 +8209,10 @@ async function transcribeWithWhisper(pcmOrWav, inputSampleRate, opts) {
|
|
|
7738
8209
|
durationMs: Date.now() - t0
|
|
7739
8210
|
};
|
|
7740
8211
|
} catch (err) {
|
|
7741
|
-
console.warn(
|
|
8212
|
+
console.warn(
|
|
8213
|
+
" \u26A0\uFE0F Whisper transcription failed:",
|
|
8214
|
+
err.message.slice(0, 100)
|
|
8215
|
+
);
|
|
7742
8216
|
return null;
|
|
7743
8217
|
}
|
|
7744
8218
|
}
|
|
@@ -7899,7 +8373,10 @@ async function streamChatResponse(params, hooks, options) {
|
|
|
7899
8373
|
if (options?.signal?.aborted || err?.name === "AbortError") {
|
|
7900
8374
|
return { ok: false, error: "aborted" };
|
|
7901
8375
|
}
|
|
7902
|
-
return {
|
|
8376
|
+
return {
|
|
8377
|
+
ok: false,
|
|
8378
|
+
error: err.message || "stream read failed"
|
|
8379
|
+
};
|
|
7903
8380
|
}
|
|
7904
8381
|
if (chunk.done) break;
|
|
7905
8382
|
buffer += decoder.decode(chunk.value, { stream: true });
|
|
@@ -8170,7 +8647,11 @@ async function runIdeChatTui(params) {
|
|
|
8170
8647
|
const bars = 4 + Math.round(amp * 12);
|
|
8171
8648
|
return `[${"=".repeat(bars).padEnd(16, " ")}]`;
|
|
8172
8649
|
});
|
|
8173
|
-
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
|
+
});
|
|
8174
8655
|
const formatTimestamp = (at) => new Date(at).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
|
|
8175
8656
|
const shortValue = (value, max = 28) => value.length > max ? `${value.slice(0, max - 3)}...` : value;
|
|
8176
8657
|
const pushAssistantNote = (message) => {
|
|
@@ -8206,7 +8687,9 @@ async function runIdeChatTui(params) {
|
|
|
8206
8687
|
if (busy) {
|
|
8207
8688
|
const dots = ".".repeat(pulseTick % 3 + 1);
|
|
8208
8689
|
chunks.push(`\u25CF PULSO ${formatTimestamp(Date.now())}`);
|
|
8209
|
-
chunks.push(
|
|
8690
|
+
chunks.push(
|
|
8691
|
+
...(currentAssistant || `thinking${dots}`).split("\n").map((line) => ` ${line}`)
|
|
8692
|
+
);
|
|
8210
8693
|
chunks.push("");
|
|
8211
8694
|
}
|
|
8212
8695
|
return chunks.join("\n");
|
|
@@ -8220,11 +8703,9 @@ async function runIdeChatTui(params) {
|
|
|
8220
8703
|
const pulseFrame = pulseFrames[pulseTick];
|
|
8221
8704
|
const logoPulse = busy ? "\u25C9" : "\u25CE";
|
|
8222
8705
|
logoBox.setContent(
|
|
8223
|
-
[
|
|
8224
|
-
|
|
8225
|
-
|
|
8226
|
-
` ${IDE_VERSION}`
|
|
8227
|
-
].join("\n")
|
|
8706
|
+
[` ${logoPulse} ${pulseFrame}`, " PULSO", ` ${IDE_VERSION}`].join(
|
|
8707
|
+
"\n"
|
|
8708
|
+
)
|
|
8228
8709
|
);
|
|
8229
8710
|
heroBox.setContent(
|
|
8230
8711
|
[
|
|
@@ -8393,12 +8874,16 @@ async function runIdeChatTui(params) {
|
|
|
8393
8874
|
if (cmd === "theme") {
|
|
8394
8875
|
const normalizedTheme = value.toLowerCase();
|
|
8395
8876
|
if (!normalizedTheme) {
|
|
8396
|
-
pushAssistantNote(
|
|
8397
|
-
|
|
8877
|
+
pushAssistantNote(
|
|
8878
|
+
`Current theme: ${currentThemeName}
|
|
8879
|
+
Available themes: pulso, claude`
|
|
8880
|
+
);
|
|
8398
8881
|
pushTimeline("theme unchanged");
|
|
8399
8882
|
} else if (normalizedTheme !== "pulso" && normalizedTheme !== "claude") {
|
|
8400
|
-
pushAssistantNote(
|
|
8401
|
-
|
|
8883
|
+
pushAssistantNote(
|
|
8884
|
+
`Invalid theme: ${value}
|
|
8885
|
+
Use /theme pulso or /theme claude`
|
|
8886
|
+
);
|
|
8402
8887
|
pushTimeline("invalid theme");
|
|
8403
8888
|
} else {
|
|
8404
8889
|
currentThemeName = normalizedTheme;
|
|
@@ -8424,8 +8909,10 @@ Use /theme pulso or /theme claude`);
|
|
|
8424
8909
|
return "handled";
|
|
8425
8910
|
}
|
|
8426
8911
|
if (normalized.startsWith("/")) {
|
|
8427
|
-
pushAssistantNote(
|
|
8428
|
-
|
|
8912
|
+
pushAssistantNote(
|
|
8913
|
+
`Unknown command: ${normalized}
|
|
8914
|
+
Use /help to list available commands.`
|
|
8915
|
+
);
|
|
8429
8916
|
pushTimeline("unknown command");
|
|
8430
8917
|
render();
|
|
8431
8918
|
return "handled";
|
|
@@ -8435,7 +8922,9 @@ Use /help to list available commands.`);
|
|
|
8435
8922
|
const sendPrompt = async (raw) => {
|
|
8436
8923
|
const prompt = raw.trim();
|
|
8437
8924
|
if (!prompt) return;
|
|
8438
|
-
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
|
+
)) {
|
|
8439
8928
|
const commandResult = handleCommand2(prompt);
|
|
8440
8929
|
if (commandResult === "exit") {
|
|
8441
8930
|
exit();
|
|
@@ -8517,7 +9006,9 @@ Use /help to list available commands.`);
|
|
|
8517
9006
|
at: Date.now(),
|
|
8518
9007
|
content: currentAssistant || "(empty response)"
|
|
8519
9008
|
});
|
|
8520
|
-
pushTimeline(
|
|
9009
|
+
pushTimeline(
|
|
9010
|
+
`usage +$${usageCost.toFixed(4)} \xB7 tok ${usageIn}/${usageOut}`
|
|
9011
|
+
);
|
|
8521
9012
|
}
|
|
8522
9013
|
currentAssistant = "";
|
|
8523
9014
|
render();
|
|
@@ -8621,7 +9112,8 @@ async function runInteractiveChat(params) {
|
|
|
8621
9112
|
const prompt = promptRaw.trim();
|
|
8622
9113
|
const normalized = prompt.toLowerCase();
|
|
8623
9114
|
if (!prompt) continue;
|
|
8624
|
-
if (normalized === "/exit" || normalized === "/quit" || normalized === "exit" || normalized === "quit")
|
|
9115
|
+
if (normalized === "/exit" || normalized === "/quit" || normalized === "exit" || normalized === "quit")
|
|
9116
|
+
break;
|
|
8625
9117
|
if (normalized === "clear" || normalized === "/clear") {
|
|
8626
9118
|
if (output.isTTY) {
|
|
8627
9119
|
output.write("\x1Bc");
|
|
@@ -8686,7 +9178,11 @@ async function runInteractiveChat(params) {
|
|
|
8686
9178
|
}
|
|
8687
9179
|
totalCost += result.costUsd ?? 0;
|
|
8688
9180
|
if (typeof result.costUsd === "number") {
|
|
8689
|
-
console.log(
|
|
9181
|
+
console.log(
|
|
9182
|
+
dimText(
|
|
9183
|
+
` [cost: $${result.costUsd.toFixed(4)} | total: $${totalCost.toFixed(4)}]`
|
|
9184
|
+
)
|
|
9185
|
+
);
|
|
8690
9186
|
console.log("");
|
|
8691
9187
|
} else {
|
|
8692
9188
|
console.log("");
|
|
@@ -8714,12 +9210,7 @@ async function deviceLogin(apiUrl) {
|
|
|
8714
9210
|
console.error(` Failed to request device code: ${err}`);
|
|
8715
9211
|
return null;
|
|
8716
9212
|
}
|
|
8717
|
-
const {
|
|
8718
|
-
device_code,
|
|
8719
|
-
user_code,
|
|
8720
|
-
verification_url,
|
|
8721
|
-
interval
|
|
8722
|
-
} = await codeRes.json();
|
|
9213
|
+
const { device_code, user_code, verification_url, interval } = await codeRes.json();
|
|
8723
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");
|
|
8724
9215
|
console.log(" \u2502 \u2502");
|
|
8725
9216
|
console.log(" \u2502 Open this URL in your browser: \u2502");
|
|
@@ -8800,9 +9291,15 @@ if (helpRequested || subCommand === "help") {
|
|
|
8800
9291
|
console.log(" Pulso Companion Commands");
|
|
8801
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");
|
|
8802
9293
|
console.log(` ${COMMAND_LOGIN.padEnd(26)} Authenticate device`);
|
|
8803
|
-
console.log(
|
|
8804
|
-
|
|
8805
|
-
|
|
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
|
+
);
|
|
8806
9303
|
console.log(` ${`${COMMAND_CHAT} --plain`.padEnd(26)} Plain terminal chat`);
|
|
8807
9304
|
console.log(` ${COMMAND_IDE.padEnd(26)} Full-screen terminal IDE`);
|
|
8808
9305
|
console.log(` ${COMMAND_DAEMON.padEnd(26)} Start companion daemon`);
|
|
@@ -8858,7 +9355,9 @@ if (subCommand === "logout") {
|
|
|
8858
9355
|
if (res.ok) {
|
|
8859
9356
|
console.log("\n Token revoked on server.");
|
|
8860
9357
|
} else {
|
|
8861
|
-
console.log(
|
|
9358
|
+
console.log(
|
|
9359
|
+
"\n Warning: Could not revoke token on server (may already be expired)."
|
|
9360
|
+
);
|
|
8862
9361
|
}
|
|
8863
9362
|
} catch {
|
|
8864
9363
|
console.log("\n Warning: Could not reach server to revoke token.");
|
|
@@ -8925,8 +9424,10 @@ if (!TOKEN) {
|
|
|
8925
9424
|
console.log(" No token found. Starting browser device login...\n");
|
|
8926
9425
|
const creds = await deviceLogin(API_URL);
|
|
8927
9426
|
if (!creds?.token) {
|
|
8928
|
-
console.error(
|
|
8929
|
-
`
|
|
9427
|
+
console.error(
|
|
9428
|
+
` Login failed. Run '${COMMAND_LOGIN}' and approve in browser.
|
|
9429
|
+
`
|
|
9430
|
+
);
|
|
8930
9431
|
process.exit(1);
|
|
8931
9432
|
}
|
|
8932
9433
|
TOKEN = creds.token;
|
|
@@ -8946,7 +9447,9 @@ var WAKE_WORD_SENSITIVITY_RAW = process.env.PULSO_WAKE_WORD_SENSITIVITY ?? proce
|
|
|
8946
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") ?? "";
|
|
8947
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") ?? "";
|
|
8948
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") ?? "";
|
|
8949
|
-
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
|
+
) ?? "";
|
|
8950
9453
|
var WS_BASE = API_URL.replace("https://", "wss://").replace("http://", "ws://") + "/ws/companion";
|
|
8951
9454
|
var HOME4 = homedir4();
|
|
8952
9455
|
var RECONNECT_DELAY = 5e3;
|
|
@@ -8976,9 +9479,15 @@ function acquireCompanionLock() {
|
|
|
8976
9479
|
try {
|
|
8977
9480
|
process.kill(existingPid, 0);
|
|
8978
9481
|
console.log("");
|
|
8979
|
-
console.log(
|
|
8980
|
-
|
|
8981
|
-
|
|
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
|
+
);
|
|
8982
9491
|
process.exit(0);
|
|
8983
9492
|
} catch {
|
|
8984
9493
|
}
|
|
@@ -9038,27 +9547,27 @@ function runAppleScript2(script) {
|
|
|
9038
9547
|
return new Promise((resolve5, reject) => {
|
|
9039
9548
|
const tmpPath = `/tmp/pulso-as-${Date.now()}-${Math.random().toString(36).slice(2, 6)}.scpt`;
|
|
9040
9549
|
writeFileSync5(tmpPath, script, "utf-8");
|
|
9041
|
-
exec5(
|
|
9042
|
-
|
|
9043
|
-
|
|
9044
|
-
|
|
9045
|
-
try {
|
|
9046
|
-
unlinkSync5(tmpPath);
|
|
9047
|
-
} catch {
|
|
9048
|
-
}
|
|
9049
|
-
if (err) reject(new Error(stderr || err.message));
|
|
9050
|
-
else resolve5(stdout.trim());
|
|
9550
|
+
exec5(`osascript ${tmpPath}`, { timeout: 15e3 }, (err, stdout, stderr) => {
|
|
9551
|
+
try {
|
|
9552
|
+
unlinkSync5(tmpPath);
|
|
9553
|
+
} catch {
|
|
9051
9554
|
}
|
|
9052
|
-
|
|
9555
|
+
if (err) reject(new Error(stderr || err.message));
|
|
9556
|
+
else resolve5(stdout.trim());
|
|
9557
|
+
});
|
|
9053
9558
|
});
|
|
9054
9559
|
}
|
|
9055
9560
|
function runShell4(cmd, timeout = 1e4) {
|
|
9056
9561
|
return new Promise((resolve5, reject) => {
|
|
9057
9562
|
const shell = process.env.SHELL || "/bin/zsh";
|
|
9058
|
-
exec5(
|
|
9059
|
-
|
|
9060
|
-
|
|
9061
|
-
|
|
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
|
+
);
|
|
9062
9571
|
});
|
|
9063
9572
|
}
|
|
9064
9573
|
function augmentedPath2() {
|
|
@@ -9093,11 +9602,14 @@ function runSwift2(code, timeout = 1e4) {
|
|
|
9093
9602
|
}
|
|
9094
9603
|
async function hasScreenRecordingPermission2() {
|
|
9095
9604
|
try {
|
|
9096
|
-
const out = await runSwift2(
|
|
9605
|
+
const out = await runSwift2(
|
|
9606
|
+
`
|
|
9097
9607
|
import Cocoa
|
|
9098
9608
|
import CoreGraphics
|
|
9099
9609
|
print(CGPreflightScreenCaptureAccess() ? "granted" : "denied")
|
|
9100
|
-
`,
|
|
9610
|
+
`,
|
|
9611
|
+
6e3
|
|
9612
|
+
);
|
|
9101
9613
|
return out.trim().toLowerCase() === "granted";
|
|
9102
9614
|
} catch {
|
|
9103
9615
|
return false;
|
|
@@ -9210,7 +9722,10 @@ var ADAPTER_COMMANDS = {
|
|
|
9210
9722
|
const buttonName = p.button;
|
|
9211
9723
|
const procName = p.procName ?? activeDialog?.procName;
|
|
9212
9724
|
if (!procName || !buttonName) {
|
|
9213
|
-
return {
|
|
9725
|
+
return {
|
|
9726
|
+
success: false,
|
|
9727
|
+
error: "No active dialog or button not specified"
|
|
9728
|
+
};
|
|
9214
9729
|
}
|
|
9215
9730
|
const script = `
|
|
9216
9731
|
tell application "System Events"
|
|
@@ -9231,7 +9746,9 @@ tell application "System Events"
|
|
|
9231
9746
|
end tell`;
|
|
9232
9747
|
try {
|
|
9233
9748
|
await runAppleScript2(script);
|
|
9234
|
-
console.log(
|
|
9749
|
+
console.log(
|
|
9750
|
+
` \u2713 Dialog action: clicked "${buttonName}" in [${procName}]`
|
|
9751
|
+
);
|
|
9235
9752
|
activeDialog = null;
|
|
9236
9753
|
lastDialogSignature = null;
|
|
9237
9754
|
return { success: true, clicked: buttonName };
|
|
@@ -9242,9 +9759,18 @@ end tell`;
|
|
|
9242
9759
|
sys_clipboard_read: (a, _) => a.clipboardRead(),
|
|
9243
9760
|
sys_clipboard_write: (a, p) => a.clipboardWrite(p.text),
|
|
9244
9761
|
sys_screenshot: (a, _) => a.screenshot(),
|
|
9245
|
-
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
|
+
),
|
|
9246
9767
|
sys_mouse_double_click: (a, p) => a.mouseDoubleClick(Number(p.x), Number(p.y)),
|
|
9247
|
-
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
|
+
),
|
|
9248
9774
|
sys_mouse_move: (a, p) => a.mouseMove(Number(p.x), Number(p.y)),
|
|
9249
9775
|
sys_drag: (a, p) => a.drag(Number(p.fromX), Number(p.fromY), Number(p.toX), Number(p.toY)),
|
|
9250
9776
|
sys_get_cursor_position: (a, _) => a.getCursorPosition(),
|
|
@@ -9257,7 +9783,10 @@ end tell`;
|
|
|
9257
9783
|
sys_browser_list_tabs: (a, _) => a.browserListTabs(),
|
|
9258
9784
|
sys_browser_navigate: (a, p) => a.browserNavigate(p.url, p.browser),
|
|
9259
9785
|
sys_browser_new_tab: (a, p) => a.browserNewTab(p.url, p.browser),
|
|
9260
|
-
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
|
+
),
|
|
9261
9790
|
sys_browser_execute_js: (a, p) => a.browserExecuteJs(p.code, p.browser),
|
|
9262
9791
|
sys_browser_list_profiles: (a, _) => a.browserListProfiles(),
|
|
9263
9792
|
sys_calendar_list: (a, p) => a.calendarList(Number(p.days) || void 0),
|
|
@@ -9277,26 +9806,61 @@ end tell`;
|
|
|
9277
9806
|
sys_imessage_send: (a, p) => a.sendMessage(p.to, p.message),
|
|
9278
9807
|
sys_system_info: (a, _) => a.getSystemInfo(),
|
|
9279
9808
|
sys_dnd: (a, p) => a.dnd(p.enabled),
|
|
9280
|
-
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
|
+
),
|
|
9281
9814
|
sys_run_shortcut: (a, p) => a.runShortcut(p.name, p.input),
|
|
9282
9815
|
sys_file_read: (a, p) => a.fileRead(p.path),
|
|
9283
9816
|
sys_file_write: (a, p) => a.fileWrite(p.path, p.content),
|
|
9284
|
-
sys_file_list: (a, p) => a.fileList(
|
|
9285
|
-
|
|
9286
|
-
|
|
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
|
+
),
|
|
9287
9829
|
sys_file_delete: (a, p) => a.fileDelete(p.path),
|
|
9288
9830
|
sys_file_info: (a, p) => a.fileInfo(p.path),
|
|
9289
|
-
sys_download: (a, p) => a.download(
|
|
9831
|
+
sys_download: (a, p) => a.download(
|
|
9832
|
+
p.url,
|
|
9833
|
+
p.destination ?? p.path ?? void 0
|
|
9834
|
+
),
|
|
9290
9835
|
sys_window_list: (a, _) => a.windowList(),
|
|
9291
9836
|
sys_window_focus: (a, p) => a.windowFocus(p.app),
|
|
9292
|
-
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
|
+
),
|
|
9293
9844
|
sys_notes_list: (a, p) => a.notesList(Number(p.limit) || void 0),
|
|
9294
|
-
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
|
+
),
|
|
9295
9850
|
sys_contacts_search: (a, p) => a.contactsSearch(p.query),
|
|
9296
9851
|
sys_ocr: (a, p) => a.ocr(p.imagePath ?? p.path),
|
|
9297
|
-
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
|
+
),
|
|
9298
9858
|
sys_spotify: (a, p) => a.spotify(p.action, p),
|
|
9299
|
-
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
|
+
),
|
|
9300
9864
|
sys_hue_lights_off: (a, p) => a.hueLightsOff(p.light),
|
|
9301
9865
|
sys_hue_lights_color: (a, p) => a.hueLightsColor(p.light, p.color),
|
|
9302
9866
|
sys_hue_lights_brightness: (a, p) => a.hueLightsBrightness(p.light, Number(p.brightness)),
|
|
@@ -9305,7 +9869,11 @@ end tell`;
|
|
|
9305
9869
|
sys_sonos_play: (a, p) => a.sonosPlay(p.room),
|
|
9306
9870
|
sys_sonos_pause: (a, p) => a.sonosPause(p.room),
|
|
9307
9871
|
sys_sonos_volume: (a, p) => a.sonosVolume(p.room, Number(p.level)),
|
|
9308
|
-
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
|
+
),
|
|
9309
9877
|
sys_sonos_rooms: (a, _) => a.sonosRooms(),
|
|
9310
9878
|
sys_sonos_next: (a, p) => a.sonosNext(p.room),
|
|
9311
9879
|
sys_sonos_previous: (a, p) => a.sonosPrevious(p.room),
|
|
@@ -9340,7 +9908,10 @@ async function handleCommand(command, params, streamCb) {
|
|
|
9340
9908
|
});
|
|
9341
9909
|
clearTimeout(timeout);
|
|
9342
9910
|
if (!res.ok) {
|
|
9343
|
-
return {
|
|
9911
|
+
return {
|
|
9912
|
+
success: true,
|
|
9913
|
+
data: { running: false, error: `HTTP ${res.status}` }
|
|
9914
|
+
};
|
|
9344
9915
|
}
|
|
9345
9916
|
const data = await res.json();
|
|
9346
9917
|
const models = (data.models ?? []).map((m) => ({
|
|
@@ -9351,7 +9922,12 @@ async function handleCommand(command, params, streamCb) {
|
|
|
9351
9922
|
}));
|
|
9352
9923
|
return {
|
|
9353
9924
|
success: true,
|
|
9354
|
-
data: {
|
|
9925
|
+
data: {
|
|
9926
|
+
running: true,
|
|
9927
|
+
url: "http://localhost:11434",
|
|
9928
|
+
modelCount: models.length,
|
|
9929
|
+
models
|
|
9930
|
+
}
|
|
9355
9931
|
};
|
|
9356
9932
|
} catch {
|
|
9357
9933
|
return { success: true, data: { running: false } };
|
|
@@ -9366,7 +9942,10 @@ async function handleCommand(command, params, streamCb) {
|
|
|
9366
9942
|
return { success: true, data: result };
|
|
9367
9943
|
}
|
|
9368
9944
|
if (adapter.platform !== "macos") {
|
|
9369
|
-
return {
|
|
9945
|
+
return {
|
|
9946
|
+
success: false,
|
|
9947
|
+
error: `Command ${command} is not available on ${adapter.platform}. It is currently macOS-only.`
|
|
9948
|
+
};
|
|
9370
9949
|
}
|
|
9371
9950
|
switch (command) {
|
|
9372
9951
|
// ── macOS-only commands (not in the cross-platform adapter interface) ──
|
|
@@ -9389,21 +9968,42 @@ async function handleCommand(command, params, streamCb) {
|
|
|
9389
9968
|
const regPath = `/tmp/pulso-ss-region-${ts2}.png`;
|
|
9390
9969
|
const regJpg = `/tmp/pulso-ss-region-${ts2}.jpg`;
|
|
9391
9970
|
try {
|
|
9392
|
-
await runShell4(
|
|
9971
|
+
await runShell4(
|
|
9972
|
+
`screencapture -x -R${rx},${ry},${rw},${rh} ${regPath}`,
|
|
9973
|
+
15e3
|
|
9974
|
+
);
|
|
9393
9975
|
} catch (e) {
|
|
9394
|
-
return {
|
|
9976
|
+
return {
|
|
9977
|
+
success: false,
|
|
9978
|
+
error: `Region screenshot failed: ${e.message}`,
|
|
9979
|
+
errorCode: "SCREENSHOT_FAILED"
|
|
9980
|
+
};
|
|
9395
9981
|
}
|
|
9396
9982
|
if (!existsSync4(regPath))
|
|
9397
|
-
return {
|
|
9983
|
+
return {
|
|
9984
|
+
success: false,
|
|
9985
|
+
error: "Region screenshot failed",
|
|
9986
|
+
errorCode: "SCREENSHOT_FAILED"
|
|
9987
|
+
};
|
|
9398
9988
|
try {
|
|
9399
|
-
await runShell4(
|
|
9989
|
+
await runShell4(
|
|
9990
|
+
`sips --setProperty format jpeg --setProperty formatOptions 85 ${regPath} --out ${regJpg}`,
|
|
9991
|
+
1e4
|
|
9992
|
+
);
|
|
9400
9993
|
} catch {
|
|
9401
9994
|
const rb = readFileSync4(regPath);
|
|
9402
9995
|
try {
|
|
9403
9996
|
unlinkSync5(regPath);
|
|
9404
9997
|
} catch {
|
|
9405
9998
|
}
|
|
9406
|
-
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
|
+
};
|
|
9407
10007
|
}
|
|
9408
10008
|
const rb2 = readFileSync4(regJpg);
|
|
9409
10009
|
try {
|
|
@@ -9441,7 +10041,16 @@ for (i, screen) in screens.enumerated() {
|
|
|
9441
10041
|
print(result)`;
|
|
9442
10042
|
const raw = await runSwift2(swift, 15e3);
|
|
9443
10043
|
const displays = raw.trim().split("\n").filter(Boolean).map((line) => {
|
|
9444
|
-
const [
|
|
10044
|
+
const [
|
|
10045
|
+
index,
|
|
10046
|
+
name,
|
|
10047
|
+
origin,
|
|
10048
|
+
size,
|
|
10049
|
+
visOrigin,
|
|
10050
|
+
visSize,
|
|
10051
|
+
scale,
|
|
10052
|
+
isMain
|
|
10053
|
+
] = line.split("|");
|
|
9445
10054
|
const [ox, oy] = (origin || "0,0").split(",").map(Number);
|
|
9446
10055
|
const [sw2, sh2] = (size || "0,0").split(",").map(Number);
|
|
9447
10056
|
const [vox, voy] = (visOrigin || "0,0").split(",").map(Number);
|
|
@@ -9470,7 +10079,10 @@ print(result)`;
|
|
|
9470
10079
|
}
|
|
9471
10080
|
};
|
|
9472
10081
|
} catch (e) {
|
|
9473
|
-
return {
|
|
10082
|
+
return {
|
|
10083
|
+
success: false,
|
|
10084
|
+
error: `Failed to list displays: ${e.message}`
|
|
10085
|
+
};
|
|
9474
10086
|
}
|
|
9475
10087
|
}
|
|
9476
10088
|
// ── NEW: System Settings ──────────────────────────────
|
|
@@ -9508,7 +10120,9 @@ print(result)`;
|
|
|
9508
10120
|
`open "x-apple.systempreferences:com.apple.settings.${pane}" 2>/dev/null || open "x-apple.systempreferences:${paneId}" 2>/dev/null || open "System Preferences"`
|
|
9509
10121
|
);
|
|
9510
10122
|
} else {
|
|
9511
|
-
await runShell4(
|
|
10123
|
+
await runShell4(
|
|
10124
|
+
`open "System Preferences" 2>/dev/null || open -a "System Settings"`
|
|
10125
|
+
);
|
|
9512
10126
|
}
|
|
9513
10127
|
return { success: true, data: { pane: pane || "main" } };
|
|
9514
10128
|
}
|
|
@@ -9706,9 +10320,7 @@ print(result)`;
|
|
|
9706
10320
|
const device = params.device;
|
|
9707
10321
|
if (!device) return { success: false, error: "Missing device name" };
|
|
9708
10322
|
try {
|
|
9709
|
-
await runShell4(
|
|
9710
|
-
`SwitchAudioSource -s "${device}" 2>/dev/null`
|
|
9711
|
-
);
|
|
10323
|
+
await runShell4(`SwitchAudioSource -s "${device}" 2>/dev/null`);
|
|
9712
10324
|
return { success: true, data: { switched: device } };
|
|
9713
10325
|
} catch {
|
|
9714
10326
|
return {
|
|
@@ -9730,12 +10342,8 @@ print(result)`;
|
|
|
9730
10342
|
case "sys_trash": {
|
|
9731
10343
|
const trashAction = params.action || "info";
|
|
9732
10344
|
if (trashAction === "info") {
|
|
9733
|
-
const count = await runShell4(
|
|
9734
|
-
|
|
9735
|
-
);
|
|
9736
|
-
const size = await runShell4(
|
|
9737
|
-
`du -sh ~/.Trash 2>/dev/null | cut -f1`
|
|
9738
|
-
);
|
|
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`);
|
|
9739
10347
|
return {
|
|
9740
10348
|
success: true,
|
|
9741
10349
|
data: {
|
|
@@ -9744,9 +10352,7 @@ print(result)`;
|
|
|
9744
10352
|
}
|
|
9745
10353
|
};
|
|
9746
10354
|
} else if (trashAction === "empty") {
|
|
9747
|
-
await runAppleScript2(
|
|
9748
|
-
`tell application "Finder" to empty the trash`
|
|
9749
|
-
);
|
|
10355
|
+
await runAppleScript2(`tell application "Finder" to empty the trash`);
|
|
9750
10356
|
return { success: true, data: { emptied: true } };
|
|
9751
10357
|
}
|
|
9752
10358
|
return { success: false, error: "Use action: info or empty" };
|
|
@@ -9778,9 +10384,7 @@ print(result)`;
|
|
|
9778
10384
|
case "sys_disk_info": {
|
|
9779
10385
|
const df = await runShell4(`df -h / | tail -1`);
|
|
9780
10386
|
const parts = df.trim().split(/\s+/);
|
|
9781
|
-
const volumes = await runShell4(
|
|
9782
|
-
`ls -1 /Volumes 2>/dev/null`
|
|
9783
|
-
);
|
|
10387
|
+
const volumes = await runShell4(`ls -1 /Volumes 2>/dev/null`);
|
|
9784
10388
|
return {
|
|
9785
10389
|
success: true,
|
|
9786
10390
|
data: {
|
|
@@ -9810,9 +10414,7 @@ print(result)`;
|
|
|
9810
10414
|
await runShell4(`shortcuts run "Set ${mode}" 2>/dev/null`);
|
|
9811
10415
|
return { success: true, data: { mode } };
|
|
9812
10416
|
} catch {
|
|
9813
|
-
await runShell4(
|
|
9814
|
-
`shortcuts run "Toggle Do Not Disturb" 2>/dev/null`
|
|
9815
|
-
);
|
|
10417
|
+
await runShell4(`shortcuts run "Toggle Do Not Disturb" 2>/dev/null`);
|
|
9816
10418
|
return {
|
|
9817
10419
|
success: true,
|
|
9818
10420
|
data: {
|
|
@@ -9877,13 +10479,18 @@ print(result)`;
|
|
|
9877
10479
|
await runShell4(`pmset sleepnow`);
|
|
9878
10480
|
return { success: true, data: { sleeping: true } };
|
|
9879
10481
|
}
|
|
9880
|
-
return {
|
|
10482
|
+
return {
|
|
10483
|
+
success: false,
|
|
10484
|
+
error: "Use action: status, caffeinate, sleep"
|
|
10485
|
+
};
|
|
9881
10486
|
}
|
|
9882
10487
|
// ── NEW: Printer Management ─────────────────────────────
|
|
9883
10488
|
case "sys_printer": {
|
|
9884
10489
|
const prAction = params.action || "list";
|
|
9885
10490
|
if (prAction === "list") {
|
|
9886
|
-
const result = await runShell4(
|
|
10491
|
+
const result = await runShell4(
|
|
10492
|
+
`lpstat -p 2>/dev/null || echo "No printers"`
|
|
10493
|
+
);
|
|
9887
10494
|
return { success: true, data: { printers: result.trim() } };
|
|
9888
10495
|
} else if (prAction === "print") {
|
|
9889
10496
|
const file = params.file;
|
|
@@ -10016,7 +10623,9 @@ print(result.stdout[:5000])
|
|
|
10016
10623
|
} else if (svcAction === "start") {
|
|
10017
10624
|
const name = params.name;
|
|
10018
10625
|
if (!name) return { success: false, error: "Missing service name" };
|
|
10019
|
-
await runShell4(
|
|
10626
|
+
await runShell4(
|
|
10627
|
+
`launchctl kickstart gui/$(id -u)/${name} 2>/dev/null`
|
|
10628
|
+
);
|
|
10020
10629
|
return { success: true, data: { started: name } };
|
|
10021
10630
|
} else if (svcAction === "stop") {
|
|
10022
10631
|
const name = params.name;
|
|
@@ -10099,7 +10708,9 @@ print(result.stdout[:5000])
|
|
|
10099
10708
|
return;
|
|
10100
10709
|
}
|
|
10101
10710
|
if (evt?.type === "assistant" && Array.isArray(evt?.message?.content)) {
|
|
10102
|
-
const textBlocks = evt.message.content.filter(
|
|
10711
|
+
const textBlocks = evt.message.content.filter(
|
|
10712
|
+
(p) => p?.type === "text" && typeof p?.text === "string"
|
|
10713
|
+
).map((p) => p.text);
|
|
10103
10714
|
if (textBlocks.length > 0) {
|
|
10104
10715
|
assistantSnapshotText = textBlocks.join("");
|
|
10105
10716
|
}
|
|
@@ -10258,12 +10869,18 @@ print(result.stdout[:5000])
|
|
|
10258
10869
|
const version = await runShell4("codex --version 2>/dev/null", 5e3);
|
|
10259
10870
|
let authStatus = "unknown";
|
|
10260
10871
|
try {
|
|
10261
|
-
const status = await runShell4(
|
|
10872
|
+
const status = await runShell4(
|
|
10873
|
+
"codex auth whoami 2>&1 || codex --help 2>&1 | head -5",
|
|
10874
|
+
1e4
|
|
10875
|
+
);
|
|
10262
10876
|
const lc = status.toLowerCase();
|
|
10263
10877
|
authStatus = lc.includes("not logged in") || lc.includes("not authenticated") || lc.includes("sign in") || lc.includes("no api key") ? "not_authenticated" : "authenticated";
|
|
10264
10878
|
} catch {
|
|
10265
10879
|
try {
|
|
10266
|
-
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
|
+
);
|
|
10267
10884
|
authStatus = "authenticated";
|
|
10268
10885
|
} catch {
|
|
10269
10886
|
authStatus = "not_authenticated";
|
|
@@ -10340,11 +10957,304 @@ print(result.stdout[:5000])
|
|
|
10340
10957
|
const testText = params.text || "Hello! This is Pulso voice test. Kokoro TTS is working correctly.";
|
|
10341
10958
|
try {
|
|
10342
10959
|
await speak(testText, { engine: "auto" });
|
|
10343
|
-
return {
|
|
10960
|
+
return {
|
|
10961
|
+
success: true,
|
|
10962
|
+
data: { spoken: testText, engine: getTTSInfo().engine }
|
|
10963
|
+
};
|
|
10344
10964
|
} catch (err) {
|
|
10345
10965
|
return { success: false, error: err.message };
|
|
10346
10966
|
}
|
|
10347
10967
|
}
|
|
10968
|
+
// ── IDE Integration ────────────────────────────────────
|
|
10969
|
+
// Helper: extract open workspace paths from a VS Code/Cursor/Windsurf storage.json
|
|
10970
|
+
// Storage format: windowsState.lastActiveWindow / openedWindows
|
|
10971
|
+
// Each window has workspaceIdentifier.configURIPath (workspace file) OR folderUri (folder)
|
|
10972
|
+
case "sys_ide_list_open": {
|
|
10973
|
+
let readIdeWorkspaces2 = function(storagePath) {
|
|
10974
|
+
const storage = JSON.parse(readFileSync4(storagePath, "utf-8"));
|
|
10975
|
+
const ws2 = storage["windowsState"] ?? {};
|
|
10976
|
+
const allWindows = [
|
|
10977
|
+
ws2["lastActiveWindow"],
|
|
10978
|
+
...Array.isArray(ws2["openedWindows"]) ? ws2["openedWindows"] : []
|
|
10979
|
+
].filter(Boolean);
|
|
10980
|
+
const seen = /* @__PURE__ */ new Set();
|
|
10981
|
+
const paths = [];
|
|
10982
|
+
for (const w of allWindows) {
|
|
10983
|
+
const configURI = w["workspaceIdentifier"]?.["configURIPath"] ?? "";
|
|
10984
|
+
const folderURI = w["folderUri"] ?? "";
|
|
10985
|
+
for (const uri of [configURI, folderURI]) {
|
|
10986
|
+
if (!uri) continue;
|
|
10987
|
+
let p = uri.replace(/^file:\/\//, "");
|
|
10988
|
+
if (p.endsWith(".code-workspace")) p = dirname(p);
|
|
10989
|
+
if (p && !seen.has(p)) {
|
|
10990
|
+
seen.add(p);
|
|
10991
|
+
paths.push(p);
|
|
10992
|
+
}
|
|
10993
|
+
}
|
|
10994
|
+
}
|
|
10995
|
+
const activePath = (() => {
|
|
10996
|
+
const aw = ws2["lastActiveWindow"];
|
|
10997
|
+
if (!aw) return null;
|
|
10998
|
+
const configURI = aw["workspaceIdentifier"]?.["configURIPath"] ?? "";
|
|
10999
|
+
const folderURI = aw["folderUri"] ?? "";
|
|
11000
|
+
const raw = configURI || folderURI;
|
|
11001
|
+
if (!raw) return null;
|
|
11002
|
+
let p = raw.replace(/^file:\/\//, "");
|
|
11003
|
+
if (p.endsWith(".code-workspace")) p = dirname(p);
|
|
11004
|
+
return p || null;
|
|
11005
|
+
})();
|
|
11006
|
+
return { active: activePath, all: paths };
|
|
11007
|
+
};
|
|
11008
|
+
var readIdeWorkspaces = readIdeWorkspaces2;
|
|
11009
|
+
return new Promise((resolve5) => {
|
|
11010
|
+
exec5("ps aux", { timeout: 5e3 }, (err, stdout) => {
|
|
11011
|
+
if (err) {
|
|
11012
|
+
resolve5({ success: false, error: err.message });
|
|
11013
|
+
return;
|
|
11014
|
+
}
|
|
11015
|
+
const IDE_PATTERNS = {
|
|
11016
|
+
"Cursor Helper": "Cursor",
|
|
11017
|
+
"Cursor.app": "Cursor",
|
|
11018
|
+
"Code Helper": "VS Code",
|
|
11019
|
+
"Visual Studio Code": "VS Code",
|
|
11020
|
+
"Windsurf Helper": "Windsurf",
|
|
11021
|
+
"Windsurf.app": "Windsurf",
|
|
11022
|
+
"zed": "Zed",
|
|
11023
|
+
"WebStorm": "WebStorm",
|
|
11024
|
+
"IntelliJ IDEA": "IntelliJ IDEA",
|
|
11025
|
+
"PyCharm": "PyCharm",
|
|
11026
|
+
"GoLand": "GoLand"
|
|
11027
|
+
};
|
|
11028
|
+
const running = /* @__PURE__ */ new Set();
|
|
11029
|
+
for (const line of stdout.split("\n")) {
|
|
11030
|
+
for (const [pattern, ideName] of Object.entries(IDE_PATTERNS)) {
|
|
11031
|
+
if (line.includes(pattern) && !line.includes("grep")) {
|
|
11032
|
+
running.add(ideName);
|
|
11033
|
+
}
|
|
11034
|
+
}
|
|
11035
|
+
}
|
|
11036
|
+
const home = homedir4();
|
|
11037
|
+
const storagePaths = [
|
|
11038
|
+
{ ide: "VS Code", path: join5(home, "Library/Application Support/Code/User/globalStorage/storage.json") },
|
|
11039
|
+
{ ide: "Cursor", path: join5(home, "Library/Application Support/Cursor/User/globalStorage/storage.json") },
|
|
11040
|
+
{ ide: "Windsurf", path: join5(home, "Library/Application Support/Windsurf/User/globalStorage/storage.json") }
|
|
11041
|
+
];
|
|
11042
|
+
const ides = [];
|
|
11043
|
+
for (const { ide: ideName, path: storagePath } of storagePaths) {
|
|
11044
|
+
if (!existsSync4(storagePath)) continue;
|
|
11045
|
+
try {
|
|
11046
|
+
const { active, all } = readIdeWorkspaces2(storagePath);
|
|
11047
|
+
ides.push({ ide: ideName, active, workspaces: all, running: running.has(ideName) });
|
|
11048
|
+
} catch {
|
|
11049
|
+
}
|
|
11050
|
+
}
|
|
11051
|
+
if (running.has("Zed") && !ides.find((i) => i.ide === "Zed")) {
|
|
11052
|
+
ides.push({ ide: "Zed", active: null, workspaces: [], running: true });
|
|
11053
|
+
}
|
|
11054
|
+
resolve5({
|
|
11055
|
+
success: true,
|
|
11056
|
+
data: {
|
|
11057
|
+
ides: ides.length > 0 ? ides : [],
|
|
11058
|
+
count: ides.length,
|
|
11059
|
+
note: ides.length === 0 ? "No IDEs detected." : void 0
|
|
11060
|
+
}
|
|
11061
|
+
});
|
|
11062
|
+
});
|
|
11063
|
+
});
|
|
11064
|
+
}
|
|
11065
|
+
case "sys_ide_get_context": {
|
|
11066
|
+
const targetIde = params.ide ?? "";
|
|
11067
|
+
const home = homedir4();
|
|
11068
|
+
const storageMap = {
|
|
11069
|
+
vscode: { label: "VS Code", path: join5(home, "Library/Application Support/Code/User/globalStorage/storage.json") },
|
|
11070
|
+
cursor: { label: "Cursor", path: join5(home, "Library/Application Support/Cursor/User/globalStorage/storage.json") },
|
|
11071
|
+
windsurf: { label: "Windsurf", path: join5(home, "Library/Application Support/Windsurf/User/globalStorage/storage.json") }
|
|
11072
|
+
};
|
|
11073
|
+
const ideKey = targetIde.toLowerCase().replace(/[\s-]/g, "");
|
|
11074
|
+
const pathsToTry = ideKey && storageMap[ideKey] ? [{ key: ideKey, ...storageMap[ideKey] }] : Object.entries(storageMap).map(([key, v]) => ({ key, ...v }));
|
|
11075
|
+
for (const { key: _key, label, path: storagePath } of pathsToTry) {
|
|
11076
|
+
if (!existsSync4(storagePath)) continue;
|
|
11077
|
+
try {
|
|
11078
|
+
const storage = JSON.parse(readFileSync4(storagePath, "utf-8"));
|
|
11079
|
+
const ws2 = storage["windowsState"] ?? {};
|
|
11080
|
+
const allWindows = [
|
|
11081
|
+
ws2["lastActiveWindow"],
|
|
11082
|
+
...Array.isArray(ws2["openedWindows"]) ? ws2["openedWindows"] : []
|
|
11083
|
+
].filter(Boolean);
|
|
11084
|
+
const seen = /* @__PURE__ */ new Set();
|
|
11085
|
+
const workspaces = [];
|
|
11086
|
+
for (const w of allWindows) {
|
|
11087
|
+
const configURI = w["workspaceIdentifier"]?.["configURIPath"] ?? "";
|
|
11088
|
+
const folderURI = w["folderUri"] ?? "";
|
|
11089
|
+
for (const uri of [configURI, folderURI]) {
|
|
11090
|
+
if (!uri) continue;
|
|
11091
|
+
let p = uri.replace(/^file:\/\//, "");
|
|
11092
|
+
if (p.endsWith(".code-workspace")) p = dirname(p);
|
|
11093
|
+
if (p && !seen.has(p)) {
|
|
11094
|
+
seen.add(p);
|
|
11095
|
+
workspaces.push(p);
|
|
11096
|
+
}
|
|
11097
|
+
}
|
|
11098
|
+
}
|
|
11099
|
+
const activeWindow = ws2["lastActiveWindow"];
|
|
11100
|
+
const activeConfigURI = activeWindow?.["workspaceIdentifier"]?.["configURIPath"] ?? "";
|
|
11101
|
+
const activeFolderURI = activeWindow?.["folderUri"] ?? "";
|
|
11102
|
+
let activeWorkspace = (activeConfigURI || activeFolderURI).replace(/^file:\/\//, "");
|
|
11103
|
+
if (activeWorkspace.endsWith(".code-workspace")) activeWorkspace = dirname(activeWorkspace);
|
|
11104
|
+
return {
|
|
11105
|
+
success: true,
|
|
11106
|
+
data: {
|
|
11107
|
+
ide: label,
|
|
11108
|
+
activeWorkspace: activeWorkspace || null,
|
|
11109
|
+
openWorkspaces: workspaces
|
|
11110
|
+
}
|
|
11111
|
+
};
|
|
11112
|
+
} catch {
|
|
11113
|
+
}
|
|
11114
|
+
}
|
|
11115
|
+
return {
|
|
11116
|
+
success: false,
|
|
11117
|
+
error: "No IDE context found. Make sure VS Code, Cursor, or Windsurf has been opened."
|
|
11118
|
+
};
|
|
11119
|
+
}
|
|
11120
|
+
case "sys_ide_run_terminal": {
|
|
11121
|
+
const command2 = params.command;
|
|
11122
|
+
if (!command2) return { success: false, error: "Missing command" };
|
|
11123
|
+
let cwd = params.workspace;
|
|
11124
|
+
if (!cwd) {
|
|
11125
|
+
const home = homedir4();
|
|
11126
|
+
for (const storagePath of [
|
|
11127
|
+
join5(home, "Library/Application Support/Cursor/User/globalStorage/storage.json"),
|
|
11128
|
+
join5(home, "Library/Application Support/Code/User/globalStorage/storage.json"),
|
|
11129
|
+
join5(home, "Library/Application Support/Windsurf/User/globalStorage/storage.json")
|
|
11130
|
+
]) {
|
|
11131
|
+
if (!existsSync4(storagePath)) continue;
|
|
11132
|
+
try {
|
|
11133
|
+
const storage = JSON.parse(readFileSync4(storagePath, "utf-8"));
|
|
11134
|
+
const ws2 = storage["windowsState"] ?? {};
|
|
11135
|
+
const aw = ws2["lastActiveWindow"];
|
|
11136
|
+
if (!aw) continue;
|
|
11137
|
+
const configURI = aw["workspaceIdentifier"]?.["configURIPath"] ?? "";
|
|
11138
|
+
const folderURI = aw["folderUri"] ?? "";
|
|
11139
|
+
const raw = configURI || folderURI;
|
|
11140
|
+
if (!raw) continue;
|
|
11141
|
+
let p = raw.replace(/^file:\/\//, "");
|
|
11142
|
+
if (p.endsWith(".code-workspace")) p = dirname(p);
|
|
11143
|
+
if (p) {
|
|
11144
|
+
cwd = p;
|
|
11145
|
+
break;
|
|
11146
|
+
}
|
|
11147
|
+
} catch {
|
|
11148
|
+
}
|
|
11149
|
+
}
|
|
11150
|
+
}
|
|
11151
|
+
const timeout = 3e4;
|
|
11152
|
+
return new Promise((resolve5) => {
|
|
11153
|
+
exec5(command2, { cwd: cwd || homedir4(), timeout }, (err, stdout, stderr) => {
|
|
11154
|
+
if (err && !stdout) {
|
|
11155
|
+
resolve5({
|
|
11156
|
+
success: false,
|
|
11157
|
+
error: `Command failed: ${stderr || err.message}`.slice(0, 2e3)
|
|
11158
|
+
});
|
|
11159
|
+
} else {
|
|
11160
|
+
resolve5({
|
|
11161
|
+
success: true,
|
|
11162
|
+
data: {
|
|
11163
|
+
command: command2,
|
|
11164
|
+
cwd: cwd || homedir4(),
|
|
11165
|
+
output: (stdout + (stderr ? `
|
|
11166
|
+
STDERR: ${stderr}` : "")).slice(0, 1e4),
|
|
11167
|
+
truncated: (stdout + stderr).length > 1e4,
|
|
11168
|
+
exitCode: err?.code ?? 0
|
|
11169
|
+
}
|
|
11170
|
+
});
|
|
11171
|
+
}
|
|
11172
|
+
});
|
|
11173
|
+
});
|
|
11174
|
+
}
|
|
11175
|
+
case "sys_ide_read_terminal": {
|
|
11176
|
+
const lines = Number(params.lines) || 50;
|
|
11177
|
+
const home = homedir4();
|
|
11178
|
+
const historyPaths = [
|
|
11179
|
+
join5(home, ".zsh_history"),
|
|
11180
|
+
join5(home, ".bash_history"),
|
|
11181
|
+
join5(home, ".local/share/fish/fish_history")
|
|
11182
|
+
];
|
|
11183
|
+
for (const histPath of historyPaths) {
|
|
11184
|
+
if (!existsSync4(histPath)) continue;
|
|
11185
|
+
try {
|
|
11186
|
+
const content = readFileSync4(histPath, "utf-8");
|
|
11187
|
+
const allLines = content.split("\n").filter(Boolean);
|
|
11188
|
+
const commands = allLines.map((l) => l.startsWith(": ") ? l.replace(/^:\s*\d+:\d+;/, "") : l).filter((l) => !l.startsWith("#")).slice(-lines);
|
|
11189
|
+
return {
|
|
11190
|
+
success: true,
|
|
11191
|
+
data: {
|
|
11192
|
+
source: histPath,
|
|
11193
|
+
lines: commands,
|
|
11194
|
+
count: commands.length
|
|
11195
|
+
}
|
|
11196
|
+
};
|
|
11197
|
+
} catch {
|
|
11198
|
+
}
|
|
11199
|
+
}
|
|
11200
|
+
return {
|
|
11201
|
+
success: false,
|
|
11202
|
+
error: "No shell history found. Make sure zsh, bash, or fish history is enabled."
|
|
11203
|
+
};
|
|
11204
|
+
}
|
|
11205
|
+
case "sys_ide_send_to_claude": {
|
|
11206
|
+
const prompt = params.prompt;
|
|
11207
|
+
if (!prompt) return { success: false, error: "Missing prompt" };
|
|
11208
|
+
let cwd = params.workspace;
|
|
11209
|
+
if (!cwd) {
|
|
11210
|
+
const home = homedir4();
|
|
11211
|
+
for (const storagePath of [
|
|
11212
|
+
join5(home, "Library/Application Support/Cursor/User/globalStorage/storage.json"),
|
|
11213
|
+
join5(home, "Library/Application Support/Code/User/globalStorage/storage.json"),
|
|
11214
|
+
join5(home, "Library/Application Support/Windsurf/User/globalStorage/storage.json")
|
|
11215
|
+
]) {
|
|
11216
|
+
if (!existsSync4(storagePath)) continue;
|
|
11217
|
+
try {
|
|
11218
|
+
const storage = JSON.parse(readFileSync4(storagePath, "utf-8"));
|
|
11219
|
+
const ws2 = storage["windowsState"] ?? {};
|
|
11220
|
+
const aw = ws2["lastActiveWindow"];
|
|
11221
|
+
if (!aw) continue;
|
|
11222
|
+
const configURI = aw["workspaceIdentifier"]?.["configURIPath"] ?? "";
|
|
11223
|
+
const folderURI = aw["folderUri"] ?? "";
|
|
11224
|
+
const raw = configURI || folderURI;
|
|
11225
|
+
if (!raw) continue;
|
|
11226
|
+
let p = raw.replace(/^file:\/\//, "");
|
|
11227
|
+
if (p.endsWith(".code-workspace")) p = dirname(p);
|
|
11228
|
+
if (p) {
|
|
11229
|
+
cwd = p;
|
|
11230
|
+
break;
|
|
11231
|
+
}
|
|
11232
|
+
} catch {
|
|
11233
|
+
}
|
|
11234
|
+
}
|
|
11235
|
+
}
|
|
11236
|
+
const claudeCmd = `claude -p ${JSON.stringify(prompt)}`;
|
|
11237
|
+
return new Promise((resolve5) => {
|
|
11238
|
+
exec5(claudeCmd, { cwd: cwd || homedir4(), timeout: 12e4, env: { ...process.env, PATH: augmentedPath2() } }, (err, stdout, stderr) => {
|
|
11239
|
+
if (err && !stdout) {
|
|
11240
|
+
resolve5({
|
|
11241
|
+
success: false,
|
|
11242
|
+
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)
|
|
11243
|
+
});
|
|
11244
|
+
} else {
|
|
11245
|
+
resolve5({
|
|
11246
|
+
success: true,
|
|
11247
|
+
data: {
|
|
11248
|
+
prompt,
|
|
11249
|
+
workspace: cwd || homedir4(),
|
|
11250
|
+
response: stdout.trim().slice(0, 1e4),
|
|
11251
|
+
truncated: stdout.length > 1e4
|
|
11252
|
+
}
|
|
11253
|
+
});
|
|
11254
|
+
}
|
|
11255
|
+
});
|
|
11256
|
+
});
|
|
11257
|
+
}
|
|
10348
11258
|
default:
|
|
10349
11259
|
return { success: false, error: `Unknown command: ${command}` };
|
|
10350
11260
|
}
|
|
@@ -10375,8 +11285,12 @@ function startImessageMonitor() {
|
|
|
10375
11285
|
lastImessageRowId = parseInt(initResult, 10) || 0;
|
|
10376
11286
|
console.log(` \u2713 iMessage: monitoring from ROWID ${lastImessageRowId}`);
|
|
10377
11287
|
} catch (err) {
|
|
10378
|
-
console.log(
|
|
10379
|
-
|
|
11288
|
+
console.log(
|
|
11289
|
+
` \u26A0 iMessage: failed to read chat.db \u2014 ${err.message}`
|
|
11290
|
+
);
|
|
11291
|
+
console.log(
|
|
11292
|
+
" Grant Full Disk Access to Terminal/iTerm in System Settings \u2192 Privacy & Security"
|
|
11293
|
+
);
|
|
10380
11294
|
return;
|
|
10381
11295
|
}
|
|
10382
11296
|
imessageTimer = setInterval(async () => {
|
|
@@ -10410,16 +11324,20 @@ function startImessageMonitor() {
|
|
|
10410
11324
|
if (rowId <= lastImessageRowId) continue;
|
|
10411
11325
|
lastImessageRowId = rowId;
|
|
10412
11326
|
if (!text || text.startsWith("\uFFFC")) continue;
|
|
10413
|
-
console.log(
|
|
10414
|
-
|
|
10415
|
-
|
|
10416
|
-
|
|
10417
|
-
|
|
10418
|
-
|
|
10419
|
-
|
|
10420
|
-
|
|
10421
|
-
|
|
10422
|
-
|
|
11327
|
+
console.log(
|
|
11328
|
+
`
|
|
11329
|
+
\u{1F4AC} iMessage from ${senderName || senderId}: ${text.slice(0, 80)}`
|
|
11330
|
+
);
|
|
11331
|
+
ws.send(
|
|
11332
|
+
JSON.stringify({
|
|
11333
|
+
type: "imessage_incoming",
|
|
11334
|
+
from: senderId || "unknown",
|
|
11335
|
+
fromName: senderName || senderId || "Unknown",
|
|
11336
|
+
chatId: chatId || senderId || "unknown",
|
|
11337
|
+
text,
|
|
11338
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
11339
|
+
})
|
|
11340
|
+
);
|
|
10423
11341
|
}
|
|
10424
11342
|
} catch {
|
|
10425
11343
|
}
|
|
@@ -10500,18 +11418,22 @@ end tell`;
|
|
|
10500
11418
|
}
|
|
10501
11419
|
} catch {
|
|
10502
11420
|
}
|
|
10503
|
-
console.log(
|
|
10504
|
-
|
|
10505
|
-
|
|
10506
|
-
|
|
10507
|
-
|
|
10508
|
-
|
|
10509
|
-
|
|
10510
|
-
|
|
10511
|
-
|
|
10512
|
-
|
|
10513
|
-
|
|
10514
|
-
|
|
11421
|
+
console.log(
|
|
11422
|
+
`
|
|
11423
|
+
\u{1F514} Permission dialog: [${procName}] "${title}" \u2014 buttons: ${buttons.join(", ")}`
|
|
11424
|
+
);
|
|
11425
|
+
ws.send(
|
|
11426
|
+
JSON.stringify({
|
|
11427
|
+
type: "permission_dialog",
|
|
11428
|
+
dialogId,
|
|
11429
|
+
procName,
|
|
11430
|
+
title: title || procName,
|
|
11431
|
+
message: desc,
|
|
11432
|
+
buttons,
|
|
11433
|
+
screenshot,
|
|
11434
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
11435
|
+
})
|
|
11436
|
+
);
|
|
10515
11437
|
} catch {
|
|
10516
11438
|
}
|
|
10517
11439
|
}, DIALOG_POLL_INTERVAL);
|
|
@@ -10541,13 +11463,22 @@ var CAPABILITY_PROBES = [
|
|
|
10541
11463
|
name: "spotify",
|
|
10542
11464
|
test: async () => {
|
|
10543
11465
|
try {
|
|
10544
|
-
await runShell4(
|
|
11466
|
+
await runShell4(
|
|
11467
|
+
"pgrep -x Spotify >/dev/null 2>&1 || ls /Applications/Spotify.app >/dev/null 2>&1"
|
|
11468
|
+
);
|
|
10545
11469
|
return true;
|
|
10546
11470
|
} catch {
|
|
10547
11471
|
return false;
|
|
10548
11472
|
}
|
|
10549
11473
|
},
|
|
10550
|
-
tools: [
|
|
11474
|
+
tools: [
|
|
11475
|
+
"sys_spotify_play",
|
|
11476
|
+
"sys_spotify_pause",
|
|
11477
|
+
"sys_spotify_current",
|
|
11478
|
+
"sys_spotify_next",
|
|
11479
|
+
"sys_spotify_previous",
|
|
11480
|
+
"sys_spotify_search"
|
|
11481
|
+
]
|
|
10551
11482
|
},
|
|
10552
11483
|
{
|
|
10553
11484
|
name: "tts",
|
|
@@ -10577,7 +11508,10 @@ var CAPABILITY_PROBES = [
|
|
|
10577
11508
|
name: "claude_cli",
|
|
10578
11509
|
test: async () => {
|
|
10579
11510
|
try {
|
|
10580
|
-
await runShell4(
|
|
11511
|
+
await runShell4(
|
|
11512
|
+
'(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)',
|
|
11513
|
+
5e3
|
|
11514
|
+
);
|
|
10581
11515
|
return true;
|
|
10582
11516
|
} catch {
|
|
10583
11517
|
return false;
|
|
@@ -10589,7 +11523,10 @@ var CAPABILITY_PROBES = [
|
|
|
10589
11523
|
name: "codex_cli",
|
|
10590
11524
|
test: async () => {
|
|
10591
11525
|
try {
|
|
10592
|
-
await runShell4(
|
|
11526
|
+
await runShell4(
|
|
11527
|
+
"which codex >/dev/null 2>&1 && codex --version >/dev/null 2>&1",
|
|
11528
|
+
5e3
|
|
11529
|
+
);
|
|
10593
11530
|
return true;
|
|
10594
11531
|
} catch {
|
|
10595
11532
|
return false;
|
|
@@ -10657,8 +11594,11 @@ async function probeCapabilities() {
|
|
|
10657
11594
|
}
|
|
10658
11595
|
}
|
|
10659
11596
|
const cap = { available, unavailable, tools: Array.from(tools) };
|
|
10660
|
-
console.log(
|
|
10661
|
-
|
|
11597
|
+
console.log(
|
|
11598
|
+
` \u2705 Available: ${available.join(", ") || "all adapter tools"}`
|
|
11599
|
+
);
|
|
11600
|
+
if (unavailable.length)
|
|
11601
|
+
console.log(` \u26A0\uFE0F Unavailable: ${unavailable.join(", ")}`);
|
|
10662
11602
|
console.log(` \u{1F4E6} ${cap.tools.length} tools verified`);
|
|
10663
11603
|
return cap;
|
|
10664
11604
|
}
|
|
@@ -10706,18 +11646,20 @@ async function connect() {
|
|
|
10706
11646
|
console.log(" Waiting for commands from Pulso agent...");
|
|
10707
11647
|
probeCapabilities().then((cap) => {
|
|
10708
11648
|
verifiedCapabilities = cap;
|
|
10709
|
-
ws.send(
|
|
10710
|
-
|
|
10711
|
-
|
|
10712
|
-
|
|
10713
|
-
|
|
10714
|
-
|
|
10715
|
-
|
|
10716
|
-
|
|
10717
|
-
|
|
10718
|
-
|
|
10719
|
-
|
|
10720
|
-
|
|
11649
|
+
ws.send(
|
|
11650
|
+
JSON.stringify({
|
|
11651
|
+
type: "extension_ready",
|
|
11652
|
+
platform: adapter.platform,
|
|
11653
|
+
version: "0.4.3",
|
|
11654
|
+
accessLevel: ACCESS_LEVEL3,
|
|
11655
|
+
homeDir: HOME4,
|
|
11656
|
+
hostname: hostname3(),
|
|
11657
|
+
capabilities: cap.available,
|
|
11658
|
+
unavailable: cap.unavailable,
|
|
11659
|
+
tools: cap.tools,
|
|
11660
|
+
totalTools: cap.tools.length
|
|
11661
|
+
})
|
|
11662
|
+
);
|
|
10721
11663
|
});
|
|
10722
11664
|
if (heartbeatTimer) clearInterval(heartbeatTimer);
|
|
10723
11665
|
heartbeatTimer = setInterval(() => {
|
|
@@ -10752,7 +11694,11 @@ async function connect() {
|
|
|
10752
11694
|
} catch {
|
|
10753
11695
|
}
|
|
10754
11696
|
};
|
|
10755
|
-
const result = await handleCommand(
|
|
11697
|
+
const result = await handleCommand(
|
|
11698
|
+
msg.command,
|
|
11699
|
+
msg.params ?? {},
|
|
11700
|
+
streamCb
|
|
11701
|
+
);
|
|
10756
11702
|
console.log(
|
|
10757
11703
|
` \u2192 ${result.success ? "\u2705" : "\u274C"}`,
|
|
10758
11704
|
result.success ? JSON.stringify(result.data).slice(0, 200) : result.error
|
|
@@ -10884,7 +11830,10 @@ function discoverWakeWordKeywordPath() {
|
|
|
10884
11830
|
const explicit = WAKE_WORD_KEYWORD_PATH.trim();
|
|
10885
11831
|
if (explicit) {
|
|
10886
11832
|
if (existsSync4(explicit)) {
|
|
10887
|
-
return {
|
|
11833
|
+
return {
|
|
11834
|
+
path: explicit,
|
|
11835
|
+
source: "PULSO_WAKE_WORD_PATH / --wake-word-path"
|
|
11836
|
+
};
|
|
10888
11837
|
}
|
|
10889
11838
|
console.log(
|
|
10890
11839
|
` \u26A0\uFE0F Wake word file not found at configured path: ${explicit}`
|
|
@@ -10943,10 +11892,15 @@ function discoverWakeWordKeywordPath() {
|
|
|
10943
11892
|
function discoverWakeWordLanguageModelPath(keywordPath, fallbackModelPath) {
|
|
10944
11893
|
const explicit = WAKE_WORD_MODEL_PATH.trim();
|
|
10945
11894
|
if (explicit && existsSync4(explicit)) {
|
|
10946
|
-
return {
|
|
11895
|
+
return {
|
|
11896
|
+
path: explicit,
|
|
11897
|
+
source: "PULSO_WAKE_WORD_MODEL_PATH / --wake-word-model-path"
|
|
11898
|
+
};
|
|
10947
11899
|
}
|
|
10948
11900
|
if (explicit && !existsSync4(explicit)) {
|
|
10949
|
-
console.log(
|
|
11901
|
+
console.log(
|
|
11902
|
+
` \u26A0\uFE0F Language model file not found at configured path: ${explicit}`
|
|
11903
|
+
);
|
|
10950
11904
|
}
|
|
10951
11905
|
const direct = join5(HOME4, ".pulso-wake-word-model.pv");
|
|
10952
11906
|
if (existsSync4(direct)) {
|
|
@@ -11001,7 +11955,10 @@ function buildWakeWordDeviceCandidates(devices, explicitDeviceIndex) {
|
|
|
11001
11955
|
}
|
|
11002
11956
|
function startWakeWordRecorder(PvRecorder, frameLength, devices) {
|
|
11003
11957
|
const explicitDeviceIndex = parseWakeWordDeviceIndex(WAKE_WORD_DEVICE_INDEX);
|
|
11004
|
-
const candidates = buildWakeWordDeviceCandidates(
|
|
11958
|
+
const candidates = buildWakeWordDeviceCandidates(
|
|
11959
|
+
devices,
|
|
11960
|
+
explicitDeviceIndex
|
|
11961
|
+
);
|
|
11005
11962
|
const errors = [];
|
|
11006
11963
|
for (const candidate of candidates) {
|
|
11007
11964
|
let recorder = null;
|
|
@@ -11024,7 +11981,9 @@ ${errors.map((e) => ` - ${e}`).join("\n")}`
|
|
|
11024
11981
|
}
|
|
11025
11982
|
function loadWakeRecorderEngine() {
|
|
11026
11983
|
if (process.platform !== "darwin") {
|
|
11027
|
-
throw new Error(
|
|
11984
|
+
throw new Error(
|
|
11985
|
+
"Wake word runtime assets are currently packaged for macOS only."
|
|
11986
|
+
);
|
|
11028
11987
|
}
|
|
11029
11988
|
const archDir = process.arch === "arm64" ? "arm64" : "x86_64";
|
|
11030
11989
|
const recorderLibraryPath = resolvePicovoiceAsset(
|
|
@@ -11045,7 +12004,9 @@ function loadWakeRecorderEngine() {
|
|
|
11045
12004
|
}
|
|
11046
12005
|
function loadWakeWordEngines() {
|
|
11047
12006
|
if (process.platform !== "darwin") {
|
|
11048
|
-
throw new Error(
|
|
12007
|
+
throw new Error(
|
|
12008
|
+
"Wake word runtime assets are currently packaged for macOS only."
|
|
12009
|
+
);
|
|
11049
12010
|
}
|
|
11050
12011
|
const archDir = process.arch === "arm64" ? "arm64" : "x86_64";
|
|
11051
12012
|
const porcupineModelPath = resolvePicovoiceAsset(
|
|
@@ -11100,21 +12061,16 @@ async function startPicovoiceWakeWordDetection() {
|
|
|
11100
12061
|
return;
|
|
11101
12062
|
}
|
|
11102
12063
|
try {
|
|
11103
|
-
const {
|
|
11104
|
-
Porcupine,
|
|
11105
|
-
PvRecorder,
|
|
11106
|
-
porcupineModelPath,
|
|
11107
|
-
porcupineLibraryPath
|
|
11108
|
-
} = loadWakeWordEngines();
|
|
12064
|
+
const { Porcupine, PvRecorder, porcupineModelPath, porcupineLibraryPath } = loadWakeWordEngines();
|
|
11109
12065
|
const keyword = discoverWakeWordKeywordPath();
|
|
11110
12066
|
if (!keyword) {
|
|
12067
|
+
console.log(" \u26A0\uFE0F Wake word model not found at ~/.pulso-wake-word.ppn");
|
|
11111
12068
|
console.log(
|
|
11112
|
-
"
|
|
12069
|
+
' "Hey Pulso" requires a Picovoice keyword model file (.ppn).'
|
|
11113
12070
|
);
|
|
11114
12071
|
console.log(
|
|
11115
|
-
|
|
12072
|
+
" Create it at https://console.picovoice.ai/ and save it to:"
|
|
11116
12073
|
);
|
|
11117
|
-
console.log(" Create it at https://console.picovoice.ai/ and save it to:");
|
|
11118
12074
|
console.log(" ~/.pulso-wake-word.ppn");
|
|
11119
12075
|
console.log(" (or set PULSO_WAKE_WORD_PATH / --wake-word-path)\n");
|
|
11120
12076
|
return;
|
|
@@ -11267,12 +12223,16 @@ async function maybeGetWakeLocalTranscript(chunks, totalSamples, sampleRate, bud
|
|
|
11267
12223
|
const timedOut = /* @__PURE__ */ Symbol("wake-stt-timeout");
|
|
11268
12224
|
const sttResult = await Promise.race([
|
|
11269
12225
|
transcribe(merged, sampleRate, { model: "tiny.en", language: "auto" }),
|
|
11270
|
-
new Promise(
|
|
12226
|
+
new Promise(
|
|
12227
|
+
(resolve5) => setTimeout(() => resolve5(timedOut), budgetMs)
|
|
12228
|
+
)
|
|
11271
12229
|
]);
|
|
11272
12230
|
if (sttResult === timedOut || !sttResult?.text) return void 0;
|
|
11273
12231
|
const transcript = sttResult.text.trim();
|
|
11274
12232
|
if (!transcript) return void 0;
|
|
11275
|
-
console.log(
|
|
12233
|
+
console.log(
|
|
12234
|
+
` \u{1F9E0} Local STT (${sttResult.durationMs ?? budgetMs}ms): "${transcript}"`
|
|
12235
|
+
);
|
|
11276
12236
|
return transcript;
|
|
11277
12237
|
} catch {
|
|
11278
12238
|
return void 0;
|
|
@@ -11307,7 +12267,9 @@ async function startSemanticWakeWordDetection() {
|
|
|
11307
12267
|
devices
|
|
11308
12268
|
);
|
|
11309
12269
|
const selectedDeviceName = selectedDeviceIndex >= 0 ? devices[selectedDeviceIndex] || `device ${selectedDeviceIndex}` : "OS default device";
|
|
11310
|
-
const calibrationFrames = Math.ceil(
|
|
12270
|
+
const calibrationFrames = Math.ceil(
|
|
12271
|
+
wakeCalibrationMs / 1e3 * sampleRate / frameLength
|
|
12272
|
+
);
|
|
11311
12273
|
let noiseFloor = 0;
|
|
11312
12274
|
if (calibrationFrames > 0) {
|
|
11313
12275
|
const samples = [];
|
|
@@ -11337,9 +12299,14 @@ async function startSemanticWakeWordDetection() {
|
|
|
11337
12299
|
" \u{1F9E0} Trigger phrase: say 'Hey Pulso' (or 'Ok Pulso', 'Ola Pulso', 'Pulso')\n"
|
|
11338
12300
|
);
|
|
11339
12301
|
const minSpeechFrames = Math.ceil(0.22 * sampleRate / frameLength);
|
|
11340
|
-
const maxSilenceFrames = Math.ceil(
|
|
12302
|
+
const maxSilenceFrames = Math.ceil(
|
|
12303
|
+
wakeEndSilenceMs / 1e3 * sampleRate / frameLength
|
|
12304
|
+
);
|
|
11341
12305
|
const maxRecordFrames = Math.ceil(10 * sampleRate / frameLength);
|
|
11342
|
-
const preRollFrames = Math.max(
|
|
12306
|
+
const preRollFrames = Math.max(
|
|
12307
|
+
1,
|
|
12308
|
+
Math.ceil(0.35 * sampleRate / frameLength)
|
|
12309
|
+
);
|
|
11343
12310
|
const sendCooldownMs = 700;
|
|
11344
12311
|
let speaking = false;
|
|
11345
12312
|
let framesCaptured = 0;
|
|
@@ -11397,7 +12364,9 @@ async function startSemanticWakeWordDetection() {
|
|
|
11397
12364
|
...localTranscript ? { localTranscript } : {}
|
|
11398
12365
|
})
|
|
11399
12366
|
);
|
|
11400
|
-
console.log(
|
|
12367
|
+
console.log(
|
|
12368
|
+
` \u{1F4E4} Semantic wake probe sent (${(durationMs / 1e3).toFixed(1)}s)`
|
|
12369
|
+
);
|
|
11401
12370
|
exec5("afplay /System/Library/Sounds/Pop.aiff");
|
|
11402
12371
|
cooldownUntil = Date.now() + sendCooldownMs;
|
|
11403
12372
|
}
|
|
@@ -11482,7 +12451,7 @@ process.on("exit", () => {
|
|
|
11482
12451
|
});
|
|
11483
12452
|
console.log("");
|
|
11484
12453
|
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");
|
|
11485
|
-
console.log(` \u2551 Pulso ${platformName} Companion v0.4.
|
|
12454
|
+
console.log(` \u2551 Pulso ${platformName} Companion v0.4.5 \u2551`);
|
|
11486
12455
|
console.log(" \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D");
|
|
11487
12456
|
console.log("");
|
|
11488
12457
|
console.log(` Platform: ${currentPlatform}`);
|