@pulso/companion 0.1.9 → 0.2.1
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 +445 -6
- 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 { join, 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) {
|
|
@@ -877,6 +877,433 @@ print("\\(x),\\(y)")`;
|
|
|
877
877
|
const result = await runShell(`shortcuts run "${name.replace(/"/g, '\\"')}" ${inputFlag}`, 3e4);
|
|
878
878
|
return { success: true, data: { shortcut: name, output: result || "Shortcut executed" } };
|
|
879
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
|
+
}
|
|
880
1307
|
default:
|
|
881
1308
|
return { success: false, error: `Unknown command: ${command}` };
|
|
882
1309
|
}
|
|
@@ -888,11 +1315,13 @@ var ws = null;
|
|
|
888
1315
|
var reconnectTimer = null;
|
|
889
1316
|
var heartbeatTimer = null;
|
|
890
1317
|
var HEARTBEAT_INTERVAL = 3e4;
|
|
1318
|
+
var reconnectAttempts = 0;
|
|
891
1319
|
function connect() {
|
|
892
1320
|
console.log("\u{1F50C} Connecting to Pulso...");
|
|
893
1321
|
console.log(` ${WS_URL.replace(/token=.*/, "token=***")}`);
|
|
894
1322
|
ws = new WebSocket(WS_URL);
|
|
895
1323
|
ws.on("open", () => {
|
|
1324
|
+
reconnectAttempts = 0;
|
|
896
1325
|
console.log("\u2705 Connected to Pulso!");
|
|
897
1326
|
console.log(`\u{1F5A5}\uFE0F Companion is active \u2014 ${ACCESS_LEVEL === "full" ? "full device access" : "sandboxed mode"}`);
|
|
898
1327
|
console.log("");
|
|
@@ -943,12 +1372,20 @@ function connect() {
|
|
|
943
1372
|
}
|
|
944
1373
|
});
|
|
945
1374
|
ws.on("close", (code, reason) => {
|
|
1375
|
+
const reasonStr = reason.toString() || "unknown";
|
|
946
1376
|
console.log(`
|
|
947
|
-
\u{1F50C} Disconnected (${code}: ${
|
|
1377
|
+
\u{1F50C} Disconnected (${code}: ${reasonStr})`);
|
|
948
1378
|
if (heartbeatTimer) {
|
|
949
1379
|
clearInterval(heartbeatTimer);
|
|
950
1380
|
heartbeatTimer = null;
|
|
951
1381
|
}
|
|
1382
|
+
if (reasonStr === "New connection from same user") {
|
|
1383
|
+
console.log("\n\u26A0\uFE0F Another Pulso Companion instance is already connected.");
|
|
1384
|
+
console.log(" This instance will exit. Only one companion per account is supported.");
|
|
1385
|
+
console.log(" If this is unexpected, close other terminals running Pulso Companion.\n");
|
|
1386
|
+
process.exit(0);
|
|
1387
|
+
return;
|
|
1388
|
+
}
|
|
952
1389
|
scheduleReconnect();
|
|
953
1390
|
});
|
|
954
1391
|
ws.on("error", (err) => {
|
|
@@ -957,15 +1394,17 @@ function connect() {
|
|
|
957
1394
|
}
|
|
958
1395
|
function scheduleReconnect() {
|
|
959
1396
|
if (reconnectTimer) return;
|
|
960
|
-
|
|
1397
|
+
reconnectAttempts++;
|
|
1398
|
+
const delay = Math.min(RECONNECT_DELAY * Math.pow(2, reconnectAttempts - 1), 6e4);
|
|
1399
|
+
console.log(` Reconnecting in ${(delay / 1e3).toFixed(0)}s... (attempt ${reconnectAttempts})`);
|
|
961
1400
|
reconnectTimer = setTimeout(() => {
|
|
962
1401
|
reconnectTimer = null;
|
|
963
1402
|
connect();
|
|
964
|
-
},
|
|
1403
|
+
}, delay);
|
|
965
1404
|
}
|
|
966
1405
|
console.log("");
|
|
967
1406
|
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");
|
|
968
|
-
console.log(" \u2551 \u{1FAC0} Pulso Mac Companion v0.1
|
|
1407
|
+
console.log(" \u2551 \u{1FAC0} Pulso Mac Companion v0.2.1 \u2551");
|
|
969
1408
|
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");
|
|
970
1409
|
console.log("");
|
|
971
1410
|
setupPermissions().then(() => {
|