@pulso/companion 0.1.8 → 0.2.0

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 +551 -4
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -3,9 +3,9 @@
3
3
  // src/index.ts
4
4
  import WebSocket from "ws";
5
5
  import { exec, execSync } from "child_process";
6
- import { readFileSync, writeFileSync, existsSync } from "fs";
6
+ import { readFileSync, writeFileSync, existsSync, readdirSync, statSync, copyFileSync, renameSync } from "fs";
7
7
  import { homedir } from "os";
8
- import { resolve } from "path";
8
+ import { join, resolve, basename, extname } 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;
@@ -761,6 +877,433 @@ print("\\(x),\\(y)")`;
761
877
  const result = await runShell(`shortcuts run "${name.replace(/"/g, '\\"')}" ${inputFlag}`, 3e4);
762
878
  return { success: true, data: { shortcut: name, output: result || "Shortcut executed" } };
763
879
  }
880
+ // ── Shell Execution ─────────────────────────────────────
881
+ case "sys_shell": {
882
+ const cmd = params.command;
883
+ if (!cmd) return { success: false, error: "Missing command" };
884
+ const blocked = ["rm -rf /", "mkfs", "dd if=", "> /dev/", ":(){ :|:& };:"];
885
+ if (blocked.some((b) => cmd.includes(b))) return { success: false, error: "Command blocked for safety" };
886
+ const timeout = Number(params.timeout) || 15e3;
887
+ try {
888
+ const output = await runShell(cmd, timeout);
889
+ return { success: true, data: { command: cmd, output: output.slice(0, 1e4), truncated: output.length > 1e4 } };
890
+ } catch (err) {
891
+ return { success: false, error: `Shell error: ${err.message.slice(0, 2e3)}` };
892
+ }
893
+ }
894
+ // ── Calendar ────────────────────────────────────────────
895
+ case "sys_calendar_list": {
896
+ const days = Number(params.days) || 7;
897
+ const startDate = /* @__PURE__ */ new Date();
898
+ const endDate = new Date(Date.now() + days * 864e5);
899
+ const fmt = (d) => `date "${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}"`;
900
+ const script = `
901
+ set output to ""
902
+ tell application "Calendar"
903
+ repeat with cal in calendars
904
+ set calName to name of cal
905
+ set evts to (every event of cal whose start date >= (current date) and start date < ((current date) + ${days} * days))
906
+ repeat with e in evts
907
+ set output to output & calName & " | " & summary of e & " | " & (start date of e as string) & " | " & (end date of e as string) & linefeed
908
+ end repeat
909
+ end repeat
910
+ end tell
911
+ return output`;
912
+ const raw = await runAppleScript(script);
913
+ const events = raw.split("\n").filter(Boolean).map((line) => {
914
+ const [cal, summary, start, end] = line.split(" | ");
915
+ return { calendar: cal?.trim(), summary: summary?.trim(), start: start?.trim(), end: end?.trim() };
916
+ });
917
+ return { success: true, data: { events, count: events.length, daysAhead: days } };
918
+ }
919
+ case "sys_calendar_create": {
920
+ const summary = params.summary || params.title;
921
+ const startStr = params.start;
922
+ const endStr = params.end;
923
+ const calendar = params.calendar || "";
924
+ const notes = params.notes || "";
925
+ if (!summary || !startStr) return { success: false, error: "Missing summary or start time" };
926
+ const calTarget = calendar ? `calendar "${calendar.replace(/"/g, '\\"')}"` : "default calendar";
927
+ const endPart = endStr ? `set end date of newEvent to date "${endStr}"` : "";
928
+ const notesPart = notes ? `set description of newEvent to "${notes.replace(/"/g, '\\"')}"` : "";
929
+ await runAppleScript(`
930
+ tell application "Calendar"
931
+ tell ${calTarget}
932
+ set newEvent to make new event with properties {summary:"${summary.replace(/"/g, '\\"')}", start date:date "${startStr}"}
933
+ ${endPart}
934
+ ${notesPart}
935
+ end tell
936
+ end tell`);
937
+ return { success: true, data: { created: summary, start: startStr, end: endStr || "auto" } };
938
+ }
939
+ // ── Reminders ───────────────────────────────────────────
940
+ case "sys_reminder_list": {
941
+ const listName = params.list || "";
942
+ const script = listName ? `tell application "Reminders" to set rems to reminders of list "${listName.replace(/"/g, '\\"')}" whose completed is false
943
+ set output to ""
944
+ repeat with r in rems
945
+ set output to output & name of r & " | " & (due date of r as string) & linefeed
946
+ end repeat
947
+ return output` : `tell application "Reminders"
948
+ set output to ""
949
+ repeat with l in lists
950
+ set rems to reminders of l whose completed is false
951
+ repeat with r in rems
952
+ set output to output & name of l & " | " & name of r & " | " & (due date of r as string) & linefeed
953
+ end repeat
954
+ end repeat
955
+ return output
956
+ end tell`;
957
+ try {
958
+ const raw = await runAppleScript(script);
959
+ const reminders = raw.split("\n").filter(Boolean).map((line) => {
960
+ const parts = line.split(" | ");
961
+ return parts.length === 3 ? { list: parts[0]?.trim(), name: parts[1]?.trim(), due: parts[2]?.trim() } : { name: parts[0]?.trim(), due: parts[1]?.trim() };
962
+ });
963
+ return { success: true, data: { reminders, count: reminders.length } };
964
+ } catch {
965
+ return { success: true, data: { reminders: [], count: 0, note: "No reminders or Reminders app not accessible" } };
966
+ }
967
+ }
968
+ case "sys_reminder_create": {
969
+ const reminderName = params.name;
970
+ const dueDate = params.due;
971
+ const listName2 = params.list || "Reminders";
972
+ if (!reminderName) return { success: false, error: "Missing reminder name" };
973
+ const duePart = dueDate ? `, due date:date "${dueDate}"` : "";
974
+ await runAppleScript(`
975
+ tell application "Reminders"
976
+ tell list "${listName2.replace(/"/g, '\\"')}"
977
+ make new reminder with properties {name:"${reminderName.replace(/"/g, '\\"')}"${duePart}}
978
+ end tell
979
+ end tell`);
980
+ return { success: true, data: { created: reminderName, due: dueDate || "none", list: listName2 } };
981
+ }
982
+ // ── iMessage ────────────────────────────────────────────
983
+ case "sys_imessage_send": {
984
+ const to2 = params.to;
985
+ const msg = params.message;
986
+ if (!to2 || !msg) return { success: false, error: "Missing 'to' (phone/email) or 'message'" };
987
+ await runAppleScript(`
988
+ tell application "Messages"
989
+ set targetService to 1st account whose service type = iMessage
990
+ set targetBuddy to participant "${to2.replace(/"/g, '\\"')}" of targetService
991
+ send "${msg.replace(/"/g, '\\"')}" to targetBuddy
992
+ end tell`);
993
+ return { success: true, data: { sent: true, to: to2, message: msg.slice(0, 100) } };
994
+ }
995
+ // ── System Info ─────────────────────────────────────────
996
+ case "sys_system_info": {
997
+ const info = {};
998
+ try {
999
+ info.battery = (await runShell("pmset -g batt | grep -Eo '\\d+%'")).trim();
1000
+ } catch {
1001
+ info.battery = "N/A";
1002
+ }
1003
+ try {
1004
+ info.wifi = (await runShell("networksetup -getairportnetwork en0 2>/dev/null | cut -d: -f2")).trim();
1005
+ } catch {
1006
+ info.wifi = "N/A";
1007
+ }
1008
+ try {
1009
+ info.ip_local = (await runShell("ipconfig getifaddr en0 2>/dev/null || echo 'N/A'")).trim();
1010
+ } catch {
1011
+ info.ip_local = "N/A";
1012
+ }
1013
+ try {
1014
+ info.ip_public = (await runShell("curl -s --max-time 3 ifconfig.me")).trim();
1015
+ } catch {
1016
+ info.ip_public = "N/A";
1017
+ }
1018
+ try {
1019
+ info.disk = (await runShell(`df -h / | tail -1 | awk '{print $3 " used / " $2 " total (" $5 " used)"}'`)).trim();
1020
+ } catch {
1021
+ info.disk = "N/A";
1022
+ }
1023
+ try {
1024
+ info.memory = (await runShell("vm_stat | head -5")).trim();
1025
+ } catch {
1026
+ info.memory = "N/A";
1027
+ }
1028
+ try {
1029
+ info.uptime = (await runShell("uptime | sed 's/.*up /up /' | sed 's/,.*//'")).trim();
1030
+ } catch {
1031
+ info.uptime = "N/A";
1032
+ }
1033
+ try {
1034
+ info.hostname = (await runShell("hostname")).trim();
1035
+ } catch {
1036
+ info.hostname = "N/A";
1037
+ }
1038
+ try {
1039
+ info.os = (await runShell("sw_vers -productVersion")).trim();
1040
+ } catch {
1041
+ info.os = "N/A";
1042
+ }
1043
+ try {
1044
+ info.cpu = (await runShell("sysctl -n machdep.cpu.brand_string")).trim();
1045
+ } catch {
1046
+ info.cpu = "N/A";
1047
+ }
1048
+ return { success: true, data: info };
1049
+ }
1050
+ // ── Volume Control ──────────────────────────────────────
1051
+ case "sys_volume": {
1052
+ const level = params.level;
1053
+ if (level !== void 0) {
1054
+ const vol = Math.max(0, Math.min(100, Number(level)));
1055
+ await runAppleScript(`set volume output volume ${vol}`);
1056
+ return { success: true, data: { volume: vol } };
1057
+ }
1058
+ const raw2 = await runAppleScript("output volume of (get volume settings)");
1059
+ return { success: true, data: { volume: Number(raw2) || 0 } };
1060
+ }
1061
+ // ── Brightness ──────────────────────────────────────────
1062
+ case "sys_brightness": {
1063
+ const level2 = params.level;
1064
+ if (level2 !== void 0) {
1065
+ const br = Math.max(0, Math.min(1, Number(level2)));
1066
+ await runShell(`brightness ${br} 2>/dev/null || osascript -e 'tell application "System Events" to tell appearance preferences to set dark mode to ${br < 0.3}'`);
1067
+ return { success: true, data: { brightness: br } };
1068
+ }
1069
+ try {
1070
+ const raw3 = await runShell("brightness -l 2>/dev/null | grep brightness | head -1 | awk '{print $NF}'");
1071
+ return { success: true, data: { brightness: parseFloat(raw3) || 0.5 } };
1072
+ } catch {
1073
+ return { success: true, data: { brightness: "unknown", note: "Install 'brightness' via brew for control" } };
1074
+ }
1075
+ }
1076
+ // ── Do Not Disturb ──────────────────────────────────────
1077
+ case "sys_dnd": {
1078
+ const enabled = params.enabled;
1079
+ if (enabled !== void 0) {
1080
+ try {
1081
+ await runShell(`shortcuts run "Toggle Do Not Disturb" 2>/dev/null || osascript -e 'do shell script "defaults write com.apple.ncprefs dnd_prefs -data 0"'`);
1082
+ return { success: true, data: { dnd: enabled, note: "DND toggled" } };
1083
+ } catch {
1084
+ return { success: true, data: { dnd: enabled, note: "Set DND manually in Control Center" } };
1085
+ }
1086
+ }
1087
+ return { success: true, data: { note: "Pass enabled: true/false to toggle DND" } };
1088
+ }
1089
+ // ── File Management ─────────────────────────────────────
1090
+ case "sys_file_list": {
1091
+ const dirPath = params.path || "Desktop";
1092
+ const fullDir = safePath(dirPath);
1093
+ if (!fullDir) return { success: false, error: `Access denied: ${dirPath}` };
1094
+ if (!existsSync(fullDir)) return { success: false, error: `Directory not found: ${dirPath}` };
1095
+ const entries = readdirSync(fullDir).map((name) => {
1096
+ try {
1097
+ const st = statSync(join(fullDir, name));
1098
+ return { name, type: st.isDirectory() ? "dir" : "file", size: st.size, modified: st.mtime.toISOString() };
1099
+ } catch {
1100
+ return { name, type: "unknown", size: 0, modified: "" };
1101
+ }
1102
+ });
1103
+ return { success: true, data: { path: dirPath, entries, count: entries.length } };
1104
+ }
1105
+ case "sys_file_move": {
1106
+ const src = params.from;
1107
+ const dst = params.to;
1108
+ if (!src || !dst) return { success: false, error: "Missing from/to paths" };
1109
+ const fullSrc = safePath(src);
1110
+ const fullDst = safePath(dst);
1111
+ if (!fullSrc || !fullDst) return { success: false, error: "Access denied" };
1112
+ if (!existsSync(fullSrc)) return { success: false, error: `Source not found: ${src}` };
1113
+ renameSync(fullSrc, fullDst);
1114
+ return { success: true, data: { moved: src, to: dst } };
1115
+ }
1116
+ case "sys_file_copy": {
1117
+ const src2 = params.from;
1118
+ const dst2 = params.to;
1119
+ if (!src2 || !dst2) return { success: false, error: "Missing from/to paths" };
1120
+ const fullSrc2 = safePath(src2);
1121
+ const fullDst2 = safePath(dst2);
1122
+ if (!fullSrc2 || !fullDst2) return { success: false, error: "Access denied" };
1123
+ if (!existsSync(fullSrc2)) return { success: false, error: `Source not found: ${src2}` };
1124
+ copyFileSync(fullSrc2, fullDst2);
1125
+ return { success: true, data: { copied: src2, to: dst2 } };
1126
+ }
1127
+ case "sys_file_delete": {
1128
+ const target = params.path;
1129
+ if (!target) return { success: false, error: "Missing path" };
1130
+ const fullTarget = safePath(target);
1131
+ if (!fullTarget) return { success: false, error: "Access denied" };
1132
+ if (!existsSync(fullTarget)) return { success: false, error: `Not found: ${target}` };
1133
+ await runShell(`osascript -e 'tell application "Finder" to delete POSIX file "${fullTarget}"'`);
1134
+ return { success: true, data: { deleted: target, method: "moved_to_trash" } };
1135
+ }
1136
+ case "sys_file_info": {
1137
+ const fPath = params.path;
1138
+ if (!fPath) return { success: false, error: "Missing path" };
1139
+ const fullF = safePath(fPath);
1140
+ if (!fullF) return { success: false, error: "Access denied" };
1141
+ if (!existsSync(fullF)) return { success: false, error: `Not found: ${fPath}` };
1142
+ const st = statSync(fullF);
1143
+ return {
1144
+ success: true,
1145
+ data: {
1146
+ path: fPath,
1147
+ name: basename(fullF),
1148
+ extension: extname(fullF),
1149
+ size: st.size,
1150
+ sizeHuman: st.size > 1048576 ? `${(st.size / 1048576).toFixed(1)} MB` : `${(st.size / 1024).toFixed(1)} KB`,
1151
+ isDirectory: st.isDirectory(),
1152
+ created: st.birthtime.toISOString(),
1153
+ modified: st.mtime.toISOString()
1154
+ }
1155
+ };
1156
+ }
1157
+ // ── Download ────────────────────────────────────────────
1158
+ case "sys_download": {
1159
+ const dlUrl = params.url;
1160
+ const dlDest = params.path || `Downloads/${basename(new URL(dlUrl).pathname) || "download"}`;
1161
+ if (!dlUrl) return { success: false, error: "Missing URL" };
1162
+ const fullDl = safePath(dlDest);
1163
+ if (!fullDl) return { success: false, error: "Access denied" };
1164
+ await runShell(`curl -sL -o "${fullDl}" "${dlUrl.replace(/"/g, '\\"')}"`, 6e4);
1165
+ const size = existsSync(fullDl) ? statSync(fullDl).size : 0;
1166
+ return { success: true, data: { downloaded: dlUrl, saved: dlDest, size } };
1167
+ }
1168
+ // ── Window Management ───────────────────────────────────
1169
+ case "sys_window_list": {
1170
+ const raw4 = await runAppleScript(`
1171
+ tell application "System Events"
1172
+ set output to ""
1173
+ repeat with proc in (every process whose visible is true)
1174
+ set procName to name of proc
1175
+ repeat with w in windows of proc
1176
+ set output to output & procName & " | " & (name of w) & " | " & (position of w as string) & " | " & (size of w as string) & linefeed
1177
+ end repeat
1178
+ end repeat
1179
+ return output
1180
+ end tell`);
1181
+ const windows = raw4.split("\n").filter(Boolean).map((line) => {
1182
+ const [app, title, pos, sz] = line.split(" | ");
1183
+ return { app: app?.trim(), title: title?.trim(), position: pos?.trim(), size: sz?.trim() };
1184
+ });
1185
+ return { success: true, data: { windows, count: windows.length } };
1186
+ }
1187
+ case "sys_window_focus": {
1188
+ const appName = params.app;
1189
+ if (!appName) return { success: false, error: "Missing app name" };
1190
+ await runAppleScript(`
1191
+ tell application "${appName.replace(/"/g, '\\"')}"
1192
+ activate
1193
+ set frontmost to true
1194
+ end tell`);
1195
+ return { success: true, data: { focused: appName } };
1196
+ }
1197
+ case "sys_window_resize": {
1198
+ const app2 = params.app;
1199
+ const x = params.x;
1200
+ const y = params.y;
1201
+ const w = params.width;
1202
+ const h = params.height;
1203
+ if (!app2) return { success: false, error: "Missing app name" };
1204
+ const posPart = x !== void 0 && y !== void 0 ? `set position of window 1 to {${x}, ${y}}` : "";
1205
+ const sizePart = w !== void 0 && h !== void 0 ? `set size of window 1 to {${w}, ${h}}` : "";
1206
+ if (!posPart && !sizePart) return { success: false, error: "Provide x,y for position and/or width,height for size" };
1207
+ await runAppleScript(`
1208
+ tell application "System Events"
1209
+ tell process "${app2.replace(/"/g, '\\"')}"
1210
+ ${posPart}
1211
+ ${sizePart}
1212
+ end tell
1213
+ end tell`);
1214
+ return { success: true, data: { app: app2, position: posPart ? { x, y } : "unchanged", size: sizePart ? { width: w, height: h } : "unchanged" } };
1215
+ }
1216
+ // ── Apple Notes ─────────────────────────────────────────
1217
+ case "sys_notes_list": {
1218
+ const limit = Number(params.limit) || 10;
1219
+ const raw5 = await runAppleScript(`
1220
+ tell application "Notes"
1221
+ set output to ""
1222
+ set noteList to notes 1 thru ${limit}
1223
+ repeat with n in noteList
1224
+ set output to output & id of n & " | " & name of n & " | " & (modification date of n as string) & linefeed
1225
+ end repeat
1226
+ return output
1227
+ end tell`);
1228
+ const notes = raw5.split("\n").filter(Boolean).map((line) => {
1229
+ const [id, name, date] = line.split(" | ");
1230
+ return { id: id?.trim(), name: name?.trim(), modified: date?.trim() };
1231
+ });
1232
+ return { success: true, data: { notes, count: notes.length } };
1233
+ }
1234
+ case "sys_notes_create": {
1235
+ const noteTitle = params.title || "Untitled";
1236
+ const noteBody = params.body || "";
1237
+ const folder = params.folder || "Notes";
1238
+ await runAppleScript(`
1239
+ tell application "Notes"
1240
+ tell folder "${folder.replace(/"/g, '\\"')}"
1241
+ make new note with properties {name:"${noteTitle.replace(/"/g, '\\"')}", body:"${noteBody.replace(/"/g, '\\"').replace(/\n/g, "\\n")}"}
1242
+ end tell
1243
+ end tell`);
1244
+ return { success: true, data: { created: noteTitle, folder } };
1245
+ }
1246
+ // ── Contacts ────────────────────────────────────────────
1247
+ case "sys_contacts_search": {
1248
+ const query2 = params.query;
1249
+ if (!query2) return { success: false, error: "Missing search query" };
1250
+ const raw6 = await runAppleScript(`
1251
+ tell application "Contacts"
1252
+ set matches to every person whose name contains "${query2.replace(/"/g, '\\"')}"
1253
+ set output to ""
1254
+ repeat with p in matches
1255
+ set pName to name of p
1256
+ set pEmail to ""
1257
+ set pPhone to ""
1258
+ try
1259
+ set pEmail to value of first email of p
1260
+ end try
1261
+ try
1262
+ set pPhone to value of first phone of p
1263
+ end try
1264
+ set output to output & pName & " | " & pEmail & " | " & pPhone & linefeed
1265
+ end repeat
1266
+ return output
1267
+ end tell`);
1268
+ const contacts = raw6.split("\n").filter(Boolean).map((line) => {
1269
+ const [name, email, phone] = line.split(" | ");
1270
+ return { name: name?.trim(), email: email?.trim(), phone: phone?.trim() };
1271
+ });
1272
+ return { success: true, data: { contacts, count: contacts.length, query: query2 } };
1273
+ }
1274
+ // ── OCR (Vision framework) ──────────────────────────────
1275
+ case "sys_ocr": {
1276
+ const imgPath = params.path;
1277
+ if (!imgPath) return { success: false, error: "Missing image path" };
1278
+ const fullImg = imgPath.startsWith("/tmp/") ? imgPath : safePath(imgPath);
1279
+ if (!fullImg) return { success: false, error: "Access denied" };
1280
+ if (!existsSync(fullImg)) return { success: false, error: `Image not found: ${imgPath}` };
1281
+ const swiftOcr = `
1282
+ import Foundation
1283
+ import Vision
1284
+
1285
+ let url = URL(fileURLWithPath: "${fullImg}")
1286
+ guard let imgData = try? Data(contentsOf: url),
1287
+ let img = NSImage(data: imgData),
1288
+ let cgImg = img.cgImage(forProposedRect: nil, context: nil, hints: nil) else {
1289
+ print("ERROR: Cannot load image")
1290
+ Foundation.exit(1)
1291
+ }
1292
+ let request = VNRecognizeTextRequest()
1293
+ request.recognitionLevel = .accurate
1294
+ request.recognitionLanguages = ["en", "pt", "es", "fr", "de", "it"]
1295
+ let handler = VNImageRequestHandler(cgImage: cgImg)
1296
+ try handler.perform([request])
1297
+ let results = request.results ?? []
1298
+ let text = results.compactMap { $0.topCandidates(1).first?.string }.joined(separator: "\\n")
1299
+ print(text)`;
1300
+ try {
1301
+ const ocrText = await runSwift(swiftOcr, 3e4);
1302
+ return { success: true, data: { text: ocrText.slice(0, 1e4), length: ocrText.length, path: imgPath } };
1303
+ } catch (err) {
1304
+ return { success: false, error: `OCR failed: ${err.message}` };
1305
+ }
1306
+ }
764
1307
  default:
765
1308
  return { success: false, error: `Unknown command: ${command}` };
766
1309
  }
@@ -849,10 +1392,14 @@ function scheduleReconnect() {
849
1392
  }
850
1393
  console.log("");
851
1394
  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");
852
- console.log(" \u2551 \u{1FAC0} Pulso Mac Companion v0.1.8 \u2551");
1395
+ console.log(" \u2551 \u{1FAC0} Pulso Mac Companion v0.1.9 \u2551");
853
1396
  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");
854
1397
  console.log("");
855
- connect();
1398
+ setupPermissions().then(() => {
1399
+ connect();
1400
+ }).catch(() => {
1401
+ connect();
1402
+ });
856
1403
  process.on("SIGINT", () => {
857
1404
  console.log("\n\u{1F44B} Shutting down Pulso Companion...");
858
1405
  ws?.close(1e3, "User shutdown");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pulso/companion",
3
- "version": "0.1.8",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "description": "Pulso Companion — gives your AI agent real control over your computer",
6
6
  "bin": {