@forsion/tangu-computer-use 0.5.8

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 (120) hide show
  1. package/CHANGELOG.md +223 -0
  2. package/LICENSE +26 -0
  3. package/LICENSE.upstream +21 -0
  4. package/README.md +169 -0
  5. package/UPSTREAM.md +277 -0
  6. package/check.mjs +369 -0
  7. package/icon.png +0 -0
  8. package/install.sh +112 -0
  9. package/main.js +541 -0
  10. package/manifest.json +47 -0
  11. package/native/linux/bridge-rs/Cargo.lock +1204 -0
  12. package/native/linux/bridge-rs/Cargo.toml +18 -0
  13. package/native/linux/bridge-rs/src/atspi.rs +640 -0
  14. package/native/linux/bridge-rs/src/error.rs +65 -0
  15. package/native/linux/bridge-rs/src/lib.rs +11 -0
  16. package/native/linux/bridge-rs/src/main.rs +1074 -0
  17. package/native/linux/bridge-rs/src/protocol.rs +49 -0
  18. package/native/linux/bridge-rs/src/state.rs +293 -0
  19. package/native/linux/bridge-rs/src/wayland.rs +89 -0
  20. package/native/linux/bridge-rs/src/x11.rs +909 -0
  21. package/native/linux/bridge-rs/tests/protocol_tests.rs +73 -0
  22. package/native/macos/agent_cursor.swift +184 -0
  23. package/native/macos/agent_cursor_motion.swift +252 -0
  24. package/native/macos/agent_cursor_tests.swift +128 -0
  25. package/native/macos/agent_highlight.swift +229 -0
  26. package/native/macos/agent_highlight_tests.swift +99 -0
  27. package/native/macos/bridge.swift +3852 -0
  28. package/native/macos/foreground_activity.swift +57 -0
  29. package/native/macos/foreground_activity_tests.swift +31 -0
  30. package/native/macos/live_stream.swift +289 -0
  31. package/native/windows/bridge-rs/Cargo.lock +396 -0
  32. package/native/windows/bridge-rs/Cargo.toml +30 -0
  33. package/native/windows/bridge-rs/src/capture.rs +518 -0
  34. package/native/windows/bridge-rs/src/error.rs +81 -0
  35. package/native/windows/bridge-rs/src/input.rs +501 -0
  36. package/native/windows/bridge-rs/src/lib.rs +16 -0
  37. package/native/windows/bridge-rs/src/main.rs +1683 -0
  38. package/native/windows/bridge-rs/src/protocol.rs +53 -0
  39. package/native/windows/bridge-rs/src/refs.rs +237 -0
  40. package/native/windows/bridge-rs/src/state.rs +55 -0
  41. package/native/windows/bridge-rs/src/uia.rs +1252 -0
  42. package/native/windows/bridge-rs/src/window.rs +718 -0
  43. package/native/windows/bridge-rs/tests/protocol_tests.rs +252 -0
  44. package/native/windows/bridge-rs/tests/refs_tests.rs +86 -0
  45. package/native/windows/bridge-rs/tests/state_tests.rs +33 -0
  46. package/package.json +74 -0
  47. package/prebuilt/linux/arm64/linux-bridge +0 -0
  48. package/prebuilt/linux/x64/linux-bridge +0 -0
  49. package/prebuilt/macos/arm64/bridge +0 -0
  50. package/prebuilt/macos/arm64/tangu-computer-use.app.json +10 -0
  51. package/prebuilt/macos/arm64/tangu-computer-use.app.zip +0 -0
  52. package/prebuilt/macos/x64/bridge +0 -0
  53. package/prebuilt/macos/x64/tangu-computer-use.app.json +10 -0
  54. package/prebuilt/macos/x64/tangu-computer-use.app.zip +0 -0
  55. package/prebuilt/windows/windows-bridge.exe +0 -0
  56. package/scripts/blind-click.check.mjs +128 -0
  57. package/scripts/build-native.mjs +302 -0
  58. package/scripts/build.mjs +84 -0
  59. package/scripts/calc-fixture.mjs +78 -0
  60. package/scripts/cli-exit.check.mjs +49 -0
  61. package/scripts/helper-path.check.mjs +82 -0
  62. package/scripts/helper-refresh.check.mjs +62 -0
  63. package/scripts/helper-signal.check.mjs +59 -0
  64. package/scripts/highlight.check.mjs +32 -0
  65. package/scripts/keychain-free-install.check.mjs +140 -0
  66. package/scripts/live-view.check.mjs +124 -0
  67. package/scripts/macos-bundle.d.mts +1 -0
  68. package/scripts/macos-bundle.mjs +119 -0
  69. package/scripts/make-signing-cert.sh +57 -0
  70. package/scripts/mini-foreground.check.mjs +11 -0
  71. package/scripts/no-foreground.check.mjs +81 -0
  72. package/scripts/overlay-visible.check.mjs +221 -0
  73. package/scripts/package-macos-app.mjs +39 -0
  74. package/scripts/permissions.check.mjs +199 -0
  75. package/scripts/platform-contract.check.mjs +30 -0
  76. package/scripts/setup-helper.mjs +154 -0
  77. package/scripts/tangu-computer-use.entitlements +10 -0
  78. package/scripts/verify-macos-bundles.mjs +25 -0
  79. package/scripts/verify-package.mjs +74 -0
  80. package/skills/computer-use/SKILL.md +110 -0
  81. package/src/foregroundNote.ts +43 -0
  82. package/src/helperState.ts +31 -0
  83. package/src/index.ts +63 -0
  84. package/src/onboarding.ts +198 -0
  85. package/src/pi-compat.ts +49 -0
  86. package/src/settings.ts +11 -0
  87. package/src/setup.ts +97 -0
  88. package/src/tools.ts +289 -0
  89. package/src/vendor/actions.ts +130 -0
  90. package/src/vendor/bridge.ts +2405 -0
  91. package/src/vendor/cdp.ts +658 -0
  92. package/src/vendor/config.ts +113 -0
  93. package/src/vendor/contract.ts +104 -0
  94. package/src/vendor/note.ts +195 -0
  95. package/src/vendor/outline.ts +651 -0
  96. package/src/vendor/output.ts +134 -0
  97. package/src/vendor/permissions.ts +111 -0
  98. package/src/vendor/platform/architecture.ts +23 -0
  99. package/src/vendor/platform/coerce.ts +16 -0
  100. package/src/vendor/platform/index.ts +59 -0
  101. package/src/vendor/platform/linux/backend.ts +186 -0
  102. package/src/vendor/platform/linux/helper.ts +238 -0
  103. package/src/vendor/platform/macos/backend.ts +131 -0
  104. package/src/vendor/platform/macos/browser.ts +110 -0
  105. package/src/vendor/platform/macos/helper-path.d.mts +9 -0
  106. package/src/vendor/platform/macos/helper-path.mjs +34 -0
  107. package/src/vendor/platform/macos/helper.ts +291 -0
  108. package/src/vendor/platform/macos/permissions.ts +146 -0
  109. package/src/vendor/platform/types.ts +222 -0
  110. package/src/vendor/platform/windows/backend.ts +142 -0
  111. package/src/vendor/platform/windows/helper.ts +140 -0
  112. package/src/vendor/root-selection.ts +25 -0
  113. package/src/vendor/runtime.ts +129 -0
  114. package/src/vendor/state.ts +146 -0
  115. package/src/vendor/view.ts +147 -0
  116. package/tangu-plugins/computer-use/dist/foregroundNote.js +19 -0
  117. package/tangu-plugins/computer-use/dist/index.js +5672 -0
  118. package/tangu-plugins/computer-use/tangu-plugin.json +9 -0
  119. package/tsconfig.json +22 -0
  120. package/types/tangu-agent.d.ts +142 -0
