@pulso/companion 0.1.9 → 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 +429 -2
  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 { 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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pulso/companion",
3
- "version": "0.1.9",
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": {