@pulso/companion 0.1.7 → 0.1.9

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 +337 -3
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@ import WebSocket from "ws";
5
5
  import { exec, execSync } from "child_process";
6
6
  import { readFileSync, writeFileSync, existsSync } from "fs";
7
7
  import { homedir } from "os";
8
- import { resolve } from "path";
8
+ import { join, resolve } from "path";
9
9
  var API_URL = process.env.PULSO_API_URL ?? process.argv.find((_, i, a) => a[i - 1] === "--api") ?? "https://pulso-api.vulk.workers.dev";
10
10
  var TOKEN = process.env.PULSO_TOKEN ?? process.argv.find((_, i, a) => a[i - 1] === "--token") ?? "";
11
11
  if (!TOKEN) {
@@ -55,6 +55,122 @@ function runSwift(code, timeout = 1e4) {
55
55
  child.stdin?.end();
56
56
  });
57
57
  }
58
+ var SETUP_DONE_FILE = join(HOME, ".pulso-companion-setup");
59
+ async function setupPermissions() {
60
+ const isFirstRun = !existsSync(SETUP_DONE_FILE);
61
+ console.log(isFirstRun ? "\u{1F510} First run \u2014 setting up permissions...\n" : "\u{1F510} Checking permissions...\n");
62
+ const status = {};
63
+ const browserDefaults = [
64
+ ["Google Chrome", "com.google.Chrome", "AllowJavaScriptAppleEvents"],
65
+ ["Safari", "com.apple.Safari", "AllowJavaScriptFromAppleEvents"],
66
+ ["Arc", "company.thebrowser.Browser", "AllowJavaScriptAppleEvents"],
67
+ ["Microsoft Edge", "com.microsoft.edgemac", "AllowJavaScriptAppleEvents"]
68
+ ];
69
+ for (const [name, domain, key] of browserDefaults) {
70
+ try {
71
+ execSync(`defaults write ${domain} ${key} -bool true 2>/dev/null`, { timeout: 3e3 });
72
+ if (name === "Google Chrome" || name === "Safari") status[`${name} JS`] = "\u2705";
73
+ } catch {
74
+ if (name === "Google Chrome" || name === "Safari") status[`${name} JS`] = "\u26A0\uFE0F";
75
+ }
76
+ }
77
+ for (const [browserName, processName] of [["Google Chrome", "Google Chrome"], ["Safari", "Safari"]]) {
78
+ try {
79
+ const running = execSync(`pgrep -x "${processName}" 2>/dev/null`, { timeout: 2e3 }).toString().trim();
80
+ if (!running) continue;
81
+ const enableJsScript = browserName === "Google Chrome" ? `tell application "System Events"
82
+ tell process "Google Chrome"
83
+ -- Find the Developer submenu (localized name varies)
84
+ set viewMenu to menu bar item -3 of menu bar 1
85
+ set viewMenuItems to name of every menu item of menu 1 of viewMenu
86
+ set devMenuItem to last menu item of menu 1 of viewMenu
87
+ set devItems to name of every menu item of menu 1 of devMenuItem
88
+ -- The JS Apple Events item is always the last one in Developer menu
89
+ set jsItem to last menu item of menu 1 of devMenuItem
90
+ set markChar to value of attribute "AXMenuItemMarkChar" of jsItem
91
+ if markChar is missing value then
92
+ click jsItem
93
+ delay 1
94
+ -- Accept any confirmation dialog
95
+ try
96
+ click button 1 of sheet 1 of window 1
97
+ end try
98
+ return "enabled"
99
+ else
100
+ return "already_enabled"
101
+ end if
102
+ end tell
103
+ end tell` : `return "skip_safari"`;
104
+ const result = execSync(`osascript -e '${enableJsScript.replace(/'/g, "'\\''")}' 2>/dev/null`, { timeout: 1e4 }).toString().trim();
105
+ if (result === "enabled") {
106
+ console.log(` \u2705 ${browserName}: JavaScript from Apple Events enabled via menu`);
107
+ }
108
+ } catch {
109
+ }
110
+ }
111
+ try {
112
+ execSync(`osascript -e 'tell application "System Events" to name of first process' 2>/dev/null`, { timeout: 5e3 });
113
+ status["Accessibility"] = "\u2705";
114
+ } catch (err) {
115
+ const msg = err.message || "";
116
+ if (msg.includes("not allowed") || msg.includes("assistive") || msg.includes("-1719")) {
117
+ status["Accessibility"] = "\u26A0\uFE0F";
118
+ if (isFirstRun) {
119
+ console.log(" \u26A0\uFE0F Accessibility: macOS should show a permission dialog.");
120
+ console.log(" If not, go to: System Settings \u2192 Privacy & Security \u2192 Accessibility");
121
+ console.log(" Add your terminal app (Terminal, iTerm, Warp, etc.)\n");
122
+ }
123
+ } else {
124
+ status["Accessibility"] = "\u2705";
125
+ }
126
+ }
127
+ try {
128
+ const testPath = `/tmp/.pulso-perm-test-${Date.now()}.png`;
129
+ execSync(`screencapture -x ${testPath} 2>/dev/null`, { timeout: 8e3 });
130
+ if (existsSync(testPath)) {
131
+ const stat = readFileSync(testPath);
132
+ execSync(`rm -f ${testPath}`);
133
+ if (stat.length > 100) {
134
+ status["Screen Recording"] = "\u2705";
135
+ } else {
136
+ status["Screen Recording"] = "\u26A0\uFE0F";
137
+ if (isFirstRun) {
138
+ console.log(" \u26A0\uFE0F Screen Recording: macOS should show a permission dialog.");
139
+ console.log(" If not, go to: System Settings \u2192 Privacy & Security \u2192 Screen Recording");
140
+ console.log(" Add your terminal app\n");
141
+ }
142
+ }
143
+ } else {
144
+ status["Screen Recording"] = "\u274C";
145
+ }
146
+ } catch {
147
+ status["Screen Recording"] = "\u26A0\uFE0F";
148
+ if (isFirstRun) {
149
+ console.log(" \u26A0\uFE0F Screen Recording: permission needed for screenshots.");
150
+ console.log(" Go to: System Settings \u2192 Privacy & Security \u2192 Screen Recording\n");
151
+ }
152
+ }
153
+ console.log(" Permission Status:");
154
+ for (const [name, icon] of Object.entries(status)) {
155
+ const note = icon === "\u2705" ? "enabled" : icon === "\u26A0\uFE0F" ? "needs approval" : "not available";
156
+ console.log(` ${icon} ${name}: ${note}`);
157
+ }
158
+ console.log("");
159
+ if (isFirstRun) {
160
+ try {
161
+ writeFileSync(SETUP_DONE_FILE, (/* @__PURE__ */ new Date()).toISOString(), "utf-8");
162
+ } catch {
163
+ }
164
+ }
165
+ const needsManual = status["Accessibility"] === "\u26A0\uFE0F" || status["Screen Recording"] === "\u26A0\uFE0F";
166
+ if (isFirstRun && needsManual) {
167
+ console.log(" Opening System Settings for you to approve permissions...\n");
168
+ try {
169
+ execSync('open "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility"', { timeout: 3e3 });
170
+ } catch {
171
+ }
172
+ }
173
+ }
58
174
  var UA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
