@pulso/companion 0.2.4 → 0.3.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 +616 -23
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -10,7 +10,8 @@ import {
|
|
|
10
10
|
readdirSync,
|
|
11
11
|
statSync,
|
|
12
12
|
copyFileSync,
|
|
13
|
-
renameSync
|
|
13
|
+
renameSync,
|
|
14
|
+
unlinkSync
|
|
14
15
|
} from "fs";
|
|
15
16
|
import { homedir } from "os";
|
|
16
17
|
import { join, resolve, basename, extname } from "path";
|
|
@@ -43,10 +44,16 @@ function safePath(relative) {
|
|
|
43
44
|
}
|
|
44
45
|
function runAppleScript(script) {
|
|
45
46
|
return new Promise((resolve2, reject) => {
|
|
47
|
+
const tmpPath = `/tmp/pulso-as-${Date.now()}-${Math.random().toString(36).slice(2, 6)}.scpt`;
|
|
48
|
+
writeFileSync(tmpPath, script, "utf-8");
|
|
46
49
|
exec(
|
|
47
|
-
`osascript
|
|
50
|
+
`osascript ${tmpPath}`,
|
|
48
51
|
{ timeout: 15e3 },
|
|
49
52
|
(err, stdout, stderr) => {
|
|
53
|
+
try {
|
|
54
|
+
unlinkSync(tmpPath);
|
|
55
|
+
} catch {
|
|
56
|
+
}
|
|
50
57
|
if (err) reject(new Error(stderr || err.message));
|
|
51
58
|
else resolve2(stdout.trim());
|
|
52
59
|
}
|
|
@@ -682,13 +689,13 @@ async function handleCommand(command, params) {
|
|
|
682
689
|
const pngPath = `/tmp/pulso-ss-${ts}.png`;
|
|
683
690
|
const jpgPath = `/tmp/pulso-ss-${ts}.jpg`;
|
|
684
691
|
try {
|
|
685
|
-
await runShell(`screencapture -x ${pngPath}`, 15e3);
|
|
692
|
+
await runShell(`screencapture -C -x -D1 ${pngPath}`, 15e3);
|
|
686
693
|
} catch (ssErr) {
|
|
687
694
|
const msg = ssErr.message || "";
|
|
688
695
|
if (msg.includes("could not create image") || msg.includes("display")) {
|
|
689
696
|
return {
|
|
690
697
|
success: false,
|
|
691
|
-
error: "Screen Recording permission required. Go to System Settings \u2192 Privacy & Security \u2192 Screen Recording \u2192 enable
|
|
698
|
+
error: "Screen Recording permission required. Go to System Settings \u2192 Privacy & Security \u2192 Screen Recording \u2192 enable your terminal app (Terminal, iTerm, etc). Then restart the companion."
|
|
692
699
|
};
|
|
693
700
|
}
|
|
694
701
|
return { success: false, error: `Screenshot failed: ${msg}` };
|
|
@@ -696,7 +703,7 @@ async function handleCommand(command, params) {
|
|
|
696
703
|
if (!existsSync(pngPath))
|
|
697
704
|
return {
|
|
698
705
|
success: false,
|
|
699
|
-
error: "Screenshot failed \u2014 Screen Recording permission
|
|
706
|
+
error: "Screenshot failed \u2014 Screen Recording permission needed. System Settings \u2192 Privacy & Security \u2192 Screen Recording \u2192 enable your terminal app, then restart companion."
|
|
700
707
|
};
|
|
701
708
|
try {
|
|
702
709
|
await runShell(
|
|
@@ -705,7 +712,10 @@ async function handleCommand(command, params) {
|
|
|
705
712
|
);
|
|
706
713
|
} catch {
|
|
707
714
|
const buf2 = readFileSync(pngPath);
|
|
708
|
-
|
|
715
|
+
try {
|
|
716
|
+
unlinkSync(pngPath);
|
|
717
|
+
} catch {
|
|
718
|
+
}
|
|
709
719
|
return {
|
|
710
720
|
success: true,
|
|
711
721
|
data: {
|
|
@@ -717,7 +727,14 @@ async function handleCommand(command, params) {
|
|
|
717
727
|
}
|
|
718
728
|
const buf = readFileSync(jpgPath);
|
|
719
729
|
const base64 = buf.toString("base64");
|
|
720
|
-
|
|
730
|
+
try {
|
|
731
|
+
unlinkSync(pngPath);
|
|
732
|
+
} catch {
|
|
733
|
+
}
|
|
734
|
+
try {
|
|
735
|
+
unlinkSync(jpgPath);
|
|
736
|
+
} catch {
|
|
737
|
+
}
|
|
721
738
|
let screenSize = "unknown";
|
|
722
739
|
try {
|
|
723
740
|
screenSize = await runShell(
|
|
@@ -1159,21 +1176,33 @@ print("\\(x),\\(y)")`;
|
|
|
1159
1176
|
const browser = params.browser || "Google Chrome";
|
|
1160
1177
|
if (!js) return { success: false, error: "Missing JavaScript code" };
|
|
1161
1178
|
try {
|
|
1179
|
+
const tmpJs = `/tmp/pulso-js-${Date.now()}.js`;
|
|
1180
|
+
writeFileSync(tmpJs, js, "utf-8");
|
|
1162
1181
|
let result;
|
|
1163
1182
|
if (browser === "Safari") {
|
|
1164
1183
|
result = await runAppleScript(
|
|
1165
|
-
`
|
|
1184
|
+
`set jsCode to read POSIX file "${tmpJs}" as \xABclass utf8\xBB
|
|
1185
|
+
tell application "Safari"
|
|
1186
|
+
do JavaScript jsCode in document 1
|
|
1187
|
+
end tell`
|
|
1166
1188
|
);
|
|
1167
1189
|
} else {
|
|
1168
1190
|
result = await runAppleScript(
|
|
1169
|
-
`
|
|
1191
|
+
`set jsCode to read POSIX file "${tmpJs}" as \xABclass utf8\xBB
|
|
1192
|
+
tell application "${browser.replace(/"/g, '\\"')}"
|
|
1193
|
+
execute javascript jsCode in active tab of front window
|
|
1194
|
+
end tell`
|
|
1170
1195
|
);
|
|
1171
1196
|
}
|
|
1172
|
-
|
|
1197
|
+
try {
|
|
1198
|
+
unlinkSync(tmpJs);
|
|
1199
|
+
} catch {
|
|
1200
|
+
}
|
|
1201
|
+
return { success: true, data: { result: (result || "").slice(0, 5e3) } };
|
|
1173
1202
|
} catch (err) {
|
|
1174
1203
|
return {
|
|
1175
1204
|
success: false,
|
|
1176
|
-
error: `JS execution failed (enable 'Allow JavaScript from Apple Events' in browser): ${err.message}`
|
|
1205
|
+
error: `JS execution failed (enable 'Allow JavaScript from Apple Events' in browser Dev menu): ${err.message}`
|
|
1177
1206
|
};
|
|
1178
1207
|
}
|
|
1179
1208
|
}
|
|
@@ -1322,12 +1351,14 @@ print("\\(x),\\(y)")`;
|
|
|
1322
1351
|
const p = parseDate(iso);
|
|
1323
1352
|
if (!p) return "";
|
|
1324
1353
|
return `set ${varName} to current date
|
|
1325
|
-
|
|
1326
|
-
set
|
|
1327
|
-
set
|
|
1328
|
-
set
|
|
1329
|
-
set
|
|
1330
|
-
set
|
|
1354
|
+
tell ${varName}
|
|
1355
|
+
set its year to ${p.y}
|
|
1356
|
+
set its month to ${p.mo}
|
|
1357
|
+
set its day to ${p.d}
|
|
1358
|
+
set its hours to ${p.h}
|
|
1359
|
+
set its minutes to ${p.mi}
|
|
1360
|
+
set its seconds to 0
|
|
1361
|
+
end tell`;
|
|
1331
1362
|
};
|
|
1332
1363
|
const startDateScript = buildDateScript("startD", startStr);
|
|
1333
1364
|
if (!startDateScript)
|
|
@@ -1726,11 +1757,7 @@ end tell`);
|
|
|
1726
1757
|
case "sys_window_focus": {
|
|
1727
1758
|
const appName = params.app;
|
|
1728
1759
|
if (!appName) return { success: false, error: "Missing app name" };
|
|
1729
|
-
await runAppleScript(`
|
|
1730
|
-
tell application "${appName.replace(/"/g, '\\"')}"
|
|
1731
|
-
activate
|
|
1732
|
-
set frontmost to true
|
|
1733
|
-
end tell`);
|
|
1760
|
+
await runAppleScript(`tell application "${appName.replace(/"/g, '\\"')}" to activate`);
|
|
1734
1761
|
return { success: true, data: { focused: appName } };
|
|
1735
1762
|
}
|
|
1736
1763
|
case "sys_window_resize": {
|
|
@@ -2167,6 +2194,572 @@ print(text)`;
|
|
|
2167
2194
|
);
|
|
2168
2195
|
return { success: true, data: { room, ...res } };
|
|
2169
2196
|
}
|
|
2197
|
+
// ── NEW: System Settings ──────────────────────────────
|
|
2198
|
+
case "sys_open_settings": {
|
|
2199
|
+
const pane = params.pane || "";
|
|
2200
|
+
const paneMap = {
|
|
2201
|
+
general: "com.apple.preference.general",
|
|
2202
|
+
wifi: "com.apple.preference.network",
|
|
2203
|
+
network: "com.apple.preference.network",
|
|
2204
|
+
bluetooth: "com.apple.preference.bluetooth",
|
|
2205
|
+
sound: "com.apple.preference.sound",
|
|
2206
|
+
display: "com.apple.preference.displays",
|
|
2207
|
+
wallpaper: "com.apple.preference.desktopscreeneffect",
|
|
2208
|
+
notifications: "com.apple.preference.notifications",
|
|
2209
|
+
focus: "com.apple.preference.focus",
|
|
2210
|
+
keyboard: "com.apple.preference.keyboard",
|
|
2211
|
+
trackpad: "com.apple.preference.trackpad",
|
|
2212
|
+
mouse: "com.apple.preference.mouse",
|
|
2213
|
+
printers: "com.apple.preference.printfax",
|
|
2214
|
+
battery: "com.apple.preference.battery",
|
|
2215
|
+
security: "com.apple.preference.security",
|
|
2216
|
+
privacy: "com.apple.preference.security",
|
|
2217
|
+
siri: "com.apple.preference.speech",
|
|
2218
|
+
spotlight: "com.apple.preference.spotlight",
|
|
2219
|
+
accessibility: "com.apple.preference.universalaccess",
|
|
2220
|
+
storage: "com.apple.preference.storage",
|
|
2221
|
+
timemachine: "com.apple.preference.timemachine",
|
|
2222
|
+
sharing: "com.apple.preference.sharing",
|
|
2223
|
+
users: "com.apple.preference.users",
|
|
2224
|
+
dock: "com.apple.preference.dock"
|
|
2225
|
+
};
|
|
2226
|
+
const paneId = paneMap[pane.toLowerCase()] || pane;
|
|
2227
|
+
if (paneId) {
|
|
2228
|
+
await runShell(
|
|
2229
|
+
`open "x-apple.systempreferences:com.apple.settings.${pane}" 2>/dev/null || open "x-apple.systempreferences:${paneId}" 2>/dev/null || open "System Preferences"`
|
|
2230
|
+
);
|
|
2231
|
+
} else {
|
|
2232
|
+
await runShell(`open "System Preferences" 2>/dev/null || open -a "System Settings"`);
|
|
2233
|
+
}
|
|
2234
|
+
return { success: true, data: { pane: pane || "main" } };
|
|
2235
|
+
}
|
|
2236
|
+
// ── NEW: Dark Mode ──────────────────────────────────────
|
|
2237
|
+
case "sys_dark_mode": {
|
|
2238
|
+
const action = params.action || "status";
|
|
2239
|
+
if (action === "status") {
|
|
2240
|
+
const result = await runAppleScript(
|
|
2241
|
+
`tell application "System Events" to tell appearance preferences to get dark mode`
|
|
2242
|
+
);
|
|
2243
|
+
return {
|
|
2244
|
+
success: true,
|
|
2245
|
+
data: { darkMode: result.trim() === "true" }
|
|
2246
|
+
};
|
|
2247
|
+
} else if (action === "toggle") {
|
|
2248
|
+
await runAppleScript(
|
|
2249
|
+
`tell application "System Events" to tell appearance preferences to set dark mode to not dark mode`
|
|
2250
|
+
);
|
|
2251
|
+
const result = await runAppleScript(
|
|
2252
|
+
`tell application "System Events" to tell appearance preferences to get dark mode`
|
|
2253
|
+
);
|
|
2254
|
+
return {
|
|
2255
|
+
success: true,
|
|
2256
|
+
data: { darkMode: result.trim() === "true" }
|
|
2257
|
+
};
|
|
2258
|
+
} else if (action === "on") {
|
|
2259
|
+
await runAppleScript(
|
|
2260
|
+
`tell application "System Events" to tell appearance preferences to set dark mode to true`
|
|
2261
|
+
);
|
|
2262
|
+
return { success: true, data: { darkMode: true } };
|
|
2263
|
+
} else if (action === "off") {
|
|
2264
|
+
await runAppleScript(
|
|
2265
|
+
`tell application "System Events" to tell appearance preferences to set dark mode to false`
|
|
2266
|
+
);
|
|
2267
|
+
return { success: true, data: { darkMode: false } };
|
|
2268
|
+
}
|
|
2269
|
+
return {
|
|
2270
|
+
success: false,
|
|
2271
|
+
error: "Invalid action. Use: status, toggle, on, off"
|
|
2272
|
+
};
|
|
2273
|
+
}
|
|
2274
|
+
// ── NEW: Spotlight Search ───────────────────────────────
|
|
2275
|
+
case "sys_spotlight_search": {
|
|
2276
|
+
const query = params.query;
|
|
2277
|
+
if (!query) return { success: false, error: "Missing query" };
|
|
2278
|
+
const dir = params.directory;
|
|
2279
|
+
const kind = params.kind;
|
|
2280
|
+
const limit = params.limit || 20;
|
|
2281
|
+
let cmd = `mdfind`;
|
|
2282
|
+
if (dir) cmd += ` -onlyin "${dir}"`;
|
|
2283
|
+
if (kind) {
|
|
2284
|
+
cmd += ` 'kMDItemContentType == "*${kind}*"cd && kMDItemDisplayName == "*${query}*"cd'`;
|
|
2285
|
+
} else {
|
|
2286
|
+
cmd += ` "${query}"`;
|
|
2287
|
+
}
|
|
2288
|
+
cmd += ` | head -${limit}`;
|
|
2289
|
+
const result = await runShell(cmd);
|
|
2290
|
+
const files = result.trim().split("\n").filter((f) => f);
|
|
2291
|
+
return { success: true, data: { query, count: files.length, files } };
|
|
2292
|
+
}
|
|
2293
|
+
// ── NEW: Process Management ─────────────────────────────
|
|
2294
|
+
case "sys_process_list": {
|
|
2295
|
+
const result = await runShell(
|
|
2296
|
+
`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 ""}'`
|
|
2297
|
+
);
|
|
2298
|
+
const lines = result.trim().split("\n").slice(1);
|
|
2299
|
+
const processes = lines.map((l) => {
|
|
2300
|
+
const [user, pid, cpu, mem, ...cmdParts] = l.split("|");
|
|
2301
|
+
return {
|
|
2302
|
+
user: user?.trim(),
|
|
2303
|
+
pid: pid?.trim(),
|
|
2304
|
+
cpu: cpu?.trim() + "%",
|
|
2305
|
+
mem: mem?.trim() + "%",
|
|
2306
|
+
command: cmdParts.join("|").trim()
|
|
2307
|
+
};
|
|
2308
|
+
});
|
|
2309
|
+
return { success: true, data: { processes } };
|
|
2310
|
+
}
|
|
2311
|
+
case "sys_process_kill": {
|
|
2312
|
+
const pid = params.pid;
|
|
2313
|
+
const name = params.name;
|
|
2314
|
+
if (!pid && !name)
|
|
2315
|
+
return { success: false, error: "Provide pid or name" };
|
|
2316
|
+
if (pid) {
|
|
2317
|
+
await runShell(`kill ${pid}`);
|
|
2318
|
+
return { success: true, data: { killed: pid } };
|
|
2319
|
+
}
|
|
2320
|
+
const blocked = [
|
|
2321
|
+
"Finder",
|
|
2322
|
+
"loginwindow",
|
|
2323
|
+
"WindowServer",
|
|
2324
|
+
"kernel_task",
|
|
2325
|
+
"launchd"
|
|
2326
|
+
];
|
|
2327
|
+
if (blocked.some((b) => name.toLowerCase().includes(b.toLowerCase())))
|
|
2328
|
+
return {
|
|
2329
|
+
success: false,
|
|
2330
|
+
error: `Cannot kill system process: ${name}`
|
|
2331
|
+
};
|
|
2332
|
+
await runShell(`pkill -f "${name}"`);
|
|
2333
|
+
return { success: true, data: { killed: name } };
|
|
2334
|
+
}
|
|
2335
|
+
// ── NEW: WiFi Info ──────────────────────────────────────
|
|
2336
|
+
case "sys_wifi_info": {
|
|
2337
|
+
try {
|
|
2338
|
+
const ssid = await runShell(
|
|
2339
|
+
`networksetup -getairportnetwork en0 2>/dev/null | sed 's/Current Wi-Fi Network: //'`
|
|
2340
|
+
);
|
|
2341
|
+
const ip = await runShell(
|
|
2342
|
+
`ipconfig getifaddr en0 2>/dev/null || echo "Not connected"`
|
|
2343
|
+
);
|
|
2344
|
+
const dns = await runShell(
|
|
2345
|
+
`networksetup -getdnsservers Wi-Fi 2>/dev/null || echo "Auto"`
|
|
2346
|
+
);
|
|
2347
|
+
return {
|
|
2348
|
+
success: true,
|
|
2349
|
+
data: {
|
|
2350
|
+
ssid: ssid.trim(),
|
|
2351
|
+
ip: ip.trim(),
|
|
2352
|
+
dns: dns.trim().split("\n")
|
|
2353
|
+
}
|
|
2354
|
+
};
|
|
2355
|
+
} catch {
|
|
2356
|
+
return { success: true, data: { ssid: "Not connected" } };
|
|
2357
|
+
}
|
|
2358
|
+
}
|
|
2359
|
+
// ── NEW: Bluetooth ──────────────────────────────────────
|
|
2360
|
+
case "sys_bluetooth": {
|
|
2361
|
+
const btAction = params.action || "status";
|
|
2362
|
+
try {
|
|
2363
|
+
if (btAction === "status") {
|
|
2364
|
+
try {
|
|
2365
|
+
const power = await runShell(`blueutil --power 2>/dev/null`);
|
|
2366
|
+
const paired = await runShell(
|
|
2367
|
+
`blueutil --paired --format json 2>/dev/null`
|
|
2368
|
+
);
|
|
2369
|
+
return {
|
|
2370
|
+
success: true,
|
|
2371
|
+
data: {
|
|
2372
|
+
power: power.trim() === "1",
|
|
2373
|
+
devices: JSON.parse(paired || "[]")
|
|
2374
|
+
}
|
|
2375
|
+
};
|
|
2376
|
+
} catch {
|
|
2377
|
+
const result = await runShell(
|
|
2378
|
+
`system_profiler SPBluetoothDataType -json 2>/dev/null`
|
|
2379
|
+
);
|
|
2380
|
+
return { success: true, data: JSON.parse(result) };
|
|
2381
|
+
}
|
|
2382
|
+
} else if (btAction === "on") {
|
|
2383
|
+
await runShell(`blueutil --power 1`);
|
|
2384
|
+
return { success: true, data: { power: true } };
|
|
2385
|
+
} else if (btAction === "off") {
|
|
2386
|
+
await runShell(`blueutil --power 0`);
|
|
2387
|
+
return { success: true, data: { power: false } };
|
|
2388
|
+
} else if (btAction === "connect") {
|
|
2389
|
+
const address = params.address;
|
|
2390
|
+
if (!address) return { success: false, error: "Missing address" };
|
|
2391
|
+
await runShell(`blueutil --connect "${address}"`);
|
|
2392
|
+
return { success: true, data: { connected: address } };
|
|
2393
|
+
} else if (btAction === "disconnect") {
|
|
2394
|
+
const address = params.address;
|
|
2395
|
+
if (!address) return { success: false, error: "Missing address" };
|
|
2396
|
+
await runShell(`blueutil --disconnect "${address}"`);
|
|
2397
|
+
return { success: true, data: { disconnected: address } };
|
|
2398
|
+
}
|
|
2399
|
+
} catch (e) {
|
|
2400
|
+
return {
|
|
2401
|
+
success: false,
|
|
2402
|
+
error: `Bluetooth requires blueutil: brew install blueutil. ${e.message}`
|
|
2403
|
+
};
|
|
2404
|
+
}
|
|
2405
|
+
return { success: false, error: "Unknown action" };
|
|
2406
|
+
}
|
|
2407
|
+
// ── NEW: Audio Devices ──────────────────────────────────
|
|
2408
|
+
case "sys_audio_devices": {
|
|
2409
|
+
const audioAction = params.action || "list";
|
|
2410
|
+
if (audioAction === "list") {
|
|
2411
|
+
const result = await runAppleScript(`
|
|
2412
|
+
set output to ""
|
|
2413
|
+
tell application "System Events"
|
|
2414
|
+
set audioOut to name of every audio output device
|
|
2415
|
+
repeat with d in audioOut
|
|
2416
|
+
set output to output & d & linefeed
|
|
2417
|
+
end repeat
|
|
2418
|
+
end tell
|
|
2419
|
+
return output`);
|
|
2420
|
+
if (!result.trim()) {
|
|
2421
|
+
const spResult = await runShell(
|
|
2422
|
+
`system_profiler SPAudioDataType 2>/dev/null | grep "Device Name" | sed 's/.*: //'`
|
|
2423
|
+
);
|
|
2424
|
+
return {
|
|
2425
|
+
success: true,
|
|
2426
|
+
data: {
|
|
2427
|
+
devices: spResult.trim().split("\n").filter((d) => d)
|
|
2428
|
+
}
|
|
2429
|
+
};
|
|
2430
|
+
}
|
|
2431
|
+
return {
|
|
2432
|
+
success: true,
|
|
2433
|
+
data: {
|
|
2434
|
+
devices: result.trim().split("\n").filter((d) => d)
|
|
2435
|
+
}
|
|
2436
|
+
};
|
|
2437
|
+
} else if (audioAction === "switch") {
|
|
2438
|
+
const device = params.device;
|
|
2439
|
+
if (!device) return { success: false, error: "Missing device name" };
|
|
2440
|
+
try {
|
|
2441
|
+
await runShell(
|
|
2442
|
+
`SwitchAudioSource -s "${device}" 2>/dev/null`
|
|
2443
|
+
);
|
|
2444
|
+
return { success: true, data: { switched: device } };
|
|
2445
|
+
} catch {
|
|
2446
|
+
return {
|
|
2447
|
+
success: false,
|
|
2448
|
+
error: `Requires: brew install switchaudio-osx. Or manually change in System Settings.`
|
|
2449
|
+
};
|
|
2450
|
+
}
|
|
2451
|
+
}
|
|
2452
|
+
return { success: false, error: "Use action: list or switch" };
|
|
2453
|
+
}
|
|
2454
|
+
// ── NEW: Screen Lock ────────────────────────────────────
|
|
2455
|
+
case "sys_screen_lock": {
|
|
2456
|
+
await runShell(
|
|
2457
|
+
`pmset displaysleepnow 2>/dev/null || osascript -e 'tell application "System Events" to keystroke "q" using {control down, command down}'`
|
|
2458
|
+
);
|
|
2459
|
+
return { success: true, data: { locked: true } };
|
|
2460
|
+
}
|
|
2461
|
+
// ── NEW: Trash Operations ───────────────────────────────
|
|
2462
|
+
case "sys_trash": {
|
|
2463
|
+
const trashAction = params.action || "info";
|
|
2464
|
+
if (trashAction === "info") {
|
|
2465
|
+
const count = await runShell(
|
|
2466
|
+
`ls -1 ~/.Trash 2>/dev/null | wc -l`
|
|
2467
|
+
);
|
|
2468
|
+
const size = await runShell(
|
|
2469
|
+
`du -sh ~/.Trash 2>/dev/null | cut -f1`
|
|
2470
|
+
);
|
|
2471
|
+
return {
|
|
2472
|
+
success: true,
|
|
2473
|
+
data: {
|
|
2474
|
+
items: parseInt(count.trim()),
|
|
2475
|
+
size: size.trim()
|
|
2476
|
+
}
|
|
2477
|
+
};
|
|
2478
|
+
} else if (trashAction === "empty") {
|
|
2479
|
+
await runAppleScript(
|
|
2480
|
+
`tell application "Finder" to empty the trash`
|
|
2481
|
+
);
|
|
2482
|
+
return { success: true, data: { emptied: true } };
|
|
2483
|
+
}
|
|
2484
|
+
return { success: false, error: "Use action: info or empty" };
|
|
2485
|
+
}
|
|
2486
|
+
// ── NEW: Archive/Zip Operations ─────────────────────────
|
|
2487
|
+
case "sys_archive": {
|
|
2488
|
+
const archiveAction = params.action || "create";
|
|
2489
|
+
const source = params.source;
|
|
2490
|
+
const destination = params.destination;
|
|
2491
|
+
if (!source) return { success: false, error: "Missing source path" };
|
|
2492
|
+
const safeSrc = safePath(source);
|
|
2493
|
+
if (!safeSrc) return { success: false, error: "Path not allowed" };
|
|
2494
|
+
if (archiveAction === "create") {
|
|
2495
|
+
const dest = destination || `${safeSrc}.zip`;
|
|
2496
|
+
await runShell(`ditto -c -k --keepParent "${safeSrc}" "${dest}"`);
|
|
2497
|
+
return { success: true, data: { archive: dest } };
|
|
2498
|
+
} else if (archiveAction === "extract") {
|
|
2499
|
+
const dest = destination || safeSrc.replace(/\.(zip|tar\.gz|tgz)$/i, "");
|
|
2500
|
+
if (safeSrc.endsWith(".zip")) {
|
|
2501
|
+
await runShell(`ditto -x -k "${safeSrc}" "${dest}"`);
|
|
2502
|
+
} else {
|
|
2503
|
+
await runShell(`tar xzf "${safeSrc}" -C "${dest}"`);
|
|
2504
|
+
}
|
|
2505
|
+
return { success: true, data: { extracted: dest } };
|
|
2506
|
+
}
|
|
2507
|
+
return { success: false, error: "Use action: create or extract" };
|
|
2508
|
+
}
|
|
2509
|
+
// ── NEW: Disk Info ──────────────────────────────────────
|
|
2510
|
+
case "sys_disk_info": {
|
|
2511
|
+
const df = await runShell(`df -h / | tail -1`);
|
|
2512
|
+
const parts = df.trim().split(/\s+/);
|
|
2513
|
+
const volumes = await runShell(
|
|
2514
|
+
`ls -1 /Volumes 2>/dev/null`
|
|
2515
|
+
);
|
|
2516
|
+
return {
|
|
2517
|
+
success: true,
|
|
2518
|
+
data: {
|
|
2519
|
+
filesystem: parts[0],
|
|
2520
|
+
total: parts[1],
|
|
2521
|
+
used: parts[2],
|
|
2522
|
+
available: parts[3],
|
|
2523
|
+
usedPercent: parts[4],
|
|
2524
|
+
mountedVolumes: volumes.trim().split("\n").filter((v) => v)
|
|
2525
|
+
}
|
|
2526
|
+
};
|
|
2527
|
+
}
|
|
2528
|
+
// ── NEW: Focus Mode ─────────────────────────────────────
|
|
2529
|
+
case "sys_focus_mode": {
|
|
2530
|
+
const focusAction = params.action || "status";
|
|
2531
|
+
if (focusAction === "status") {
|
|
2532
|
+
const result = await runShell(
|
|
2533
|
+
`defaults read com.apple.controlcenter "NSStatusItem Visible FocusModes" 2>/dev/null || echo "unknown"`
|
|
2534
|
+
);
|
|
2535
|
+
return {
|
|
2536
|
+
success: true,
|
|
2537
|
+
data: { focusActive: result.trim() !== "0" }
|
|
2538
|
+
};
|
|
2539
|
+
} else if (focusAction === "set") {
|
|
2540
|
+
const mode = params.mode || "Do Not Disturb";
|
|
2541
|
+
try {
|
|
2542
|
+
await runShell(`shortcuts run "Set ${mode}" 2>/dev/null`);
|
|
2543
|
+
return { success: true, data: { mode } };
|
|
2544
|
+
} catch {
|
|
2545
|
+
await runShell(
|
|
2546
|
+
`shortcuts run "Toggle Do Not Disturb" 2>/dev/null`
|
|
2547
|
+
);
|
|
2548
|
+
return {
|
|
2549
|
+
success: true,
|
|
2550
|
+
data: {
|
|
2551
|
+
mode: "DND toggled (custom Focus modes require a Shortcut named 'Set <ModeName>')"
|
|
2552
|
+
}
|
|
2553
|
+
};
|
|
2554
|
+
}
|
|
2555
|
+
}
|
|
2556
|
+
return { success: false, error: "Use action: status or set" };
|
|
2557
|
+
}
|
|
2558
|
+
// ── NEW: Login Items ────────────────────────────────────
|
|
2559
|
+
case "sys_login_items": {
|
|
2560
|
+
const liAction = params.action || "list";
|
|
2561
|
+
if (liAction === "list") {
|
|
2562
|
+
const result = await runAppleScript(
|
|
2563
|
+
`tell application "System Events" to get name of every login item`
|
|
2564
|
+
);
|
|
2565
|
+
return {
|
|
2566
|
+
success: true,
|
|
2567
|
+
data: {
|
|
2568
|
+
items: result.trim().split(", ").filter((i) => i)
|
|
2569
|
+
}
|
|
2570
|
+
};
|
|
2571
|
+
} else if (liAction === "add") {
|
|
2572
|
+
const app = params.app;
|
|
2573
|
+
if (!app) return { success: false, error: "Missing app name/path" };
|
|
2574
|
+
const appPath = app.includes("/") ? app : `/Applications/${app}.app`;
|
|
2575
|
+
await runAppleScript(
|
|
2576
|
+
`tell application "System Events" to make login item at end with properties {path:"${appPath}", hidden:false}`
|
|
2577
|
+
);
|
|
2578
|
+
return { success: true, data: { added: app } };
|
|
2579
|
+
} else if (liAction === "remove") {
|
|
2580
|
+
const app = params.app;
|
|
2581
|
+
if (!app) return { success: false, error: "Missing app name" };
|
|
2582
|
+
await runAppleScript(
|
|
2583
|
+
`tell application "System Events" to delete login item "${app}"`
|
|
2584
|
+
);
|
|
2585
|
+
return { success: true, data: { removed: app } };
|
|
2586
|
+
}
|
|
2587
|
+
return { success: false, error: "Use action: list, add, or remove" };
|
|
2588
|
+
}
|
|
2589
|
+
// ── NEW: Power Management ───────────────────────────────
|
|
2590
|
+
case "sys_power": {
|
|
2591
|
+
const pwrAction = params.action || "status";
|
|
2592
|
+
if (pwrAction === "status") {
|
|
2593
|
+
const batt = await runShell(`pmset -g batt`);
|
|
2594
|
+
const assertions = await runShell(
|
|
2595
|
+
`pmset -g assertions 2>/dev/null | head -5`
|
|
2596
|
+
);
|
|
2597
|
+
return {
|
|
2598
|
+
success: true,
|
|
2599
|
+
data: { battery: batt.trim(), assertions: assertions.trim() }
|
|
2600
|
+
};
|
|
2601
|
+
} else if (pwrAction === "caffeinate") {
|
|
2602
|
+
const duration = params.duration || 3600;
|
|
2603
|
+
exec(`caffeinate -d -t ${duration} &`);
|
|
2604
|
+
return {
|
|
2605
|
+
success: true,
|
|
2606
|
+
data: { caffeinated: true, seconds: duration }
|
|
2607
|
+
};
|
|
2608
|
+
} else if (pwrAction === "sleep") {
|
|
2609
|
+
await runShell(`pmset sleepnow`);
|
|
2610
|
+
return { success: true, data: { sleeping: true } };
|
|
2611
|
+
}
|
|
2612
|
+
return { success: false, error: "Use action: status, caffeinate, sleep" };
|
|
2613
|
+
}
|
|
2614
|
+
// ── NEW: Printer Management ─────────────────────────────
|
|
2615
|
+
case "sys_printer": {
|
|
2616
|
+
const prAction = params.action || "list";
|
|
2617
|
+
if (prAction === "list") {
|
|
2618
|
+
const result = await runShell(`lpstat -p 2>/dev/null || echo "No printers"`);
|
|
2619
|
+
return { success: true, data: { printers: result.trim() } };
|
|
2620
|
+
} else if (prAction === "print") {
|
|
2621
|
+
const file = params.file;
|
|
2622
|
+
if (!file) return { success: false, error: "Missing file path" };
|
|
2623
|
+
const safe = safePath(file);
|
|
2624
|
+
if (!safe) return { success: false, error: "Path not allowed" };
|
|
2625
|
+
const printer = params.printer;
|
|
2626
|
+
const cmd = printer ? `lp -d "${printer}" "${safe}"` : `lp "${safe}"`;
|
|
2627
|
+
await runShell(cmd);
|
|
2628
|
+
return { success: true, data: { printed: file } };
|
|
2629
|
+
}
|
|
2630
|
+
return { success: false, error: "Use action: list or print" };
|
|
2631
|
+
}
|
|
2632
|
+
// ── NEW: Finder Tags ────────────────────────────────────
|
|
2633
|
+
case "sys_finder_tags": {
|
|
2634
|
+
const tagAction = params.action || "get";
|
|
2635
|
+
const filePath = params.path;
|
|
2636
|
+
if (!filePath) return { success: false, error: "Missing path" };
|
|
2637
|
+
const safeFp = safePath(filePath);
|
|
2638
|
+
if (!safeFp) return { success: false, error: "Path not allowed" };
|
|
2639
|
+
if (tagAction === "get") {
|
|
2640
|
+
const result = await runShell(
|
|
2641
|
+
`mdls -name kMDItemUserTags "${safeFp}" | sed 's/kMDItemUserTags = //' | tr -d '()"'`
|
|
2642
|
+
);
|
|
2643
|
+
return {
|
|
2644
|
+
success: true,
|
|
2645
|
+
data: {
|
|
2646
|
+
tags: result.trim().split(",").map((t) => t.trim()).filter((t) => t)
|
|
2647
|
+
}
|
|
2648
|
+
};
|
|
2649
|
+
} else if (tagAction === "set") {
|
|
2650
|
+
const tags = params.tags;
|
|
2651
|
+
if (!tags || !tags.length)
|
|
2652
|
+
return { success: false, error: "Missing tags array" };
|
|
2653
|
+
const tagStr = tags.map((t) => `"${t}"`).join(", ");
|
|
2654
|
+
await runShell(
|
|
2655
|
+
`xattr -w com.apple.metadata:_kMDItemUserTags '(${tagStr})' "${safeFp}"`
|
|
2656
|
+
);
|
|
2657
|
+
return { success: true, data: { path: safeFp, tags } };
|
|
2658
|
+
}
|
|
2659
|
+
return { success: false, error: "Use action: get or set" };
|
|
2660
|
+
}
|
|
2661
|
+
// ── NEW: Quick Note ─────────────────────────────────────
|
|
2662
|
+
case "sys_quick_note": {
|
|
2663
|
+
const body = params.body || "";
|
|
2664
|
+
await runAppleScript(
|
|
2665
|
+
`tell application "Notes" to activate
|
|
2666
|
+
tell application "System Events"
|
|
2667
|
+
keystroke "n" using command down
|
|
2668
|
+
delay 0.3
|
|
2669
|
+
keystroke "${body.replace(/"/g, '\\"')}"
|
|
2670
|
+
end tell`
|
|
2671
|
+
);
|
|
2672
|
+
return { success: true, data: { created: true } };
|
|
2673
|
+
}
|
|
2674
|
+
// ── NEW: Terminal Tab ───────────────────────────────────
|
|
2675
|
+
case "sys_terminal": {
|
|
2676
|
+
const termAction = params.action || "new_tab";
|
|
2677
|
+
const termCmd = params.command || "";
|
|
2678
|
+
if (termAction === "new_tab") {
|
|
2679
|
+
await runAppleScript(`
|
|
2680
|
+
tell application "Terminal"
|
|
2681
|
+
activate
|
|
2682
|
+
do script "${termCmd.replace(/"/g, '\\"')}"
|
|
2683
|
+
end tell`);
|
|
2684
|
+
return { success: true, data: { opened: true, command: termCmd } };
|
|
2685
|
+
} else if (termAction === "new_window") {
|
|
2686
|
+
await runAppleScript(`
|
|
2687
|
+
tell application "Terminal"
|
|
2688
|
+
activate
|
|
2689
|
+
do script "${termCmd.replace(/"/g, '\\"')}" in (make new window)
|
|
2690
|
+
end tell`);
|
|
2691
|
+
return { success: true, data: { opened: true, command: termCmd } };
|
|
2692
|
+
}
|
|
2693
|
+
return { success: false, error: "Use action: new_tab or new_window" };
|
|
2694
|
+
}
|
|
2695
|
+
// ── NEW: PDF Operations ─────────────────────────────────
|
|
2696
|
+
case "sys_pdf": {
|
|
2697
|
+
const pdfAction = params.action || "info";
|
|
2698
|
+
const pdfPath = params.path;
|
|
2699
|
+
if (!pdfPath) return { success: false, error: "Missing path" };
|
|
2700
|
+
const safePdf = safePath(pdfPath);
|
|
2701
|
+
if (!safePdf) return { success: false, error: "Path not allowed" };
|
|
2702
|
+
if (pdfAction === "info") {
|
|
2703
|
+
const result = await runShell(
|
|
2704
|
+
`mdls -name kMDItemNumberOfPages -name kMDItemTitle -name kMDItemAuthors -name kMDItemFSSize "${safePdf}"`
|
|
2705
|
+
);
|
|
2706
|
+
return { success: true, data: { info: result.trim() } };
|
|
2707
|
+
} else if (pdfAction === "text") {
|
|
2708
|
+
try {
|
|
2709
|
+
const text2 = await runShell(
|
|
2710
|
+
`textutil -convert txt -stdout "${safePdf}" 2>/dev/null | head -500`
|
|
2711
|
+
);
|
|
2712
|
+
if (text2.trim())
|
|
2713
|
+
return { success: true, data: { text: text2.trim() } };
|
|
2714
|
+
} catch {
|
|
2715
|
+
}
|
|
2716
|
+
const text = await runShell(
|
|
2717
|
+
`python3 -c "
|
|
2718
|
+
import subprocess
|
|
2719
|
+
result = subprocess.run(['mdls', '-name', 'kMDItemTextContent', '${safePdf.replace(/'/g, "\\'")}'], capture_output=True, text=True)
|
|
2720
|
+
print(result.stdout[:5000])
|
|
2721
|
+
" 2>/dev/null || echo "Could not extract text. Install: brew install poppler (for pdftotext)"`
|
|
2722
|
+
);
|
|
2723
|
+
return { success: true, data: { text: text.trim() } };
|
|
2724
|
+
} else if (pdfAction === "merge") {
|
|
2725
|
+
const files = params.files;
|
|
2726
|
+
const output = params.output;
|
|
2727
|
+
if (!files || !output)
|
|
2728
|
+
return { success: false, error: "Missing files array and output" };
|
|
2729
|
+
const safePaths = files.map((f) => `"${safePath(f) || f}"`).join(" ");
|
|
2730
|
+
await runShell(
|
|
2731
|
+
`/System/Library/Automator/Combine\\ PDF\\ Pages.action/Contents/MacOS/join -o "${safePath(output) || output}" ${safePaths}`
|
|
2732
|
+
);
|
|
2733
|
+
return { success: true, data: { merged: output } };
|
|
2734
|
+
}
|
|
2735
|
+
return {
|
|
2736
|
+
success: false,
|
|
2737
|
+
error: "Use action: info, text, or merge"
|
|
2738
|
+
};
|
|
2739
|
+
}
|
|
2740
|
+
// ── NEW: Launchctl / Services ───────────────────────────
|
|
2741
|
+
case "sys_services": {
|
|
2742
|
+
const svcAction = params.action || "list";
|
|
2743
|
+
if (svcAction === "list") {
|
|
2744
|
+
const result = await runShell(
|
|
2745
|
+
`launchctl list 2>/dev/null | head -30`
|
|
2746
|
+
);
|
|
2747
|
+
return { success: true, data: { services: result.trim() } };
|
|
2748
|
+
} else if (svcAction === "start") {
|
|
2749
|
+
const name = params.name;
|
|
2750
|
+
if (!name) return { success: false, error: "Missing service name" };
|
|
2751
|
+
await runShell(`launchctl kickstart gui/$(id -u)/${name} 2>/dev/null`);
|
|
2752
|
+
return { success: true, data: { started: name } };
|
|
2753
|
+
} else if (svcAction === "stop") {
|
|
2754
|
+
const name = params.name;
|
|
2755
|
+
if (!name) return { success: false, error: "Missing service name" };
|
|
2756
|
+
await runShell(
|
|
2757
|
+
`launchctl kill SIGTERM gui/$(id -u)/${name} 2>/dev/null`
|
|
2758
|
+
);
|
|
2759
|
+
return { success: true, data: { stopped: name } };
|
|
2760
|
+
}
|
|
2761
|
+
return { success: false, error: "Use action: list, start, stop" };
|
|
2762
|
+
}
|
|
2170
2763
|
default:
|
|
2171
2764
|
return { success: false, error: `Unknown command: ${command}` };
|
|
2172
2765
|
}
|
|
@@ -2586,7 +3179,7 @@ function writeString(view, offset, str) {
|
|
|
2586
3179
|
}
|
|
2587
3180
|
console.log("");
|
|
2588
3181
|
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");
|
|
2589
|
-
console.log(" \u2551 \u{1FAC0} Pulso Mac Companion v0.
|
|
3182
|
+
console.log(" \u2551 \u{1FAC0} Pulso Mac Companion v0.3.0 \u2551");
|
|
2590
3183
|
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");
|
|
2591
3184
|
console.log("");
|
|
2592
3185
|
setupPermissions().then(() => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pulso/companion",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Pulso Companion — gives your AI agent real control over your computer",
|
|
6
6
|
"bin": {
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"mac",
|
|
23
23
|
"automation"
|
|
24
24
|
],
|
|
25
|
-
"author": "Pulso <hello@
|
|
25
|
+
"author": "Pulso <hello@runpulso.com>",
|
|
26
26
|
"license": "MIT",
|
|
27
27
|
"repository": {
|
|
28
28
|
"type": "git",
|