Files

165 lines
6.3 KiB
JavaScript

// Computer-use helper integration: locating the bundled ComputerUse.app,
// permission checks (spawn --check for a fresh TCC read), running-app
// listing for @App mentions, and opening the permission-setup GUI.
// Extracted from main.mjs; consumed only by the desktop IPC registry.
import { spawn } from "node:child_process";
import { existsSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { app, shell } from "electron";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const COMPUTER_USE_HELPER_APP_NAME = "OpenWork Computer Use.app";
const COMPUTER_USE_HELPER_EXECUTABLE = "ComputerUse";
function computerUseHelperExecutablePath() {
const appPath = computerUseHelperAppPath();
const explicitBinary = process.env.OPENWORK_COMPUTER_USE_BINARY?.trim();
const candidates = [
explicitBinary,
appPath ? path.join(appPath, "Contents", "MacOS", COMPUTER_USE_HELPER_EXECUTABLE) : null,
].filter(Boolean);
return candidates.find((candidate) => existsSync(candidate)) ?? null;
}
function computerUseHelperAppPath() {
const explicitApp = process.env.OPENWORK_COMPUTER_USE_APP?.trim();
const candidates = [
explicitApp,
process.resourcesPath ? path.join(process.resourcesPath, "helpers", COMPUTER_USE_HELPER_APP_NAME) : null,
path.resolve(__dirname, "..", "resources", "helpers", COMPUTER_USE_HELPER_APP_NAME),
].filter(Boolean);
return candidates.find((candidate) => existsSync(candidate)) ?? null;
}
function getComputerUseMcpCommand() {
const helperExecutable = computerUseHelperExecutablePath();
if (helperExecutable) return [helperExecutable, "mcp"];
if (app.isPackaged) {
throw new Error("OpenWork Computer Use is missing from this OpenWork build.");
}
if (process.env.OPENWORK_DEV_MODE === "1") {
return ["node", path.resolve(__dirname, "../../..", "packages/handsfree/bin/openwork-handsfree-computer-use.mjs"), "mcp"];
}
return ["npx", "-y", "@openwork/handsfree", "mcp"];
}
// ---------------------------------------------------------------------------
// Permission checks — spawn the binary with --check, read stdout, done.
// Fresh process = fresh TCC read = always accurate. No HTTP server needed.
// ---------------------------------------------------------------------------
function resolveComputerUseExecutable() {
// 1. Explicit env override.
const explicit = process.env.OPENWORK_COMPUTER_USE_BINARY?.trim();
if (explicit && existsSync(explicit)) return explicit;
// 2. .app bundle (packaged builds + pnpm dev).
const appPath = computerUseHelperAppPath();
if (appPath) {
const bin = path.join(appPath, "Contents", "MacOS", COMPUTER_USE_HELPER_EXECUTABLE);
if (existsSync(bin)) return bin;
}
// 3. Dev fallback — raw Swift build output.
if (!app.isPackaged) {
const swiftPkg = path.resolve(__dirname, "../../..", "packages/handsfree/native/HandsFree");
const devCandidates = [
path.join(swiftPkg, ".build", "release", "HandsFreeComputerUse"),
path.join(swiftPkg, ".build", "arm64-apple-macosx", "release", "HandsFreeComputerUse"),
path.join(swiftPkg, ".build", "debug", "HandsFreeComputerUse"),
path.join(swiftPkg, ".build", "arm64-apple-macosx", "debug", "HandsFreeComputerUse"),
];
for (const c of devCandidates) {
if (existsSync(c)) return c;
}
}
return null;
}
async function checkComputerUsePermissions() {
// Spawn binary --check → read JSON from stdout → exit. Always fresh.
const bin = resolveComputerUseExecutable();
if (!bin) {
return { ok: false, accessibility: false, screenRecording: false, error: "Helper binary not found. Run pnpm dev to build it." };
}
return spawnCheckPermissions(bin);
}
function spawnCheckPermissions(bin) {
return new Promise((resolve) => {
let stdout = "";
const child = spawn(bin, ["--check"], { stdio: ["ignore", "pipe", "ignore"], timeout: 5_000 });
child.stdout.setEncoding("utf8");
child.stdout.on("data", (chunk) => { stdout += chunk; });
child.on("error", () => resolve({ ok: false, accessibility: false, screenRecording: false, error: "Failed to run permission check." }));
child.on("close", () => {
try {
const parsed = JSON.parse(stdout.trim());
resolve({
ok: parsed?.ok === true,
accessibility: parsed?.accessibility === true,
screenRecording: parsed?.screenRecording === true,
});
} catch {
resolve({ ok: false, accessibility: false, screenRecording: false, error: "Permission check returned invalid output." });
}
});
});
}
async function listRunningApps() {
// Spawn binary --list-apps → read JSON from stdout → exit. Needs no TCC
// permissions, so this works before Computer Use setup is complete.
if (process.platform !== "darwin") return { ok: false, apps: [] };
const bin = resolveComputerUseExecutable();
if (!bin) return { ok: false, apps: [] };
return new Promise((resolve) => {
let stdout = "";
const child = spawn(bin, ["--list-apps"], { stdio: ["ignore", "pipe", "ignore"], timeout: 5_000 });
child.stdout.setEncoding("utf8");
child.stdout.on("data", (chunk) => { stdout += chunk; });
child.on("error", () => resolve({ ok: false, apps: [] }));
child.on("close", () => {
try {
const parsed = JSON.parse(stdout.trim());
const apps = Array.isArray(parsed?.apps) ? parsed.apps.filter((name) => typeof name === "string" && name.trim()) : [];
resolve({ ok: parsed?.ok === true, apps });
} catch {
resolve({ ok: false, apps: [] });
}
});
});
}
async function openComputerUseSetupApp() {
// Open the GUI. Use the .app bundle if available so macOS shows it as
// a real app with its own dock icon and permission identity.
const appPath = computerUseHelperAppPath();
if (appPath) {
const result = await shell.openPath(appPath);
if (result) console.error("[ComputerUse] shell.openPath error:", result);
return;
}
// Fallback: spawn the raw binary (opens the same GUI).
const bin = resolveComputerUseExecutable();
if (!bin) throw new Error("Helper binary not found. Run pnpm dev to build it.");
const child = spawn(bin, [], { detached: true, stdio: "ignore" });
child.unref();
}
export {
checkComputerUsePermissions,
getComputerUseMcpCommand,
listRunningApps,
openComputerUseSetupApp,
};