59
175
  var searchCache = /* @__PURE__ */ new Map();
60
176
  var CACHE_TTL = 7 * 24 * 3600 * 1e3;
@@ -541,6 +657,218 @@ print("\\(x),\\(y)")`;
541
657
  const [cx, cy] = pos.trim().split(",").map(Number);
542
658
  return { success: true, data: { x: cx, y: cy } };
543
659
  }
660
+ // ── Browser Automation ─────────────────────────────────
661
+ case "sys_browser_list_tabs": {
662
+ const browsers = ["Google Chrome", "Safari", "Arc", "Firefox", "Microsoft Edge"];
663
+ const allTabs = [];
664
+ for (const browser of browsers) {
665
+ try {
666
+ const running = await runAppleScript(
667
+ `tell application "System Events" to (name of processes) contains "${browser}"`
668
+ );
669
+ if (running.trim() !== "true") continue;
670
+ if (browser === "Safari") {
671
+ const tabData = await runAppleScript(`
672
+ tell application "Safari"
673
+ set tabList to ""
674
+ set activeURL to URL of front document
675
+ repeat with w in windows
676
+ repeat with t in tabs of w
677
+ set tabList to tabList & name of t & "|||" & URL of t & "|||"
678
+ end repeat
679
+ end repeat
680
+ return activeURL & "~~~" & tabList
681
+ end tell`);
682
+ const [activeURL, ...rest] = tabData.split("~~~");
683
+ const tabStr = rest.join("~~~");
684
+ const pairs = tabStr.split("|||").filter(Boolean);
685
+ for (let i = 0; i < pairs.length - 1; i += 2) {
686
+ allTabs.push({ browser: "Safari", title: pairs[i], url: pairs[i + 1], active: pairs[i + 1] === activeURL.trim() });
687
+ }
688
+ } else {
689
+ const tabData = await runAppleScript(`
690
+ tell application "${browser}"
691
+ set tabList to ""
692
+ set activeTabURL to URL of active tab of front window
693
+ repeat with w in windows
694
+ repeat with t in tabs of w
695
+ set tabList to tabList & title of t & "|||" & URL of t & "|||"
696
+ end repeat
697
+ end repeat
698
+ return activeTabURL & "~~~" & tabList
699
+ end tell`);
700
+ const [activeURL, ...rest] = tabData.split("~~~");
701
+ const tabStr = rest.join("~~~");
702
+ const pairs = tabStr.split("|||").filter(Boolean);
703
+ for (let i = 0; i < pairs.length - 1; i += 2) {
704
+ allTabs.push({ browser, title: pairs[i], url: pairs[i + 1], active: pairs[i + 1] === activeURL.trim() });
705
+ }
706
+ }
707
+ } catch {
708
+ }
709
+ }
710
+ return { success: true, data: { tabs: allTabs, count: allTabs.length } };
711
+ }
712
+ case "sys_browser_navigate": {
713
+ const url = params.url;
714
+ const browser = params.browser || "Google Chrome";
715
+ if (!url) return { success: false, error: "Missing URL" };
716
+ try {
717
+ if (browser === "Safari") {
718
+ await runAppleScript(`
719
+ tell application "Safari"
720
+ activate
721
+ if (count of windows) = 0 then make new document
722
+ set URL of front document to "${url.replace(/"/g, '\\"')}"
723
+ end tell`);
724
+ } else {
725
+ await runAppleScript(`
726
+ tell application "${browser.replace(/"/g, '\\"')}"
727
+ activate
728
+ if (count of windows) = 0 then
729
+ make new window
730
+ set URL of active tab of front window to "${url.replace(/"/g, '\\"')}"
731
+ else
732
+ set URL of active tab of front window to "${url.replace(/"/g, '\\"')}"
733
+ end if
734
+ end tell`);
735
+ }
736
+ return { success: true, data: { navigated: url, browser } };
737
+ } catch (err) {
738
+ return { success: false, error: `Failed to navigate: ${err.message}` };
739
+ }
740
+ }
741
+ case "sys_browser_new_tab": {
742
+ const url = params.url;
743
+ const browser = params.browser || "Google Chrome";
744
+ if (!url) return { success: false, error: "Missing URL" };
745
+ try {
746
+ if (browser === "Safari") {
747
+ await runAppleScript(`
748
+ tell application "Safari"
749
+ activate
750
+ tell front window to set current tab to (make new tab with properties {URL:"${url.replace(/"/g, '\\"')}"})
751
+ end tell`);
752
+ } else {
753
+ await runAppleScript(`
754
+ tell application "${browser.replace(/"/g, '\\"')}"
755
+ activate
756
+ tell front window to make new tab with properties {URL:"${url.replace(/"/g, '\\"')}"}
757
+ end tell`);
758
+ }
759
+ return { success: true, data: { opened: url, browser } };
760
+ } catch (err) {
761
+ return { success: false, error: `Failed to open tab: ${err.message}` };
762
+ }
763
+ }
764
+ case "sys_browser_read_page": {
765
+ const browser = params.browser || "Google Chrome";
766
+ const maxLen = Number(params.maxLength) || 8e3;
767
+ let content = "";
768
+ let method = "";
769
+ try {
770
+ if (browser === "Safari") {
771
+ content = await runAppleScript(`
772
+ tell application "Safari"
773
+ set pageText to do JavaScript "document.body.innerText" in front document
774
+ return pageText
775
+ end tell`);
776
+ method = "javascript";
777
+ } else {
778
+ content = await runAppleScript(`
779
+ tell application "${browser.replace(/"/g, '\\"')}"
780
+ set pageText to execute javascript "document.body.innerText" in active tab of front window
781
+ return pageText
782
+ end tell`);
783
+ method = "javascript";
784
+ }
785
+ } catch {
786
+ try {
787
+ const savedClipboard = await runShell("pbpaste 2>/dev/null || true");
788
+ await runAppleScript(`tell application "${browser.replace(/"/g, '\\"')}" to activate`);
789
+ await new Promise((r) => setTimeout(r, 300));
790
+ await runAppleScript('tell application "System Events" to keystroke "a" using command down');
791
+ await new Promise((r) => setTimeout(r, 200));
792
+ await runAppleScript('tell application "System Events" to keystroke "c" using command down');
793
+ await new Promise((r) => setTimeout(r, 300));
794
+ content = await runShell("pbpaste");
795
+ method = "clipboard";
796
+ await runAppleScript('tell application "System Events" to key code 53');
797
+ if (savedClipboard && savedClipboard !== content) {
798
+ execSync(`echo ${JSON.stringify(savedClipboard)} | pbcopy`);
799
+ }
800
+ } catch (clipErr) {
801
+ return { success: false, error: `Could not read page. Enable 'Allow JavaScript from Apple Events' in ${browser} or grant Accessibility permission. Error: ${clipErr.message}` };
802
+ }
803
+ }
804
+ let pageUrl = "";
805
+ let pageTitle = "";
806
+ try {
807
+ if (browser === "Safari") {
808
+ pageUrl = await runAppleScript('tell application "Safari" to return URL of front document');
809
+ pageTitle = await runAppleScript('tell application "Safari" to return name of front document');
810
+ } else {
811
+ pageUrl = await runAppleScript(`tell application "${browser.replace(/"/g, '\\"')}" to return URL of active tab of front window`);
812
+ pageTitle = await runAppleScript(`tell application "${browser.replace(/"/g, '\\"')}" to return title of active tab of front window`);
813
+ }
814
+ } catch {
815
+ }
816
+ return {
817
+ success: true,
818
+ data: {
819
+ title: pageTitle.trim(),
820
+ url: pageUrl.trim(),
821
+ content: content.slice(0, maxLen),
822
+ length: content.length,
823
+ truncated: content.length > maxLen,
824
+ method
825
+ }
826
+ };
827
+ }
828
+ case "sys_browser_execute_js": {
829
+ const js = params.code;
830
+ const browser = params.browser || "Google Chrome";
831
+ if (!js) return { success: false, error: "Missing JavaScript code" };
832
+ try {
833
+ let result;
834
+ if (browser === "Safari") {
835
+ result = await runAppleScript(`tell application "Safari" to do JavaScript ${JSON.stringify(js)} in front document`);
836
+ } else {
837
+ result = await runAppleScript(`tell application "${browser.replace(/"/g, '\\"')}" to execute javascript ${JSON.stringify(js)} in active tab of front window`);
838
+ }
839
+ return { success: true, data: { result: result.slice(0, 5e3) } };
840
+ } catch (err) {
841
+ return { success: false, error: `JS execution failed (enable 'Allow JavaScript from Apple Events' in browser): ${err.message}` };
842
+ }
843
+ }
844
+ // ── Email ──────────────────────────────────────────────
845
+ case "sys_email_send": {
846
+ const to = params.to;
847
+ const subject = params.subject;
848
+ const body = params.body;
849
+ const method = params.method || "mail";
850
+ if (!to || !subject || !body) return { success: false, error: "Missing to, subject, or body" };
851
+ if (method === "gmail") {
852
+ const gmailUrl = `https://mail.google.com/mail/u/0/?view=cm&fs=1&to=${encodeURIComponent(to)}&su=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`;
853
+ await runShell(`open "${gmailUrl}"`);
854
+ return { success: true, data: { method: "gmail", to, subject, note: "Gmail compose opened. User needs to click Send." } };
855
+ }
856
+ try {
857
+ await runAppleScript(`
858
+ tell application "Mail"
859
+ set newMessage to make new outgoing message with properties {subject:"${subject.replace(/"/g, '\\"')}", content:"${body.replace(/"/g, '\\"')}"}
860
+ tell newMessage
861
+ make new to recipient at end of to recipients with properties {address:"${to.replace(/"/g, '\\"')}"}
862
+ end tell
863
+ send newMessage
864
+ end tell`);
865
+ return { success: true, data: { method: "mail", to, subject, sent: true } };
866
+ } catch (err) {
867
+ const gmailUrl = `https://mail.google.com/mail/u/0/?view=cm&fs=1&to=${encodeURIComponent(to)}&su=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`;
868
+ await runShell(`open "${gmailUrl}"`);
869
+ return { success: true, data: { method: "gmail_fallback", to, subject, note: `Mail.app failed (${err.message}). Gmail compose opened instead.` } };
870
+ }
871
+ }
544
872
  case "sys_run_shortcut": {
545
873
  const name = params.name;
546
874
  const input = params.input;
@@ -575,6 +903,8 @@ function connect() {
575
903
  console.log(" \u2022 Clipboard access");
576
904
  console.log(" \u2022 Screenshots");
577
905
  console.log(" \u2022 Text-to-speech");
906
+ console.log(" \u2022 Browser automation (tabs, navigate, read pages)");
907
+ console.log(" \u2022 Email composition (Mail.app / Gmail)");
578
908
  console.log(" \u2022 Terminal commands");
579
909
  console.log(" \u2022 System notifications");
580
910
  console.log(" \u2022 macOS Shortcuts");
@@ -635,10 +965,14 @@ function scheduleReconnect() {
635
965
  }
636
966
  console.log("");
637
967
  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");
638
- console.log(" \u2551 \u{1FAC0} Pulso Mac Companion v0.1.5 \u2551");
968
+ console.log(" \u2551 \u{1FAC0} Pulso Mac Companion v0.1.9 \u2551");
639
969
  console.log(" \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D");
640
970
  console.log("");
641
- connect();
971
+ setupPermissions().then(() => {
972
+ connect();
973
+ }).catch(() => {
974
+ connect();
975
+ });
642
976
  process.on("SIGINT", () => {
643
977
  console.log("\n\u{1F44B} Shutting down Pulso Companion...");
644
978
  ws?.close(1e3, "User shutdown");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pulso/companion",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "type": "module",
5
5
  "description": "Pulso Companion — gives your AI agent real control over your computer",
6
6
  "bin": {