@pulso/companion 0.4.3 → 0.4.5

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