@@ -0,0 +1,238 @@
1
+ import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process";
2
+ import { randomUUID } from "node:crypto";
3
+ import { constants as fsConstants } from "node:fs";
4
+ import { access } from "node:fs/promises";
5
+ import os from "node:os";
6
+ import path from "node:path";
7
+ import { fileURLToPath } from "node:url";
8
+
9
+ const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "..");
10
+ const SETUP_HELPER_SCRIPT = path.join(PACKAGE_ROOT, "scripts", "setup-helper.mjs");
11
+ const HELPER_SETUP_TIMEOUT_MS = 60_000;
12
+ const COMMAND_TIMEOUT_MS = 15_000;
13
+
14
+ export const LINUX_HELPER_PROTOCOL_VERSION = 4;
15
+ export const LINUX_HELPER_PATH = process.env.PI_COMPUTER_USE_LINUX_HELPER_PATH
16
+ || path.join(os.homedir(), ".pi", "agent", "helpers", "tangu-computer-use", "linux-bridge");
17
+
18
+ interface Pending<T> {
19
+ resolve(value: T): void;
20
+ reject(error: Error): void;
21
+ timer: NodeJS.Timeout;
22
+ }
23
+
24
+ async function isExecutable(filePath: string): Promise<boolean> {
25
+ try { await access(filePath, fsConstants.X_OK); return true; } catch { return false; }
26
+ }
27
+
28
+ async function runProcess(command: string, args: string[], timeoutMs: number, signal?: AbortSignal, env?: NodeJS.ProcessEnv): Promise<void> {
29
+ if (signal?.aborted) throw new Error("Operation aborted.");
30
+ await new Promise<void>((resolve, reject) => {
31
+ const child = spawn(command, args, { stdio: ["ignore", "pipe", "pipe"], env });
32
+ let stdout = "";
33
+ let stderr = "";
34
+ let settled = false;
35
+ const finish = (error?: Error) => {
36
+ if (settled) return;
37
+ settled = true;
38
+ clearTimeout(timer);
39
+ signal?.removeEventListener("abort", onAbort);
40
+ if (error) reject(error); else resolve();
41
+ };
42
+ const timer = setTimeout(() => {
43
+ child.kill("SIGTERM");
44
+ finish(new Error(`Command timed out after ${timeoutMs}ms: ${command} ${args.join(" ")}`));
45
+ }, timeoutMs);
46
+ const onAbort = () => { child.kill("SIGTERM"); finish(new Error("Operation aborted.")); };
47
+ child.stdout.on("data", (chunk) => { stdout += String(chunk); });
48
+ child.stderr.on("data", (chunk) => { stderr += String(chunk); });
49
+ child.on("error", (error) => finish(error));
50
+ child.on("close", (code) => {
51
+ if (code === 0) return finish();
52
+ const output = [stderr.trim(), stdout.trim()].filter(Boolean).join("\n");
53
+ finish(new Error(`Command failed (${code}): ${command} ${args.join(" ")}\n${output}`.trim()));
54
+ });
55
+ signal?.addEventListener("abort", onAbort, { once: true });
56
+ });
57
+ }
58
+
59
+ async function waitForShared<T>(promise: Promise<T>, signal?: AbortSignal): Promise<T> {
60
+ if (!signal) return await promise;
61
+ if (signal.aborted) throw new Error("Operation aborted.");
62
+ return await new Promise<T>((resolve, reject) => {
63
+ const onAbort = () => reject(new Error("Operation aborted."));
64
+ signal.addEventListener("abort", onAbort, { once: true });
65
+ promise.then(
66
+ (value) => {
67
+ signal.removeEventListener("abort", onAbort);
68
+ resolve(value);
69
+ },
70
+ (error) => {
71
+ signal.removeEventListener("abort", onAbort);
72
+ reject(error);
73
+ },
74
+ );
75
+ });
76
+ }
77
+
78
+ export interface LinuxHelperClientOptions {
79
+ helperPath?: string;
80
+ setupHelperScript?: string;
81
+ }
82
+
83
+ export class LinuxHelperClient {
84
+ private installChecked = false;
85
+ private installPromise?: Promise<void>;
86
+ private child?: ChildProcessWithoutNullStreams;
87
+ private processPromise?: Promise<ChildProcessWithoutNullStreams>;
88
+ private buffer = "";
89
+ private pending = new Map<string, Pending<unknown>>();
90
+ private readonly helperPath: string;
91
+ private readonly setupHelperScript: string;
92
+
93
+ constructor(options: LinuxHelperClientOptions = {}) {
94
+ this.helperPath = options.helperPath ?? LINUX_HELPER_PATH;
95
+ this.setupHelperScript = options.setupHelperScript ?? SETUP_HELPER_SCRIPT;
96
+ }
97
+
98
+ dispose(): void {
99
+ this.rejectPending(new Error("Linux helper closed because the Tangu session ended."));
100
+ const child = this.child;
101
+ this.child = undefined;
102
+ if (!child) return;
103
+ child.stdin.destroy();
104
+ child.stdout.destroy();
105
+ child.stderr.destroy();
106
+ child.kill("SIGTERM");
107
+ child.unref();
108
+ }
109
+
110
+ private rejectPending(error: Error): void {
111
+ for (const pending of this.pending.values()) {
112
+ clearTimeout(pending.timer);
113
+ pending.reject(error);
114
+ }
115
+ this.pending.clear();
116
+ this.buffer = "";
117
+ }
118
+
119
+ async ensureInstalled(signal?: AbortSignal): Promise<void> {
120
+ if ((await isExecutable(this.helperPath)) && this.installChecked) return;
121
+ if (!this.installPromise) {
122
+ const installPromise = (async () => {
123
+ await runProcess(process.execPath, [this.setupHelperScript, "--platform", "linux", "--runtime"], HELPER_SETUP_TIMEOUT_MS, undefined, {
124
+ ...process.env,
125
+ ELECTRON_RUN_AS_NODE: "1",
126
+ BUN_BE_BUN: "1",
127
+ PI_COMPUTER_USE_LINUX_HELPER_PATH: this.helperPath,
128
+ });
129
+ if (!(await isExecutable(this.helperPath))) throw new Error(`Failed to install Linux helper at ${this.helperPath}.`);
130
+ this.installChecked = true;
131
+ })();
132
+ this.installPromise = installPromise;
133
+ installPromise.then(
134
+ () => { if (this.installPromise === installPromise) this.installPromise = undefined; },
135
+ () => { if (this.installPromise === installPromise) this.installPromise = undefined; },
136
+ );
137
+ }
138
+ await waitForShared(this.installPromise, signal);
139
+ }
140
+
141
+ private async process(signal?: AbortSignal): Promise<ChildProcessWithoutNullStreams> {
142
+ await this.ensureInstalled(signal);
143
+ if (this.processPromise) return await waitForShared(this.processPromise, signal);
144
+ if (this.child && this.child.exitCode === null && !this.child.killed) return this.child;
145
+ if (!this.processPromise) {
146
+ const processPromise = this.startProcess();
147
+ this.processPromise = processPromise;
148
+ processPromise.then(
149
+ () => { if (this.processPromise === processPromise) this.processPromise = undefined; },
150
+ () => { if (this.processPromise === processPromise) this.processPromise = undefined; },
151
+ );
152
+ }
153
+ return await waitForShared(this.processPromise, signal);
154
+ }
155
+
156
+ private async startProcess(): Promise<ChildProcessWithoutNullStreams> {
157
+ const child = spawn(this.helperPath, [], { stdio: ["pipe", "pipe", "pipe"] });
158
+ child.stdout.setEncoding("utf8");
159
+ child.stderr.setEncoding("utf8");
160
+ child.stdin.setDefaultEncoding("utf8");
161
+ child.stdout.on("data", (chunk: string) => this.onStdout(chunk));
162
+ child.on("exit", (code, signalName) => {
163
+ if (this.child !== child) return;
164
+ this.child = undefined;
165
+ this.rejectPending(new Error(`Linux helper exited${signalName ? ` on ${signalName}` : ` with code ${code ?? "unknown"}`}.`));
166
+ });
167
+ child.on("error", (error) => {
168
+ if (this.child !== child) return;
169
+ this.child = undefined;
170
+ this.rejectPending(error);
171
+ });
172
+ this.child = child;
173
+ this.buffer = "";
174
+ return await new Promise<ChildProcessWithoutNullStreams>((resolve, reject) => {
175
+ child.once("spawn", () => resolve(child));
176
+ child.once("error", reject);
177
+ });
178
+ }
179
+
180
+ private onStdout(chunk: string): void {
181
+ this.buffer += chunk;
182
+ for (;;) {
183
+ const newline = this.buffer.indexOf("\n");
184
+ if (newline < 0) return;
185
+ const line = this.buffer.slice(0, newline).trim();
186
+ this.buffer = this.buffer.slice(newline + 1);
187
+ if (!line) continue;
188
+ let parsed: any;
189
+ try { parsed = JSON.parse(line); } catch { continue; }
190
+ const pending = this.pending.get(parsed.id);
191
+ if (!pending) continue;
192
+ this.pending.delete(parsed.id);
193
+ clearTimeout(pending.timer);
194
+ if (parsed.protocolVersion !== LINUX_HELPER_PROTOCOL_VERSION) {
195
+ pending.reject(new Error(`Linux helper protocol mismatch: expected ${LINUX_HELPER_PROTOCOL_VERSION}, got ${parsed.protocolVersion ?? "unknown"}. Restart Tangu to use the installed helper.`));
196
+ } else if (parsed.ok === true) {
197
+ pending.resolve(parsed.result);
198
+ } else {
199
+ const error = new Error(parsed.error?.message ?? "Linux helper command failed.") as Error & { code?: string };
200
+ error.code = parsed.error?.code;
201
+ pending.reject(error);
202
+ }
203
+ }
204
+ }
205
+
206
+ async command<T>(cmd: string, args: Record<string, unknown> = {}, options?: { timeoutMs?: number; signal?: AbortSignal }): Promise<T> {
207
+ const child = await this.process(options?.signal);
208
+ const id = randomUUID();
209
+ const timeoutMs = options?.timeoutMs ?? COMMAND_TIMEOUT_MS;
210
+ return await new Promise<T>((resolve, reject) => {
211
+ const onAbort = () => {
212
+ this.pending.delete(id);
213
+ clearTimeout(timer);
214
+ reject(new Error("Operation aborted."));
215
+ };
216
+ const timer = setTimeout(() => {
217
+ options?.signal?.removeEventListener("abort", onAbort);
218
+ this.pending.delete(id);
219
+ reject(new Error(`Helper command '${cmd}' timed out after ${timeoutMs}ms.`));
220
+ }, timeoutMs);
221
+ this.pending.set(id, {
222
+ resolve: (value) => { options?.signal?.removeEventListener("abort", onAbort); resolve(value as T); },
223
+ reject: (error) => { options?.signal?.removeEventListener("abort", onAbort); reject(error); },
224
+ timer,
225
+ });
226
+ options?.signal?.addEventListener("abort", onAbort, { once: true });
227
+ child.stdin.write(`${JSON.stringify({ protocolVersion: LINUX_HELPER_PROTOCOL_VERSION, id, cmd, args })}\n`, (error) => {
228
+ if (!error) return;
229
+ options?.signal?.removeEventListener("abort", onAbort);
230
+ this.pending.delete(id);
231
+ clearTimeout(timer);
232
+ reject(error);
233
+ });
234
+ });
235
+ }
236
+ }
237
+
238
+ export const linuxHelper = new LinuxHelperClient();
@@ -0,0 +1,131 @@
1
+ import { getComputerUseConfig } from "../../config.ts";
2
+ import { parseLookResponse, type LookResponse } from "../../outline.ts";
3
+ import { toBoolean, toFiniteNumber, toOptionalString } from "../coerce.ts";
4
+ import type { ComputerUsePlatformBackend, FramePoints, HelperActResult, PlatformActRequest, PlatformApp, PlatformFocusWindowResult, PlatformFrontmostResult, PlatformObserveRequest, PlatformReadTextRequest, PlatformReadTextResponse, PlatformRoot, PlatformRootKind, PlatformRootQuery, PlatformTarget, PlatformWaitForRequest, PlatformWaitForResponse } from "../types.ts";
5
+ import { macosHelper } from "./helper.ts";
6
+
7
+ function parseApps(result: unknown): PlatformApp[] {
8
+ const array = Array.isArray(result) ? result : (result as any)?.apps;
9
+ if (!Array.isArray(array)) return [];
10
+
11
+ return array
12
+ .map((raw) => {
13
+ const pid = Math.trunc(toFiniteNumber((raw as any)?.pid, NaN));
14
+ if (!Number.isFinite(pid) || pid <= 0) return undefined;
15
+ const appName = toOptionalString((raw as any)?.appName) ?? "Unknown App";
16
+ return {
17
+ appName,
18
+ bundleId: toOptionalString((raw as any)?.bundleId),
19
+ pid,
20
+ isFrontmost: toBoolean((raw as any)?.isFrontmost),
21
+ } as PlatformApp;
22
+ })
23
+ .filter((item): item is PlatformApp => Boolean(item));
24
+ }
25
+
26
+ function parseFramePoints(raw: unknown): FramePoints {
27
+ const frame = (raw as any)?.framePoints ?? {};
28
+ return {
29
+ x: toFiniteNumber(frame.x, 0),
30
+ y: toFiniteNumber(frame.y, 0),
31
+ w: Math.max(1, toFiniteNumber(frame.w, 1)),
32
+ h: Math.max(1, toFiniteNumber(frame.h, 1)),
33
+ };
34
+ }
35
+
36
+ function parseRoots(result: unknown): PlatformRoot[] {
37
+ const array = Array.isArray(result) ? result : (result as any)?.roots;
38
+ if (!Array.isArray(array)) return [];
39
+
40
+ return array.map((raw) => {
41
+ const metadata = typeof (raw as any)?.metadata === "object" && (raw as any).metadata !== null ? (raw as any).metadata as Record<string, unknown> : {};
42
+ const kind = ["window", "menu", "sheet", "popover", "dialog"].includes((raw as any)?.kind) ? (raw as any).kind as PlatformRootKind : "window";
43
+ return {
44
+ kind,
45
+ rootRef: toOptionalString((raw as any)?.rootRef ?? (raw as any)?.windowRef),
46
+ windowRef: toOptionalString((raw as any)?.windowRef ?? (raw as any)?.rootRef),
47
+ windowId: Number.isFinite((raw as any)?.windowId) ? Math.trunc((raw as any).windowId) : undefined,
48
+ pid: Number.isFinite((raw as any)?.pid) ? Math.trunc((raw as any).pid) : undefined,
49
+ appName: toOptionalString((raw as any)?.appName),
50
+ bundleId: toOptionalString((raw as any)?.bundleId),
51
+ title: toOptionalString((raw as any)?.title) ?? "",
52
+ role: toOptionalString((raw as any)?.role),
53
+ subrole: toOptionalString((raw as any)?.subrole),
54
+ framePoints: parseFramePoints(raw),
55
+ scaleFactor: Math.max(1, toFiniteNumber((raw as any)?.scaleFactor, 1)),
56
+ zOrder: Math.trunc(toFiniteNumber((raw as any)?.zOrder, 0)),
57
+ isMinimized: toBoolean((raw as any)?.isMinimized),
58
+ isOnscreen: toBoolean((raw as any)?.isOnscreen),
59
+ isMain: toBoolean((raw as any)?.isMain),
60
+ isFocused: toBoolean((raw as any)?.isFocused),
61
+ isModal: toBoolean((raw as any)?.isModal),
62
+ metadata,
63
+ };
64
+ });
65
+ }
66
+
67
+ function helperAction(request: PlatformActRequest): Record<string, unknown> {
68
+ if (!("focus" in request.target)) return { ...request };
69
+ return { ...request, target: request.target.focus, params: { ...request.params, preserveFocus: true } };
70
+ }
71
+
72
+ export const macosBackend: Pick<ComputerUsePlatformBackend, "listApps" | "listRoots" | "getFrontmost" | "focusWindow" | "observe" | "act" | "actBatch" | "readText" | "waitFor"> = {
73
+ async listApps(signal?: AbortSignal): Promise<PlatformApp[]> {
74
+ return parseApps(await macosHelper.command<unknown>("listApps", {}, { signal }));
75
+ },
76
+
77
+ async listRoots(query: PlatformRootQuery, signal?: AbortSignal): Promise<PlatformRoot[]> {
78
+ return parseRoots(await macosHelper.command<unknown>("listRoots", {
79
+ ...(Number.isFinite(query.pid) ? { pid: Math.trunc(query.pid!) } : {}),
80
+ ...(query.title?.trim() ? { title: query.title.trim() } : {}),
81
+ }, { signal }));
82
+ },
83
+
84
+ async getFrontmost(signal?: AbortSignal): Promise<PlatformFrontmostResult> {
85
+ const result = await macosHelper.command<any>("getFrontmost", {}, { signal });
86
+ const pid = Math.trunc(toFiniteNumber(result?.pid, NaN));
87
+ if (!Number.isFinite(pid) || pid <= 0) {
88
+ throw new Error("No frontmost app was available for screenshot targeting.");
89
+ }
90
+ return {
91
+ appName: toOptionalString(result?.appName) ?? "Unknown App",
92
+ bundleId: toOptionalString(result?.bundleId),
93
+ pid,
94
+ windowTitle: toOptionalString(result?.windowTitle),
95
+ windowId: Number.isFinite(result?.windowId) ? Math.trunc(result.windowId) : undefined,
96
+ };
97
+ },
98
+
99
+ async focusWindow(target: PlatformTarget, signal?: AbortSignal): Promise<PlatformFocusWindowResult> {
100
+ return await macosHelper.command<PlatformFocusWindowResult>("focusWindow", { ...target }, { signal });
101
+ },
102
+
103
+ async observe(request: PlatformObserveRequest, options?: { timeoutMs?: number; signal?: AbortSignal }): Promise<LookResponse> {
104
+ return parseLookResponse(await macosHelper.command("look", {
105
+ baseLookId: request.baseLookId,
106
+ windowId: request.target.windowId,
107
+ windowRef: request.target.rootRef,
108
+ maxDimension: request.maxDimension,
109
+ readText: request.readText,
110
+ scopeRef: request.scopeRef,
111
+ includeImage: request.includeImage,
112
+ }, options));
113
+ },
114
+
115
+ async act(request: PlatformActRequest, options?: { timeoutMs?: number; signal?: AbortSignal }): Promise<HelperActResult> {
116
+ return await macosHelper.command<HelperActResult>("act", { ...helperAction(request), cursorOverlay: getComputerUseConfig().cursor_overlay }, options);
117
+ },
118
+
119
+ async actBatch(requests: PlatformActRequest[], options?: { timeoutMs?: number; signal?: AbortSignal }): Promise<HelperActResult> {
120
+ const cursorOverlay = getComputerUseConfig().cursor_overlay;
121
+ return await macosHelper.command<HelperActResult>("actBatch", { actions: requests.map((request) => ({ ...helperAction(request), cursorOverlay })) }, options);
122
+ },
123
+
124
+ async readText(args: PlatformReadTextRequest, options?: { timeoutMs?: number; signal?: AbortSignal }): Promise<PlatformReadTextResponse> {
125
+ return await macosHelper.command("axReadText", { ...args }, options);
126
+ },
127
+
128
+ async waitFor(args: PlatformWaitForRequest, options?: { timeoutMs?: number; signal?: AbortSignal }): Promise<PlatformWaitForResponse> {
129
+ return await macosHelper.command("axWaitFor", { ...args }, options);
130
+ },
131
+ };
@@ -0,0 +1,110 @@
1
+ import { runProcess } from "./helper.ts";
2
+
3
+ const BROWSER_WINDOW_OPEN_TIMEOUT_MS = 10_000;
4
+
5
+ const BROWSER_BUNDLE_IDS = new Set([
6
+ "com.apple.Safari",
7
+ "com.google.Chrome",
8
+ "org.chromium.Chromium",
9
+ "company.thebrowser.Browser",
10
+ "com.brave.Browser",
11
+ "com.microsoft.edgemac",
12
+ "com.vivaldi.Vivaldi",
13
+ "net.imput.helium",
14
+ "org.mozilla.firefox",
15
+ ]);
16
+ const BROWSER_APP_NAMES = new Set([
17
+ "safari",
18
+ "google chrome",
19
+ "chrome",
20
+ "chromium",
21
+ "arc",
22
+ "brave browser",
23
+ "brave",
24
+ "microsoft edge",
25
+ "edge",
26
+ "vivaldi",
27
+ "helium",
28
+ "firefox",
29
+ ]);
30
+ const CHROME_FAMILY_BUNDLE_IDS = new Set([
31
+ "com.google.Chrome",
32
+ "org.chromium.Chromium",
33
+ "company.thebrowser.Browser",
34
+ "com.brave.Browser",
35
+ "com.microsoft.edgemac",
36
+ "com.vivaldi.Vivaldi",
37
+ "net.imput.helium",
38
+ ]);
39
+ const CHROME_FAMILY_APP_NAMES = new Set([
40
+ "google chrome",
41
+ "chrome",
42
+ "chromium",
43
+ "arc",
44
+ "brave browser",
45
+ "brave",
46
+ "microsoft edge",
47
+ "edge",
48
+ "vivaldi",
49
+ "helium",
50
+ ]);
51
+
52
+ function normalizeText(value: string): string {
53
+ return value.trim().toLowerCase();
54
+ }
55
+
56
+ function escapeAppleScriptString(value: string): string {
57
+ return value.replaceAll("\\", "\\\\").replaceAll('"', '\\"');
58
+ }
59
+
60
+ function appendBrowserJavaScriptAppleEventsHint(error: Error): Error {
61
+ const hint = [
62
+ "Browser JavaScript Apple Events are disabled for the target browser.",
63
+ "Ask the user to enable \"Allow JavaScript from Apple Events\" in the browser's developer menu, then retry the browser action.",
64
+ ].join(" ");
65
+ if (!/not allowed to send javascript commands|executing javascript through applescript is turned off|allow javascript from apple events|enable javascript from apple events/i.test(error.message) || error.message.includes(hint)) {
66
+ return error;
67
+ }
68
+ const enhanced = new Error(`${error.message}\n\n${hint}`);
69
+ enhanced.name = error.name;
70
+ return enhanced;
71
+ }
72
+
73
+ export function isBrowserApp(appName: string, bundleId?: string): boolean {
74
+ return BROWSER_BUNDLE_IDS.has(bundleId ?? "") || BROWSER_APP_NAMES.has(normalizeText(appName));
75
+ }
76
+
77
+ export function isChromeFamilyApp(appName: string, bundleId?: string): boolean {
78
+ return CHROME_FAMILY_BUNDLE_IDS.has(bundleId ?? "") || CHROME_FAMILY_APP_NAMES.has(normalizeText(appName));
79
+ }
80
+
81
+ function scriptForBrowserOpenLocation(target: { appName: string; bundleId?: string }, url: string): string[] | undefined {
82
+ const appTarget = target.bundleId
83
+ ? `application id "${escapeAppleScriptString(target.bundleId)}"`
84
+ : `application "${escapeAppleScriptString(target.appName)}"`;
85
+ const escapedUrl = escapeAppleScriptString(url);
86
+ const normalizedName = normalizeText(target.appName);
87
+ if (target.bundleId === "com.apple.Safari" || normalizedName === "safari") {
88
+ return [`tell ${appTarget} to set URL of front document to "${escapedUrl}"`];
89
+ }
90
+ if (isChromeFamilyApp(target.appName, target.bundleId)) {
91
+ return [`tell ${appTarget} to set URL of active tab of front window to "${escapedUrl}"`];
92
+ }
93
+ return undefined;
94
+ }
95
+
96
+ export async function openBrowserLocationWithAppleScript(
97
+ target: { appName: string; bundleId?: string },
98
+ url: string,
99
+ signal?: AbortSignal,
100
+ ): Promise<boolean> {
101
+ const script = scriptForBrowserOpenLocation(target, url);
102
+ if (!script) return false;
103
+ const args = script.flatMap((line) => ["-e", line]);
104
+ try {
105
+ await runProcess("osascript", args, BROWSER_WINDOW_OPEN_TIMEOUT_MS, signal);
106
+ return true;
107
+ } catch (error) {
108
+ throw appendBrowserJavaScriptAppleEventsHint(error instanceof Error ? error : new Error(String(error)));
109
+ }
110
+ }
@@ -0,0 +1,9 @@
1
+ export interface MacosHelperPathOptions {
2
+ env?: NodeJS.ProcessEnv;
3
+ homeDir?: string;
4
+ systemHelperAppPath?: string;
5
+ fileExists?: (filePath: string) => boolean;
6
+ directoryIsWritable?: (directoryPath: string) => boolean;
7
+ }
8
+
9
+ export function resolveMacosHelperAppPath(options?: MacosHelperPathOptions): string;
@@ -0,0 +1,34 @@
1
+ import { accessSync, constants as fsConstants, existsSync } from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+
5
+ const HELPER_APP_NAME = "tangu-computer-use.app";
6
+ const SYSTEM_HELPER_APP_PATH = path.join("/Applications", HELPER_APP_NAME);
7
+
8
+ /**
9
+ * Resolve one stable helper location for both installation and runtime use.
10
+ * Existing writable system installs stay in place; all other installs are
11
+ * per-user so standard macOS accounts never require administrator access.
12
+ */
13
+ export function resolveMacosHelperAppPath(options = {}) {
14
+ const env = options.env ?? process.env;
15
+ const explicitPath = env.PI_COMPUTER_USE_HELPER_APP_PATH?.trim();
16
+ if (explicitPath) return path.resolve(explicitPath);
17
+
18
+ const homeDir = options.homeDir ?? os.homedir();
19
+ const systemHelperAppPath = options.systemHelperAppPath ?? SYSTEM_HELPER_APP_PATH;
20
+ const fileExists = options.fileExists ?? existsSync;
21
+ const directoryIsWritable = options.directoryIsWritable ?? ((directoryPath) => {
22
+ try {
23
+ accessSync(directoryPath, fsConstants.W_OK);
24
+ return true;
25
+ } catch {
26
+ return false;
27
+ }
28
+ });
29
+
30
+ if (fileExists(systemHelperAppPath) && directoryIsWritable(path.dirname(systemHelperAppPath))) {
31
+ return systemHelperAppPath;
32
+ }
33
+ return path.join(homeDir, "Applications", HELPER_APP_NAME);
34
+ }