@pulso/companion 0.2.3 → 0.3.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 +656 -24
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -681,9 +681,23 @@ async function handleCommand(command, params) {
681
681
  const ts = Date.now();
682
682
  const pngPath = `/tmp/pulso-ss-${ts}.png`;
683
683
  const jpgPath = `/tmp/pulso-ss-${ts}.jpg`;
684
- await runShell(`screencapture -x ${pngPath}`, 15e3);
684
+ try {
685
+ await runShell(`screencapture -x ${pngPath}`, 15e3);
686
+ } catch (ssErr) {
687
+ const msg = ssErr.message || "";
688
+ if (msg.includes("could not create image") || msg.includes("display")) {
689
+ return {
690
+ success: false,
691
+ error: "Screen Recording permission required. Go to System Settings \u2192 Privacy & Security \u2192 Screen Recording \u2192 enable Terminal (or your terminal app)."
692
+ };
693
+ }
694
+ return { success: false, error: `Screenshot failed: ${msg}` };
695
+ }
685
696
  if (!existsSync(pngPath))
686
- return { success: false, error: "Screenshot failed" };
697
+ return {
698
+ success: false,
699
+ error: "Screenshot failed \u2014 Screen Recording permission may be needed. Go to System Settings \u2192 Privacy & Security \u2192 Screen Recording."
700
+ };
687
701
  try {
688
702
  await runShell(
689
703
  `sips --resampleWidth 1280 --setProperty format jpeg --setProperty formatOptions 60 ${pngPath} --out ${jpgPath}`,
@@ -1259,9 +1273,11 @@ print("\\(x),\\(y)")`;
1259
1273
  // ── Calendar ────────────────────────────────────────────
1260
1274
  case "sys_calendar_list": {
1261
1275
  const days = Number(params.days) || 7;
1262
- const startDate = /* @__PURE__ */ new Date();
1263
- const endDate = new Date(Date.now() + days * 864e5);
1264
- const fmt = (d) => `date "${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}"`;
1276
+ try {
1277
+ await runAppleScript(`tell application "Calendar" to launch`);
1278
+ await new Promise((r) => setTimeout(r, 500));
1279
+ } catch {
1280
+ }
1265
1281
  const script = `
1266
1282
  set output to ""
1267
1283
  tell application "Calendar"
@@ -1297,20 +1313,47 @@ print("\\(x),\\(y)")`;
1297
1313
  const notes = params.notes || "";
1298
1314
  if (!summary || !startStr)
1299
1315
  return { success: false, error: "Missing summary or start time" };
1316
+ const parseDate = (iso) => {
1317
+ const d = new Date(iso);
1318
+ if (isNaN(d.getTime())) return null;
1319
+ return { y: d.getFullYear(), mo: d.getMonth() + 1, d: d.getDate(), h: d.getHours(), mi: d.getMinutes() };
1320
+ };
1321
+ const buildDateScript = (varName, iso) => {
1322
+ const p = parseDate(iso);
1323
+ if (!p) return "";
1324
+ return `set ${varName} to current date
1325
+ set year of ${varName} to ${p.y}
1326
+ set month of ${varName} to ${p.mo}
1327
+ set day of ${varName} to ${p.d}
1328
+ set hours of ${varName} to ${p.h}
1329
+ set minutes of ${varName} to ${p.mi}
1330
+ set seconds of ${varName} to 0`;
1331
+ };
1332
+ const startDateScript = buildDateScript("startD", startStr);
1333
+ if (!startDateScript)
1334
+ return { success: false, error: `Invalid start date: ${startStr}` };
1335
+ const endDateScript = endStr ? buildDateScript("endD", endStr) : "";
1300
1336
  const calTarget = calendar ? `calendar "${calendar.replace(/"/g, '\\"')}"` : "default calendar";
1301
- const endPart = endStr ? `set end date of newEvent to date "${endStr}"` : "";
1302
- const notesPart = notes ? `set description of newEvent to "${notes.replace(/"/g, '\\"')}"` : "";
1337
+ const notesPart = notes ? `
1338
+ set description of newEvent to "${notes.replace(/"/g, '\\"')}"` : "";
1339
+ const endPart = endDateScript ? `
1340
+ set end date of newEvent to endD` : "";
1341
+ try {
1342
+ await runAppleScript(`tell application "Calendar" to launch`);
1343
+ await new Promise((r) => setTimeout(r, 500));
1344
+ } catch {
1345
+ }
1303
1346
  await runAppleScript(`
1304
- tell application "Calendar"
1305
- tell ${calTarget}
1306
- set newEvent to make new event with properties {summary:"${summary.replace(/"/g, '\\"')}", start date:date "${startStr}"}
1307
- ${endPart}
1308
- ${notesPart}
1309
- end tell
1310
- end tell`);
1347
+ ${startDateScript}
1348
+ ${endDateScript}
1349
+ tell application "Calendar"
1350
+ tell ${calTarget}
1351
+ set newEvent to make new event with properties {summary:"${summary.replace(/"/g, '\\"')}", start date:startD}${endPart}${notesPart}
1352
+ end tell
1353
+ end tell`);
1311
1354
  return {
1312
1355
  success: true,
1313
- data: { created: summary, start: startStr, end: endStr || "auto" }
1356
+ data: { created: summary, start: startStr, end: endStr || "1 hour" }
1314
1357
  };
1315
1358
  }
1316
1359
  // ── Reminders ───────────────────────────────────────────
@@ -1359,16 +1402,39 @@ print("\\(x),\\(y)")`;
1359
1402
  case "sys_reminder_create": {
1360
1403
  const reminderName = params.name;
1361
1404
  const dueDate = params.due;
1362
- const listName2 = params.list || "Reminders";
1405
+ let listName2 = params.list || "";
1363
1406
  if (!reminderName)
1364
1407
  return { success: false, error: "Missing reminder name" };
1365
- const duePart = dueDate ? `, due date:date "${dueDate}"` : "";
1366
- await runAppleScript(`
1367
- tell application "Reminders"
1368
- tell list "${listName2.replace(/"/g, '\\"')}"
1369
- make new reminder with properties {name:"${reminderName.replace(/"/g, '\\"')}"${duePart}}
1370
- end tell
1371
- end tell`);
1408
+ if (!listName2) {
1409
+ try {
1410
+ listName2 = (await runAppleScript(
1411
+ `tell application "Reminders" to return name of default list`
1412
+ )).trim();
1413
+ } catch {
1414
+ listName2 = "Reminders";
1415
+ }
1416
+ }
1417
+ let duePart = "";
1418
+ if (dueDate) {
1419
+ const d = new Date(dueDate);
1420
+ if (!isNaN(d.getTime())) {
1421
+ duePart = `
1422
+ set dueD to current date
1423
+ set year of dueD to ${d.getFullYear()}
1424
+ set month of dueD to ${d.getMonth() + 1}
1425
+ set day of dueD to ${d.getDate()}
1426
+ set hours of dueD to ${d.getHours()}
1427
+ set minutes of dueD to ${d.getMinutes()}
1428
+ set seconds of dueD to 0`;
1429
+ }
1430
+ }
1431
+ const dueProperty = duePart ? `, due date:dueD` : "";
1432
+ await runAppleScript(`${duePart}
1433
+ tell application "Reminders"
1434
+ tell list "${listName2.replace(/"/g, '\\"')}"
1435
+ make new reminder with properties {name:"${reminderName.replace(/"/g, '\\"')}"${dueProperty}}
1436
+ end tell
1437
+ end tell`);
1372
1438
  return {
1373
1439
  success: true,
1374
1440
  data: {
@@ -2101,6 +2167,572 @@ print(text)`;
2101
2167
  );
2102
2168
  return { success: true, data: { room, ...res } };
2103
2169
  }
2170
+ // ── NEW: System Settings ──────────────────────────────
2171
+ case "sys_open_settings": {
2172
+ const pane = params.pane || "";
2173
+ const paneMap = {
2174
+ general: "com.apple.preference.general",
2175
+ wifi: "com.apple.preference.network",
2176
+ network: "com.apple.preference.network",
2177
+ bluetooth: "com.apple.preference.bluetooth",
2178
+ sound: "com.apple.preference.sound",
2179
+ display: "com.apple.preference.displays",
2180
+ wallpaper: "com.apple.preference.desktopscreeneffect",
2181
+ notifications: "com.apple.preference.notifications",
2182
+ focus: "com.apple.preference.focus",
2183
+ keyboard: "com.apple.preference.keyboard",
2184
+ trackpad: "com.apple.preference.trackpad",
2185
+ mouse: "com.apple.preference.mouse",
2186
+ printers: "com.apple.preference.printfax",
2187
+ battery: "com.apple.preference.battery",
2188
+ security: "com.apple.preference.security",
2189
+ privacy: "com.apple.preference.security",
2190
+ siri: "com.apple.preference.speech",
2191
+ spotlight: "com.apple.preference.spotlight",
2192
+ accessibility: "com.apple.preference.universalaccess",
2193
+ storage: "com.apple.preference.storage",
2194
+ timemachine: "com.apple.preference.timemachine",
2195
+ sharing: "com.apple.preference.sharing",
2196
+ users: "com.apple.preference.users",
2197
+ dock: "com.apple.preference.dock"
2198
+ };
2199
+ const paneId = paneMap[pane.toLowerCase()] || pane;
2200
+ if (paneId) {
2201
+ await runShell(
2202
+ `open "x-apple.systempreferences:com.apple.settings.${pane}" 2>/dev/null || open "x-apple.systempreferences:${paneId}" 2>/dev/null || open "System Preferences"`
2203
+ );
2204
+ } else {
2205
+ await runShell(`open "System Preferences" 2>/dev/null || open -a "System Settings"`);
2206
+ }
2207
+ return { success: true, data: { pane: pane || "main" } };
2208
+ }
2209
+ // ── NEW: Dark Mode ──────────────────────────────────────
2210
+ case "sys_dark_mode": {
2211
+ const action = params.action || "status";
2212
+ if (action === "status") {
2213
+ const result = await runAppleScript(
2214
+ `tell application "System Events" to tell appearance preferences to get dark mode`
2215
+ );
2216
+ return {
2217
+ success: true,
2218
+ data: { darkMode: result.trim() === "true" }
2219
+ };
2220
+ } else if (action === "toggle") {
2221
+ await runAppleScript(
2222
+ `tell application "System Events" to tell appearance preferences to set dark mode to not dark mode`
2223
+ );
2224
+ const result = await runAppleScript(
2225
+ `tell application "System Events" to tell appearance preferences to get dark mode`
2226
+ );
2227
+ return {
2228
+ success: true,
2229
+ data: { darkMode: result.trim() === "true" }
2230
+ };
2231
+ } else if (action === "on") {
2232
+ await runAppleScript(
2233
+ `tell application "System Events" to tell appearance preferences to set dark mode to true`
2234
+ );
2235
+ return { success: true, data: { darkMode: true } };
2236
+ } else if (action === "off") {
2237
+ await runAppleScript(
2238
+ `tell application "System Events" to tell appearance preferences to set dark mode to false`
2239
+ );
2240
+ return { success: true, data: { darkMode: false } };
2241
+ }
2242
+ return {
2243
+ success: false,
2244
+ error: "Invalid action. Use: status, toggle, on, off"
2245
+ };
2246
+ }
2247
+ // ── NEW: Spotlight Search ───────────────────────────────
2248
+ case "sys_spotlight_search": {
2249
+ const query = params.query;
2250
+ if (!query) return { success: false, error: "Missing query" };
2251
+ const dir = params.directory;
2252
+ const kind = params.kind;
2253
+ const limit = params.limit || 20;
2254
+ let cmd = `mdfind`;
2255
+ if (dir) cmd += ` -onlyin "${dir}"`;
2256
+ if (kind) {
2257
+ cmd += ` 'kMDItemContentType == "*${kind}*"cd && kMDItemDisplayName == "*${query}*"cd'`;
2258
+ } else {
2259
+ cmd += ` "${query}"`;
2260
+ }
2261
+ cmd += ` | head -${limit}`;
2262
+ const result = await runShell(cmd);
2263
+ const files = result.trim().split("\n").filter((f) => f);
2264
+ return { success: true, data: { query, count: files.length, files } };
2265
+ }
2266
+ // ── NEW: Process Management ─────────────────────────────
2267
+ case "sys_process_list": {
2268
+ const result = await runShell(
2269
+ `ps aux --sort=-%mem | head -25 | awk '{printf "%s|%s|%s|%s|", $1, $2, $3, $4; for(i=11;i<=NF;i++) printf "%s ", $i; print ""}'`
2270
+ );
2271
+ const lines = result.trim().split("\n").slice(1);
2272
+ const processes = lines.map((l) => {
2273
+ const [user, pid, cpu, mem, ...cmdParts] = l.split("|");
2274
+ return {
2275
+ user: user?.trim(),
2276
+ pid: pid?.trim(),
2277
+ cpu: cpu?.trim() + "%",
2278
+ mem: mem?.trim() + "%",
2279
+ command: cmdParts.join("|").trim()
2280
+ };
2281
+ });
2282
+ return { success: true, data: { processes } };
2283
+ }
2284
+ case "sys_process_kill": {
2285
+ const pid = params.pid;
2286
+ const name = params.name;
2287
+ if (!pid && !name)
2288
+ return { success: false, error: "Provide pid or name" };
2289
+ if (pid) {
2290
+ await runShell(`kill ${pid}`);
2291
+ return { success: true, data: { killed: pid } };
2292
+ }
2293
+ const blocked = [
2294
+ "Finder",
2295
+ "loginwindow",
2296
+ "WindowServer",
2297
+ "kernel_task",
2298
+ "launchd"
2299
+ ];
2300
+ if (blocked.some((b) => name.toLowerCase().includes(b.toLowerCase())))
2301
+ return {
2302
+ success: false,
2303
+ error: `Cannot kill system process: ${name}`
2304
+ };
2305
+ await runShell(`pkill -f "${name}"`);
2306
+ return { success: true, data: { killed: name } };
2307
+ }
2308
+ // ── NEW: WiFi Info ──────────────────────────────────────
2309
+ case "sys_wifi_info": {
2310
+ try {
2311
+ const ssid = await runShell(
2312
+ `networksetup -getairportnetwork en0 2>/dev/null | sed 's/Current Wi-Fi Network: //'`
2313
+ );
2314
+ const ip = await runShell(
2315
+ `ipconfig getifaddr en0 2>/dev/null || echo "Not connected"`
2316
+ );
2317
+ const dns = await runShell(
2318
+ `networksetup -getdnsservers Wi-Fi 2>/dev/null || echo "Auto"`
2319
+ );
2320
+ return {
2321
+ success: true,
2322
+ data: {
2323
+ ssid: ssid.trim(),
2324
+ ip: ip.trim(),
2325
+ dns: dns.trim().split("\n")
2326
+ }
2327
+ };
2328
+ } catch {
2329
+ return { success: true, data: { ssid: "Not connected" } };
2330
+ }
2331
+ }
2332
+ // ── NEW: Bluetooth ──────────────────────────────────────
2333
+ case "sys_bluetooth": {
2334
+ const btAction = params.action || "status";
2335
+ try {
2336
+ if (btAction === "status") {
2337
+ try {
2338
+ const power = await runShell(`blueutil --power 2>/dev/null`);
2339
+ const paired = await runShell(
2340
+ `blueutil --paired --format json 2>/dev/null`
2341
+ );
2342
+ return {
2343
+ success: true,
2344
+ data: {
2345
+ power: power.trim() === "1",
2346
+ devices: JSON.parse(paired || "[]")
2347
+ }
2348
+ };
2349
+ } catch {
2350
+ const result = await runShell(
2351
+ `system_profiler SPBluetoothDataType -json 2>/dev/null`
2352
+ );
2353
+ return { success: true, data: JSON.parse(result) };
2354
+ }
2355
+ } else if (btAction === "on") {
2356
+ await runShell(`blueutil --power 1`);
2357
+ return { success: true, data: { power: true } };
2358
+ } else if (btAction === "off") {
2359
+ await runShell(`blueutil --power 0`);
2360
+ return { success: true, data: { power: false } };
2361
+ } else if (btAction === "connect") {
2362
+ const address = params.address;
2363
+ if (!address) return { success: false, error: "Missing address" };
2364
+ await runShell(`blueutil --connect "${address}"`);
2365
+ return { success: true, data: { connected: address } };
2366
+ } else if (btAction === "disconnect") {
2367
+ const address = params.address;
2368
+ if (!address) return { success: false, error: "Missing address" };
2369
+ await runShell(`blueutil --disconnect "${address}"`);
2370
+ return { success: true, data: { disconnected: address } };
2371
+ }
2372
+ } catch (e) {
2373
+ return {
2374
+ success: false,
2375
+ error: `Bluetooth requires blueutil: brew install blueutil. ${e.message}`
2376
+ };
2377
+ }
2378
+ return { success: false, error: "Unknown action" };
2379
+ }
2380
+ // ── NEW: Audio Devices ──────────────────────────────────
2381
+ case "sys_audio_devices": {
2382
+ const audioAction = params.action || "list";
2383
+ if (audioAction === "list") {
2384
+ const result = await runAppleScript(`
2385
+ set output to ""
2386
+ tell application "System Events"
2387
+ set audioOut to name of every audio output device
2388
+ repeat with d in audioOut
2389
+ set output to output & d & linefeed
2390
+ end repeat
2391
+ end tell
2392
+ return output`);
2393
+ if (!result.trim()) {
2394
+ const spResult = await runShell(
2395
+ `system_profiler SPAudioDataType 2>/dev/null | grep "Device Name" | sed 's/.*: //'`
2396
+ );
2397
+ return {
2398
+ success: true,
2399
+ data: {
2400
+ devices: spResult.trim().split("\n").filter((d) => d)
2401
+ }
2402
+ };
2403
+ }
2404
+ return {
2405
+ success: true,
2406
+ data: {
2407
+ devices: result.trim().split("\n").filter((d) => d)
2408
+ }
2409
+ };
2410
+ } else if (audioAction === "switch") {
2411
+ const device = params.device;
2412
+ if (!device) return { success: false, error: "Missing device name" };
2413
+ try {
2414
+ await runShell(
2415
+ `SwitchAudioSource -s "${device}" 2>/dev/null`
2416
+ );
2417
+ return { success: true, data: { switched: device } };
2418
+ } catch {
2419
+ return {
2420
+ success: false,
2421
+ error: `Requires: brew install switchaudio-osx. Or manually change in System Settings.`
2422
+ };
2423
+ }
2424
+ }
2425
+ return { success: false, error: "Use action: list or switch" };
2426
+ }
2427
+ // ── NEW: Screen Lock ────────────────────────────────────
2428
+ case "sys_screen_lock": {
2429
+ await runShell(
2430
+ `pmset displaysleepnow 2>/dev/null || osascript -e 'tell application "System Events" to keystroke "q" using {control down, command down}'`
2431
+ );
2432
+ return { success: true, data: { locked: true } };
2433
+ }
2434
+ // ── NEW: Trash Operations ───────────────────────────────
2435
+ case "sys_trash": {
2436
+ const trashAction = params.action || "info";
2437
+ if (trashAction === "info") {
2438
+ const count = await runShell(
2439
+ `ls -1 ~/.Trash 2>/dev/null | wc -l`
2440
+ );
2441
+ const size = await runShell(
2442
+ `du -sh ~/.Trash 2>/dev/null | cut -f1`
2443
+ );
2444
+ return {
2445
+ success: true,
2446
+ data: {
2447
+ items: parseInt(count.trim()),
2448
+ size: size.trim()
2449
+ }
2450
+ };
2451
+ } else if (trashAction === "empty") {
2452
+ await runAppleScript(
2453
+ `tell application "Finder" to empty the trash`
2454
+ );
2455
+ return { success: true, data: { emptied: true } };
2456
+ }
2457
+ return { success: false, error: "Use action: info or empty" };
2458
+ }
2459
+ // ── NEW: Archive/Zip Operations ─────────────────────────
2460
+ case "sys_archive": {
2461
+ const archiveAction = params.action || "create";
2462
+ const source = params.source;
2463
+ const destination = params.destination;
2464
+ if (!source) return { success: false, error: "Missing source path" };
2465
+ const safeSrc = safePath(source);
2466
+ if (!safeSrc) return { success: false, error: "Path not allowed" };
2467
+ if (archiveAction === "create") {
2468
+ const dest = destination || `${safeSrc}.zip`;
2469
+ await runShell(`ditto -c -k --keepParent "${safeSrc}" "${dest}"`);
2470
+ return { success: true, data: { archive: dest } };
2471
+ } else if (archiveAction === "extract") {
2472
+ const dest = destination || safeSrc.replace(/\.(zip|tar\.gz|tgz)$/i, "");
2473
+ if (safeSrc.endsWith(".zip")) {
2474
+ await runShell(`ditto -x -k "${safeSrc}" "${dest}"`);
2475
+ } else {
2476
+ await runShell(`tar xzf "${safeSrc}" -C "${dest}"`);
2477
+ }
2478
+ return { success: true, data: { extracted: dest } };
2479
+ }
2480
+ return { success: false, error: "Use action: create or extract" };
2481
+ }
2482
+ // ── NEW: Disk Info ──────────────────────────────────────
2483
+ case "sys_disk_info": {
2484
+ const df = await runShell(`df -h / | tail -1`);
2485
+ const parts = df.trim().split(/\s+/);
2486
+ const volumes = await runShell(
2487
+ `ls -1 /Volumes 2>/dev/null`
2488
+ );
2489
+ return {
2490
+ success: true,
2491
+ data: {
2492
+ filesystem: parts[0],
2493
+ total: parts[1],
2494
+ used: parts[2],
2495
+ available: parts[3],
2496
+ usedPercent: parts[4],
2497
+ mountedVolumes: volumes.trim().split("\n").filter((v) => v)
2498
+ }
2499
+ };
2500
+ }
2501
+ // ── NEW: Focus Mode ─────────────────────────────────────
2502
+ case "sys_focus_mode": {
2503
+ const focusAction = params.action || "status";
2504
+ if (focusAction === "status") {
2505
+ const result = await runShell(
2506
+ `defaults read com.apple.controlcenter "NSStatusItem Visible FocusModes" 2>/dev/null || echo "unknown"`
2507
+ );
2508
+ return {
2509
+ success: true,
2510
+ data: { focusActive: result.trim() !== "0" }
2511
+ };
2512
+ } else if (focusAction === "set") {
2513
+ const mode = params.mode || "Do Not Disturb";
2514
+ try {
2515
+ await runShell(`shortcuts run "Set ${mode}" 2>/dev/null`);
2516
+ return { success: true, data: { mode } };
2517
+ } catch {
2518
+ await runShell(
2519
+ `shortcuts run "Toggle Do Not Disturb" 2>/dev/null`
2520
+ );
2521
+ return {
2522
+ success: true,
2523
+ data: {
2524
+ mode: "DND toggled (custom Focus modes require a Shortcut named 'Set <ModeName>')"
2525
+ }
2526
+ };
2527
+ }
2528
+ }
2529
+ return { success: false, error: "Use action: status or set" };
2530
+ }
2531
+ // ── NEW: Login Items ────────────────────────────────────
2532
+ case "sys_login_items": {
2533
+ const liAction = params.action || "list";
2534
+ if (liAction === "list") {
2535
+ const result = await runAppleScript(
2536
+ `tell application "System Events" to get name of every login item`
2537
+ );
2538
+ return {
2539
+ success: true,
2540
+ data: {
2541
+ items: result.trim().split(", ").filter((i) => i)
2542
+ }
2543
+ };
2544
+ } else if (liAction === "add") {
2545
+ const app = params.app;
2546
+ if (!app) return { success: false, error: "Missing app name/path" };
2547
+ const appPath = app.includes("/") ? app : `/Applications/${app}.app`;
2548
+ await runAppleScript(
2549
+ `tell application "System Events" to make login item at end with properties {path:"${appPath}", hidden:false}`
2550
+ );
2551
+ return { success: true, data: { added: app } };
2552
+ } else if (liAction === "remove") {
2553
+ const app = params.app;
2554
+ if (!app) return { success: false, error: "Missing app name" };
2555
+ await runAppleScript(
2556
+ `tell application "System Events" to delete login item "${app}"`
2557
+ );
2558
+ return { success: true, data: { removed: app } };
2559
+ }
2560
+ return { success: false, error: "Use action: list, add, or remove" };
2561
+ }
2562
+ // ── NEW: Power Management ───────────────────────────────
2563
+ case "sys_power": {
2564
+ const pwrAction = params.action || "status";
2565
+ if (pwrAction === "status") {
2566
+ const batt = await runShell(`pmset -g batt`);
2567
+ const assertions = await runShell(
2568
+ `pmset -g assertions 2>/dev/null | head -5`
2569
+ );
2570
+ return {
2571
+ success: true,
2572
+ data: { battery: batt.trim(), assertions: assertions.trim() }
2573
+ };
2574
+ } else if (pwrAction === "caffeinate") {
2575
+ const duration = params.duration || 3600;
2576
+ exec(`caffeinate -d -t ${duration} &`);
2577
+ return {
2578
+ success: true,
2579
+ data: { caffeinated: true, seconds: duration }
2580
+ };
2581
+ } else if (pwrAction === "sleep") {
2582
+ await runShell(`pmset sleepnow`);
2583
+ return { success: true, data: { sleeping: true } };
2584
+ }
2585
+ return { success: false, error: "Use action: status, caffeinate, sleep" };
2586
+ }
2587
+ // ── NEW: Printer Management ─────────────────────────────
2588
+ case "sys_printer": {
2589
+ const prAction = params.action || "list";
2590
+ if (prAction === "list") {
2591
+ const result = await runShell(`lpstat -p 2>/dev/null || echo "No printers"`);
2592
+ return { success: true, data: { printers: result.trim() } };
2593
+ } else if (prAction === "print") {
2594
+ const file = params.file;
2595
+ if (!file) return { success: false, error: "Missing file path" };
2596
+ const safe = safePath(file);
2597
+ if (!safe) return { success: false, error: "Path not allowed" };
2598
+ const printer = params.printer;
2599
+ const cmd = printer ? `lp -d "${printer}" "${safe}"` : `lp "${safe}"`;
2600
+ await runShell(cmd);
2601
+ return { success: true, data: { printed: file } };
2602
+ }
2603
+ return { success: false, error: "Use action: list or print" };
2604
+ }
2605
+ // ── NEW: Finder Tags ────────────────────────────────────
2606
+ case "sys_finder_tags": {
2607
+ const tagAction = params.action || "get";
2608
+ const filePath = params.path;
2609
+ if (!filePath) return { success: false, error: "Missing path" };
2610
+ const safeFp = safePath(filePath);
2611
+ if (!safeFp) return { success: false, error: "Path not allowed" };
2612
+ if (tagAction === "get") {
2613
+ const result = await runShell(
2614
+ `mdls -name kMDItemUserTags "${safeFp}" | sed 's/kMDItemUserTags = //' | tr -d '()"'`
2615
+ );
2616
+ return {
2617
+ success: true,
2618
+ data: {
2619
+ tags: result.trim().split(",").map((t) => t.trim()).filter((t) => t)
2620
+ }
2621
+ };
2622
+ } else if (tagAction === "set") {
2623
+ const tags = params.tags;
2624
+ if (!tags || !tags.length)
2625
+ return { success: false, error: "Missing tags array" };
2626
+ const tagStr = tags.map((t) => `"${t}"`).join(", ");
2627
+ await runShell(
2628
+ `xattr -w com.apple.metadata:_kMDItemUserTags '(${tagStr})' "${safeFp}"`
2629
+ );
2630
+ return { success: true, data: { path: safeFp, tags } };
2631
+ }
2632
+ return { success: false, error: "Use action: get or set" };
2633
+ }
2634
+ // ── NEW: Quick Note ─────────────────────────────────────
2635
+ case "sys_quick_note": {
2636
+ const body = params.body || "";
2637
+ await runAppleScript(
2638
+ `tell application "Notes" to activate
2639
+ tell application "System Events"
2640
+ keystroke "n" using command down
2641
+ delay 0.3
2642
+ keystroke "${body.replace(/"/g, '\\"')}"
2643
+ end tell`
2644
+ );
2645
+ return { success: true, data: { created: true } };
2646
+ }
2647
+ // ── NEW: Terminal Tab ───────────────────────────────────
2648
+ case "sys_terminal": {
2649
+ const termAction = params.action || "new_tab";
2650
+ const termCmd = params.command || "";
2651
+ if (termAction === "new_tab") {
2652
+ await runAppleScript(`
2653
+ tell application "Terminal"
2654
+ activate
2655
+ do script "${termCmd.replace(/"/g, '\\"')}"
2656
+ end tell`);
2657
+ return { success: true, data: { opened: true, command: termCmd } };
2658
+ } else if (termAction === "new_window") {
2659
+ await runAppleScript(`
2660
+ tell application "Terminal"
2661
+ activate
2662
+ do script "${termCmd.replace(/"/g, '\\"')}" in (make new window)
2663
+ end tell`);
2664
+ return { success: true, data: { opened: true, command: termCmd } };
2665
+ }
2666
+ return { success: false, error: "Use action: new_tab or new_window" };
2667
+ }
2668
+ // ── NEW: PDF Operations ─────────────────────────────────
2669
+ case "sys_pdf": {
2670
+ const pdfAction = params.action || "info";
2671
+ const pdfPath = params.path;
2672
+ if (!pdfPath) return { success: false, error: "Missing path" };
2673
+ const safePdf = safePath(pdfPath);
2674
+ if (!safePdf) return { success: false, error: "Path not allowed" };
2675
+ if (pdfAction === "info") {
2676
+ const result = await runShell(
2677
+ `mdls -name kMDItemNumberOfPages -name kMDItemTitle -name kMDItemAuthors -name kMDItemFSSize "${safePdf}"`
2678
+ );
2679
+ return { success: true, data: { info: result.trim() } };
2680
+ } else if (pdfAction === "text") {
2681
+ try {
2682
+ const text2 = await runShell(
2683
+ `textutil -convert txt -stdout "${safePdf}" 2>/dev/null | head -500`
2684
+ );
2685
+ if (text2.trim())
2686
+ return { success: true, data: { text: text2.trim() } };
2687
+ } catch {
2688
+ }
2689
+ const text = await runShell(
2690
+ `python3 -c "
2691
+ import subprocess
2692
+ result = subprocess.run(['mdls', '-name', 'kMDItemTextContent', '${safePdf.replace(/'/g, "\\'")}'], capture_output=True, text=True)
2693
+ print(result.stdout[:5000])
2694
+ " 2>/dev/null || echo "Could not extract text. Install: brew install poppler (for pdftotext)"`
2695
+ );
2696
+ return { success: true, data: { text: text.trim() } };
2697
+ } else if (pdfAction === "merge") {
2698
+ const files = params.files;
2699
+ const output = params.output;
2700
+ if (!files || !output)
2701
+ return { success: false, error: "Missing files array and output" };
2702
+ const safePaths = files.map((f) => `"${safePath(f) || f}"`).join(" ");
2703
+ await runShell(
2704
+ `/System/Library/Automator/Combine\\ PDF\\ Pages.action/Contents/MacOS/join -o "${safePath(output) || output}" ${safePaths}`
2705
+ );
2706
+ return { success: true, data: { merged: output } };
2707
+ }
2708
+ return {
2709
+ success: false,
2710
+ error: "Use action: info, text, or merge"
2711
+ };
2712
+ }
2713
+ // ── NEW: Launchctl / Services ───────────────────────────
2714
+ case "sys_services": {
2715
+ const svcAction = params.action || "list";
2716
+ if (svcAction === "list") {
2717
+ const result = await runShell(
2718
+ `launchctl list 2>/dev/null | head -30`
2719
+ );
2720
+ return { success: true, data: { services: result.trim() } };
2721
+ } else if (svcAction === "start") {
2722
+ const name = params.name;
2723
+ if (!name) return { success: false, error: "Missing service name" };
2724
+ await runShell(`launchctl kickstart gui/$(id -u)/${name} 2>/dev/null`);
2725
+ return { success: true, data: { started: name } };
2726
+ } else if (svcAction === "stop") {
2727
+ const name = params.name;
2728
+ if (!name) return { success: false, error: "Missing service name" };
2729
+ await runShell(
2730
+ `launchctl kill SIGTERM gui/$(id -u)/${name} 2>/dev/null`
2731
+ );
2732
+ return { success: true, data: { stopped: name } };
2733
+ }
2734
+ return { success: false, error: "Use action: list, start, stop" };
2735
+ }
2104
2736
  default:
2105
2737
  return { success: false, error: `Unknown command: ${command}` };
2106
2738
  }
@@ -2520,7 +3152,7 @@ function writeString(view, offset, str) {
2520
3152
  }
2521
3153
  console.log("");
2522
3154
  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");
2523
- console.log(" \u2551 \u{1FAC0} Pulso Mac Companion v0.2.3 \u2551");
3155
+ console.log(" \u2551 \u{1FAC0} Pulso Mac Companion v0.3.0 \u2551");
2524
3156
  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");
2525
3157
  console.log("");
2526
3158
  setupPermissions().then(() => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pulso/companion",
3
- "version": "0.2.3",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "description": "Pulso Companion — gives your AI agent real control over your computer",
6
6
  "bin": {