@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.
- package/dist/index.js +551 -4
- 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.
|
|
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
|
-
|
|
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");
|