@cydm/pie 2.2.0 → 2.4.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.
- package/dist/builtin/extensions/ask-user/index.js +3 -3
- package/dist/builtin/extensions/computer-use/index.js +49 -12
- package/dist/builtin/extensions/computer-use/package.json +3 -0
- package/dist/builtin/extensions/computer-use/prebuilt/macos/arm64/pie-computer-use +0 -0
- package/dist/builtin/extensions/computer-use/prebuilt/macos/universal/pie-computer-use.app/Contents/MacOS/pie-computer-use +0 -0
- package/dist/builtin/extensions/computer-use/prebuilt/macos/x64/pie-computer-use +0 -0
- package/dist/builtin/extensions/computer-use/scripts/build-native.mjs +2 -1
- package/dist/builtin/extensions/computer-use/scripts/setup-helper.mjs +2 -2
- package/dist/builtin/extensions/computer-use/scripts/test-native-retention.mjs +204 -0
- package/dist/builtin/extensions/herdr-agent-state/index.js +272 -0
- package/dist/builtin/extensions/herdr-agent-state/package.json +11 -0
- package/dist/builtin/extensions/mcp-adapter/index.js +188 -74
- package/dist/builtin/extensions/plan-mode/index.js +3 -3
- package/dist/builtin/extensions/subagent/index.js +5 -5
- package/dist/builtin/extensions/todo/index.js +3 -3
- package/dist/builtin/skills/chrome-devtools-axi/chrome-devtools-axi-runtime.js +1 -1
- package/dist/builtin/skills/chrome-devtools-axi/{cli-F2VCSVL4.js → cli-P4VG6ZZG.js} +48 -8
- package/dist/builtin/skills/skill-creator/scripts/pie_runner.mjs +33 -12
- package/dist/builtin/skills/tui-use/tui-use-daemon.cjs +5 -7
- package/dist/chunks/{chunk-HH2Q2SNX.js → chunk-FKUEIYFQ.js} +8213 -7367
- package/dist/chunks/{chunk-G3SE3I2L.js → chunk-XVUV3IM2.js} +112 -40
- package/dist/chunks/{chunk-IM43UYVV.js → chunk-YRXXRLMQ.js} +75 -14
- package/dist/chunks/{chunk-QJH6YN2W.js → chunk-ZKHMNEXL.js} +1 -1
- package/dist/chunks/{src-QSP2OPHU.js → src-SXUQ7OY6.js} +2 -2
- package/dist/chunks/{test-stream-CVGWFLSX.js → test-stream-OD3B57AS.js} +1 -1
- package/dist/cli.js +1321 -894
- package/package.json +2 -2
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { createRequire as __createRequire } from "node:module"; const require = __createRequire(import.meta.url);
|
|
2
2
|
import {
|
|
3
3
|
createAskUserCapability
|
|
4
|
-
} from "../../../chunks/chunk-
|
|
5
|
-
import "../../../chunks/chunk-
|
|
6
|
-
import "../../../chunks/chunk-
|
|
4
|
+
} from "../../../chunks/chunk-FKUEIYFQ.js";
|
|
5
|
+
import "../../../chunks/chunk-ZKHMNEXL.js";
|
|
6
|
+
import "../../../chunks/chunk-YRXXRLMQ.js";
|
|
7
7
|
import "../../../chunks/chunk-XXGYUYXI.js";
|
|
8
8
|
import "../../../chunks/chunk-LZQXQOPJ.js";
|
|
9
9
|
|
|
@@ -3776,10 +3776,26 @@ function containsEditable(node) {
|
|
|
3776
3776
|
if (node.canSetValue || node.role.toLowerCase().includes("text")) return true;
|
|
3777
3777
|
return node.children.some(containsEditable);
|
|
3778
3778
|
}
|
|
3779
|
+
function wireableInputDescendants(node) {
|
|
3780
|
+
const matches = [];
|
|
3781
|
+
const visit = (current) => {
|
|
3782
|
+
for (const child of current.children) {
|
|
3783
|
+
if (child.isTextInput && child.wireRef && !child.pictureOnly && !child.offscreen) matches.push(child);
|
|
3784
|
+
visit(child);
|
|
3785
|
+
}
|
|
3786
|
+
};
|
|
3787
|
+
visit(node);
|
|
3788
|
+
return matches;
|
|
3789
|
+
}
|
|
3779
3790
|
function recordActionFocus(action, state, node, coordinateFocus = false) {
|
|
3780
3791
|
if (!["click", "press", "keypress", "typeText"].includes(action.action) || !action.ref && action.action !== "click") return;
|
|
3781
3792
|
state.currentFocus = Boolean(node?.isTextInput && !node.pictureOnly) || coordinateFocus;
|
|
3782
3793
|
state.focusRef = node?.isTextInput && node.wireRef && !node.pictureOnly ? node.ref : void 0;
|
|
3794
|
+
if (!state.currentFocus && node && !node.pictureOnly && action.ref && (action.action === "click" || action.action === "press") && containsEditable(node)) {
|
|
3795
|
+
const wireable = wireableInputDescendants(node);
|
|
3796
|
+
state.currentFocus = true;
|
|
3797
|
+
state.focusRef = wireable.length === 1 ? wireable[0].ref : void 0;
|
|
3798
|
+
}
|
|
3783
3799
|
}
|
|
3784
3800
|
function editorAtPoint(nodes, x, y) {
|
|
3785
3801
|
const matches = nodes.filter((node) => node.isTextInput && node.wireRef && !node.pictureOnly && !node.offscreen && node.rect && x >= node.rect.x && x < node.rect.x + node.rect.w && y >= node.rect.y && y < node.rect.y + node.rect.h);
|
|
@@ -3790,24 +3806,23 @@ function prepareAction(action, state, env) {
|
|
|
3790
3806
|
const focusedKeyboard = !env.headless && state.currentFocus && !action.ref && (operation === "typeText" || operation === "keypress");
|
|
3791
3807
|
const usesCurrentFocus = focusedKeyboard && !state.focusRef;
|
|
3792
3808
|
const target = usesCurrentFocus ? focusedTarget(env) : nativeTarget(focusedKeyboard && state.focusRef ? { ...action, ref: state.focusRef } : action, operation, env);
|
|
3793
|
-
const establishesFocus = !env.headless && Boolean(action.ref) && (operation === "click" || operation === "press") && containsEditable(env.node(action.ref));
|
|
3794
3809
|
const needsForeground = !env.headless && (operation === "click" || operation === "press") && "x" in target;
|
|
3795
3810
|
switch (operation) {
|
|
3796
3811
|
case "press":
|
|
3797
3812
|
case "click":
|
|
3798
|
-
return { action: operation, target, params: { button: mouseButton(action.button), clickCount: clickCount(action.clickCount) },
|
|
3813
|
+
return { action: operation, target, params: { button: mouseButton(action.button), clickCount: clickCount(action.clickCount) }, usesCurrentFocus: false, needsForeground };
|
|
3799
3814
|
case "setText":
|
|
3800
|
-
return { action: operation, target, params: { text: action.text ?? "" },
|
|
3815
|
+
return { action: operation, target, params: { text: action.text ?? "" }, usesCurrentFocus: false, needsForeground: false };
|
|
3801
3816
|
case "typeText":
|
|
3802
|
-
return { action: operation, target, params: { text: action.text ?? "" },
|
|
3817
|
+
return { action: operation, target, params: { text: action.text ?? "" }, usesCurrentFocus, needsForeground: false };
|
|
3803
3818
|
case "keypress":
|
|
3804
|
-
return { action: operation, target, params: { keys: keys(action.keys) },
|
|
3819
|
+
return { action: operation, target, params: { keys: keys(action.keys) }, usesCurrentFocus, needsForeground: false };
|
|
3805
3820
|
case "scroll":
|
|
3806
|
-
return { action: operation, target, params: { scrollX: scrollDelta(action.scrollX), scrollY: scrollDelta(action.scrollY) },
|
|
3821
|
+
return { action: operation, target, params: { scrollX: scrollDelta(action.scrollX), scrollY: scrollDelta(action.scrollY) }, usesCurrentFocus: false, needsForeground: false };
|
|
3807
3822
|
case "drag":
|
|
3808
|
-
return { action: operation, target, params: { path: path(action.path, env) },
|
|
3823
|
+
return { action: operation, target, params: { path: path(action.path, env) }, usesCurrentFocus: false, needsForeground: false };
|
|
3809
3824
|
case "moveMouse":
|
|
3810
|
-
return { action: operation, target, params: {},
|
|
3825
|
+
return { action: operation, target, params: {}, usesCurrentFocus: false, needsForeground: false };
|
|
3811
3826
|
}
|
|
3812
3827
|
}
|
|
3813
3828
|
function canRetryInForeground(action, outcome, headless) {
|
|
@@ -4405,6 +4420,9 @@ var CdpTab = class _CdpTab {
|
|
|
4405
4420
|
/** Evaluates a JS expression in the page and returns its primitive value. */
|
|
4406
4421
|
async evaluate(expression) {
|
|
4407
4422
|
const result = await this.send("Runtime.evaluate", { expression, returnByValue: true, timeout: COMMAND_TIMEOUT_MS, awaitPromise: true });
|
|
4423
|
+
if (result?.exceptionDetails) {
|
|
4424
|
+
throw new Error(`Evaluation failed: ${exceptionDetailsMessage(result.exceptionDetails)}`);
|
|
4425
|
+
}
|
|
4408
4426
|
return result?.result?.value;
|
|
4409
4427
|
}
|
|
4410
4428
|
async accessibilityTree() {
|
|
@@ -4429,6 +4447,9 @@ var CdpTab = class _CdpTab {
|
|
|
4429
4447
|
async clickBackendNode(backendNodeId) {
|
|
4430
4448
|
await this.withBackendNode(backendNodeId, "function(){ this.scrollIntoView({block:'center', inline:'center'}); this.click(); }");
|
|
4431
4449
|
}
|
|
4450
|
+
async focusBackendNode(backendNodeId) {
|
|
4451
|
+
await this.withBackendNode(backendNodeId, "function(){ this.scrollIntoView({block:'center', inline:'center'}); this.focus(); }");
|
|
4452
|
+
}
|
|
4432
4453
|
async typeIntoBackendNode(backendNodeId, text, replace) {
|
|
4433
4454
|
await this.withBackendNode(backendNodeId, "function(text, replace){ this.scrollIntoView({block:'center', inline:'center'}); this.focus(); if (replace) { if ('value' in this) this.value = ''; else this.textContent = ''; } if ('value' in this) this.value += text; else this.textContent = (this.textContent || '') + text; this.dispatchEvent(new InputEvent('input', {bubbles:true, inputType:'insertText', data:text})); this.dispatchEvent(new Event('change', {bubbles:true})); }", [text, replace]);
|
|
4434
4455
|
}
|
|
@@ -4464,11 +4485,14 @@ var CdpTab = class _CdpTab {
|
|
|
4464
4485
|
const resolved = await this.send("DOM.resolveNode", { backendNodeId });
|
|
4465
4486
|
const objectId = resolved?.object?.objectId;
|
|
4466
4487
|
if (typeof objectId !== "string") throw new Error(`CDP could not resolve backend node ${backendNodeId}.`);
|
|
4467
|
-
await this.send("Runtime.callFunctionOn", {
|
|
4488
|
+
const callResult = await this.send("Runtime.callFunctionOn", {
|
|
4468
4489
|
objectId,
|
|
4469
4490
|
functionDeclaration,
|
|
4470
4491
|
arguments: args.map((value) => ({ value }))
|
|
4471
4492
|
});
|
|
4493
|
+
if (callResult?.exceptionDetails) {
|
|
4494
|
+
throw new Error(`CDP call on backend node ${backendNodeId} failed: ${exceptionDetailsMessage(callResult.exceptionDetails)}`);
|
|
4495
|
+
}
|
|
4472
4496
|
}
|
|
4473
4497
|
/** Screen bounds of the browser window containing this tab. */
|
|
4474
4498
|
async windowBounds() {
|
|
@@ -4655,6 +4679,12 @@ async function cdpTypeFocusedForContext(contextId, text) {
|
|
|
4655
4679
|
return true;
|
|
4656
4680
|
}) === true;
|
|
4657
4681
|
}
|
|
4682
|
+
async function cdpFocusForContext(contextId, backendNodeId) {
|
|
4683
|
+
return await withCdpContextTab(contextId, async (tab) => {
|
|
4684
|
+
await tab.focusBackendNode(backendNodeId);
|
|
4685
|
+
return true;
|
|
4686
|
+
}) === true;
|
|
4687
|
+
}
|
|
4658
4688
|
async function cdpKeypressForContext(contextId, keys2) {
|
|
4659
4689
|
return await withCdpContextTab(contextId, async (tab) => {
|
|
4660
4690
|
await tab.keypress(keys2);
|
|
@@ -4758,6 +4788,13 @@ function axString(raw) {
|
|
|
4758
4788
|
const value = raw?.value ?? raw;
|
|
4759
4789
|
return typeof value === "string" ? value.trim() : "";
|
|
4760
4790
|
}
|
|
4791
|
+
function exceptionDetailsMessage(details) {
|
|
4792
|
+
const exception = details?.exception;
|
|
4793
|
+
if (typeof exception?.description === "string" && exception.description) return exception.description;
|
|
4794
|
+
if (typeof exception?.value === "string" && exception.value) return exception.value;
|
|
4795
|
+
if (typeof details?.text === "string" && details.text) return details.text;
|
|
4796
|
+
return "the script threw an exception.";
|
|
4797
|
+
}
|
|
4761
4798
|
function cdpSnapshotOutline(snapshotId, nodes) {
|
|
4762
4799
|
const records = /* @__PURE__ */ new Map();
|
|
4763
4800
|
for (const raw of nodes) {
|
|
@@ -4774,7 +4811,7 @@ function cdpSnapshotOutline(snapshotId, nodes) {
|
|
|
4774
4811
|
const actions = browserActionsForAxRole(role);
|
|
4775
4812
|
const backendNodeId = Number.isFinite(raw?.backendDOMNodeId) ? Math.trunc(raw.backendDOMNodeId) : void 0;
|
|
4776
4813
|
const wireRef = `cdp:${nodeId}`;
|
|
4777
|
-
if (actions.length > 0 &&
|
|
4814
|
+
if (actions.length > 0 && (!actions.includes("click") || backendNodeId)) {
|
|
4778
4815
|
targets.push({ ref: wireRef, source: "browser_ax", role, name, value: axString(raw?.value) || void 0, actions, backendNodeId });
|
|
4779
4816
|
}
|
|
4780
4817
|
const childIds = Array.isArray(raw?.childIds) ? raw.childIds.map(String) : [];
|
|
@@ -8713,7 +8750,7 @@ async function performBrowserTransaction(params, actions, signal) {
|
|
|
8713
8750
|
if (!BROWSER_TRANSACTION_ACTIONS.has(action.action)) throw new Error(`Browser transactions do not support '${action.action}'.`);
|
|
8714
8751
|
if (action.action === "click" && action.ref && action.button && action.button !== "left") throw new Error("Browser ref clicks support only the left button; use coordinate clicks for right or middle buttons.");
|
|
8715
8752
|
const target = browserSnapshotTarget(params.stateId, trimOrUndefined(action.ref));
|
|
8716
|
-
if ((action.action === "press" || action.action === "setText" || action.action === "click" && action.ref || action.action === "typeText" && action.ref) && !Number.isFinite(target?.backendNodeId)) {
|
|
8753
|
+
if ((action.action === "press" || action.action === "setText" || action.action === "click" && action.ref || action.action === "typeText" && action.ref || action.action === "keypress" && action.ref) && !Number.isFinite(target?.backendNodeId)) {
|
|
8717
8754
|
throw new Error(`Browser ${action.action} requires an actionable @e ref owned by ${params.stateId}.`);
|
|
8718
8755
|
}
|
|
8719
8756
|
if (action.ref && (!target || target.contextId !== contextId)) throw new Error(`Browser ${action.action} ref must be owned by ${params.stateId}.`);
|
|
@@ -8729,7 +8766,7 @@ async function performBrowserTransaction(params, actions, signal) {
|
|
|
8729
8766
|
worked = await cdpMouseForContext(contextId, action.x, action.y, "mousePressed", action.button ?? "left", action.clickCount ?? 1) && await cdpMouseForContext(contextId, action.x, action.y, "mouseReleased", action.button ?? "left", action.clickCount ?? 1);
|
|
8730
8767
|
} else if (action.action === "setText") worked = await cdpTypeForContext(contextId, target.backendNodeId, action.text ?? "", true);
|
|
8731
8768
|
else if (action.action === "typeText") worked = target?.backendNodeId ? await cdpTypeForContext(contextId, target.backendNodeId, action.text ?? "", false) : await cdpTypeFocusedForContext(contextId, action.text ?? "");
|
|
8732
|
-
else if (action.action === "keypress") worked = await cdpKeypressForContext(contextId, action.keys ?? []);
|
|
8769
|
+
else if (action.action === "keypress") worked = target?.backendNodeId ? await cdpFocusForContext(contextId, target.backendNodeId) && await cdpKeypressForContext(contextId, action.keys ?? []) : await cdpKeypressForContext(contextId, action.keys ?? []);
|
|
8733
8770
|
else if (action.action === "scroll") worked = await cdpScrollForContext(contextId, toFiniteNumber(action.scrollX, 0), toFiniteNumber(action.scrollY, 0), target?.backendNodeId);
|
|
8734
8771
|
else if (action.action === "drag") worked = await cdpDragForContext(contextId, normalizeActionPath(action.path));
|
|
8735
8772
|
else if (action.action === "moveMouse") worked = await cdpMouseForContext(contextId, action.x, action.y, "mouseMoved");
|
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
"description": "Desktop computer-use tools (observe/act) vendored from @injaneity/pi-computer-use",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"test:native-retention": "node scripts/test-native-retention.mjs"
|
|
9
|
+
},
|
|
7
10
|
"pie": {
|
|
8
11
|
"name": "computer-use",
|
|
9
12
|
"main": "index.js"
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -11,6 +11,7 @@ const macosSourcePaths = [
|
|
|
11
11
|
"agent_cursor.swift",
|
|
12
12
|
"agent_cursor_motion.swift",
|
|
13
13
|
"bridge.swift",
|
|
14
|
+
"ref_store.swift",
|
|
14
15
|
].map((file) => path.join(rootDir, "native", "macos", file));
|
|
15
16
|
const windowsCrateDir = path.join(rootDir, "native", "windows", "bridge-rs");
|
|
16
17
|
const linuxCrateDir = path.join(rootDir, "native", "linux", "bridge-rs");
|
|
@@ -20,7 +21,7 @@ const archTriples = {
|
|
|
20
21
|
x64: "x86_64-apple-macosx",
|
|
21
22
|
};
|
|
22
23
|
const deploymentTarget = "14.0";
|
|
23
|
-
const frameworks = ["ApplicationServices", "AppKit", "ScreenCaptureKit", "Foundation", "SwiftUI"];
|
|
24
|
+
const frameworks = ["ApplicationServices", "AppKit", "ScreenCaptureKit", "Foundation", "SwiftUI", "CryptoKit"];
|
|
24
25
|
const defaultCodeSignIdentifier = "com.pie.computer-use";
|
|
25
26
|
|
|
26
27
|
async function exists(filePath) {
|
|
@@ -20,7 +20,7 @@ const windowsCrateDir = path.join(rootDir, "native", "windows", "bridge-rs");
|
|
|
20
20
|
const windowsHelperDestPath = process.env.PIE_COMPUTER_USE_WINDOWS_HELPER_PATH || path.join(resolvePieConfigDir(), "helpers", "computer-use", "windows-bridge.exe");
|
|
21
21
|
const linuxCrateDir = path.join(rootDir, "native", "linux", "bridge-rs");
|
|
22
22
|
const linuxHelperDestPath = process.env.PIE_COMPUTER_USE_LINUX_HELPER_PATH || path.join(resolvePieConfigDir(), "helpers", "computer-use", "linux-bridge");
|
|
23
|
-
const helperSourcePaths = ["agent_cursor.swift", "agent_cursor_motion.swift", "bridge.swift"]
|
|
23
|
+
const helperSourcePaths = ["agent_cursor.swift", "agent_cursor_motion.swift", "bridge.swift", "ref_store.swift"]
|
|
24
24
|
.map((file) => path.join(rootDir, "native", "macos", file));
|
|
25
25
|
const packageJsonPath = path.join(rootDir, "package.json");
|
|
26
26
|
const localCodeSignCommonName = "pie-computer-use Local Signing (com.pie.computer-use)";
|
|
@@ -57,7 +57,7 @@ const archTriples = {
|
|
|
57
57
|
x64: "x86_64-apple-macosx",
|
|
58
58
|
};
|
|
59
59
|
const deploymentTarget = "14.0";
|
|
60
|
-
const frameworks = ["ApplicationServices", "AppKit", "ScreenCaptureKit", "Foundation", "SwiftUI"];
|
|
60
|
+
const frameworks = ["ApplicationServices", "AppKit", "ScreenCaptureKit", "Foundation", "SwiftUI", "CryptoKit"];
|
|
61
61
|
const defaultCodeSignIdentifier = "com.pie.computer-use";
|
|
62
62
|
|
|
63
63
|
function normalizeArch(arch) {
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// Compiles and runs the macOS native AXRefStore retention tests
|
|
4
|
+
// (native/macos/retention_tests.swift) from source, then compiles the full
|
|
5
|
+
// helper and drives its stdin JSON-lines transport with permission-free
|
|
6
|
+
// requests to assert the bridge-level stale-ref/stale-look integration.
|
|
7
|
+
//
|
|
8
|
+
// Nothing here needs Accessibility or Screen Recording permission or real UI
|
|
9
|
+
// interaction: the store tests use synthetic AXUIElement values, and the
|
|
10
|
+
// helper protocol probes only exercise failure paths (unknown look/element
|
|
11
|
+
// ids, missing windows). This script is macOS-only by design; on other
|
|
12
|
+
// platforms it skips with an explicit message (Linux/Windows helpers have
|
|
13
|
+
// their own native suites).
|
|
14
|
+
|
|
15
|
+
import { spawn } from "node:child_process";
|
|
16
|
+
import fs from "node:fs/promises";
|
|
17
|
+
import os from "node:os";
|
|
18
|
+
import path from "node:path";
|
|
19
|
+
import { fileURLToPath } from "node:url";
|
|
20
|
+
|
|
21
|
+
const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
22
|
+
const nativeDir = path.join(rootDir, "native", "macos");
|
|
23
|
+
const storeTestSources = ["ref_store.swift", "retention_tests.swift"].map((file) => path.join(nativeDir, file));
|
|
24
|
+
const helperSources = ["agent_cursor.swift", "agent_cursor_motion.swift", "bridge.swift", "ref_store.swift"].map((file) => path.join(nativeDir, file));
|
|
25
|
+
const deploymentTarget = "14.0";
|
|
26
|
+
const helperFrameworks = ["ApplicationServices", "AppKit", "ScreenCaptureKit", "Foundation", "SwiftUI", "CryptoKit"];
|
|
27
|
+
|
|
28
|
+
function run(command, args, options = {}) {
|
|
29
|
+
return new Promise((resolve, reject) => {
|
|
30
|
+
const child = spawn(command, args, { stdio: "inherit", ...options });
|
|
31
|
+
child.on("error", reject);
|
|
32
|
+
child.on("close", (code) => {
|
|
33
|
+
if (code === 0) {
|
|
34
|
+
resolve();
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
reject(new Error(`Command failed (${code}): ${command} ${args.join(" ")}`));
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function swiftcArgs(sources, output, moduleCache, frameworks) {
|
|
43
|
+
const args = [
|
|
44
|
+
"swiftc",
|
|
45
|
+
"-target",
|
|
46
|
+
`${process.arch === "arm64" ? "arm64" : "x86_64"}-apple-macosx${deploymentTarget}`,
|
|
47
|
+
"-module-cache-path",
|
|
48
|
+
moduleCache,
|
|
49
|
+
"-O",
|
|
50
|
+
];
|
|
51
|
+
for (const framework of frameworks) args.push("-framework", framework);
|
|
52
|
+
args.push(...sources, "-o", output);
|
|
53
|
+
return args;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Drives the helper over its stdin transport and resolves one response per request id. */
|
|
57
|
+
function helperResponses(executable, requests, timeoutMs = 15_000) {
|
|
58
|
+
return new Promise((resolve, reject) => {
|
|
59
|
+
const child = spawn(executable, [], { stdio: ["pipe", "pipe", "ignore"] });
|
|
60
|
+
const pending = new Map(requests.map((request) => [request.id, request]));
|
|
61
|
+
const settled = new Map();
|
|
62
|
+
const fail = (error) => {
|
|
63
|
+
child.kill();
|
|
64
|
+
reject(error);
|
|
65
|
+
};
|
|
66
|
+
const timer = setTimeout(() => fail(new Error(`Helper did not answer all requests within ${timeoutMs}ms`)), timeoutMs);
|
|
67
|
+
let buffer = "";
|
|
68
|
+
child.stdout.setEncoding("utf8");
|
|
69
|
+
child.stdout.on("data", (chunk) => {
|
|
70
|
+
buffer += chunk;
|
|
71
|
+
let newline;
|
|
72
|
+
while ((newline = buffer.indexOf("\n")) >= 0) {
|
|
73
|
+
const line = buffer.slice(0, newline);
|
|
74
|
+
buffer = buffer.slice(newline + 1);
|
|
75
|
+
if (!line.trim()) continue;
|
|
76
|
+
let response;
|
|
77
|
+
try {
|
|
78
|
+
response = JSON.parse(line);
|
|
79
|
+
} catch {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (typeof response.id === "string" && pending.delete(response.id)) {
|
|
83
|
+
settled.set(response.id, response);
|
|
84
|
+
if (pending.size === 0) {
|
|
85
|
+
clearTimeout(timer);
|
|
86
|
+
child.stdin.end();
|
|
87
|
+
child.on("close", () => resolve(settled));
|
|
88
|
+
child.kill();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
child.on("error", fail);
|
|
94
|
+
for (const request of requests) child.stdin.write(`${JSON.stringify(request)}\n`);
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Verifies the helper's true-exit path: shutdown answers, then the process exits zero on its own. */
|
|
99
|
+
function helperShutdownExits(executable, timeoutMs = 8_000) {
|
|
100
|
+
return new Promise((resolve, reject) => {
|
|
101
|
+
const child = spawn(executable, [], { stdio: ["pipe", "pipe", "ignore"] });
|
|
102
|
+
let buffer = "";
|
|
103
|
+
let sawResponse = false;
|
|
104
|
+
const fail = (error) => {
|
|
105
|
+
child.kill();
|
|
106
|
+
reject(error);
|
|
107
|
+
};
|
|
108
|
+
const timer = setTimeout(() => fail(new Error(`Helper did not shut down within ${timeoutMs}ms`)), timeoutMs);
|
|
109
|
+
child.stdout.setEncoding("utf8");
|
|
110
|
+
child.stdout.on("data", (chunk) => {
|
|
111
|
+
buffer += chunk;
|
|
112
|
+
let newline;
|
|
113
|
+
while ((newline = buffer.indexOf("\n")) >= 0) {
|
|
114
|
+
const line = buffer.slice(0, newline);
|
|
115
|
+
buffer = buffer.slice(newline + 1);
|
|
116
|
+
if (!line.trim()) continue;
|
|
117
|
+
let response;
|
|
118
|
+
try {
|
|
119
|
+
response = JSON.parse(line);
|
|
120
|
+
} catch {
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (response.id === "shutdown" && response.ok === true && response.result?.shuttingDown === true) sawResponse = true;
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
child.on("error", fail);
|
|
127
|
+
child.on("close", (code, signal) => {
|
|
128
|
+
clearTimeout(timer);
|
|
129
|
+
if (!sawResponse) {
|
|
130
|
+
reject(new Error("Helper did not acknowledge shutdown before exiting"));
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (code === 0) {
|
|
134
|
+
resolve();
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
reject(new Error(`Helper exited with ${code ?? "null"}${signal ? ` (signal ${signal})` : ""} instead of 0`));
|
|
138
|
+
});
|
|
139
|
+
child.stdin.write(`${JSON.stringify({ id: "shutdown", cmd: "shutdown" })}\n`);
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function assert(condition, message) {
|
|
144
|
+
if (!condition) throw new Error(`[pie-computer-use] bridge integration assertion failed: ${message}`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function runBridgeIntegration(helperPath) {
|
|
148
|
+
const responses = await helperResponses(helperPath, [
|
|
149
|
+
{ id: "diag", cmd: "diagnostics" },
|
|
150
|
+
{ id: "act-stale", cmd: "act", lookId: "look_999", pid: 123, action: "press", target: { ref: "e1" } },
|
|
151
|
+
{ id: "batch-stale", cmd: "actBatch", actions: [{ lookId: "look_999", pid: 123, action: "press", target: { ref: "e1" } }] },
|
|
152
|
+
{ id: "hit-stale", cmd: "hitTest", lookId: "look_999", x: 10, y: 10 },
|
|
153
|
+
{ id: "read-stale", cmd: "axReadText", elementRef: "e999999" },
|
|
154
|
+
{ id: "look-stale-base", cmd: "look", baseLookId: "look_999", windowId: 12345 },
|
|
155
|
+
]);
|
|
156
|
+
const diagnostics = responses.get("diag");
|
|
157
|
+
assert(diagnostics?.ok === true, "diagnostics must succeed");
|
|
158
|
+
assert(diagnostics.result?.protocolVersion === 6, "helper must keep protocol version 6");
|
|
159
|
+
assert(Array.isArray(diagnostics.result?.invariants) && diagnostics.result.invariants.includes("bounded-observation-history"), "helper must report the bounded-observation-history invariant");
|
|
160
|
+
for (const [id, expectedCode] of [
|
|
161
|
+
["act-stale", "stale_look"],
|
|
162
|
+
["batch-stale", "stale_look"],
|
|
163
|
+
["hit-stale", "stale_look"],
|
|
164
|
+
["read-stale", "element_ref_invalid"],
|
|
165
|
+
["look-stale-base", "stale_look"],
|
|
166
|
+
]) {
|
|
167
|
+
const response = responses.get(id);
|
|
168
|
+
assert(response?.ok === false, `${id} must fail`);
|
|
169
|
+
assert(response.error?.code === expectedCode, `${id} must fail with ${expectedCode}, got ${response.error?.code}`);
|
|
170
|
+
}
|
|
171
|
+
console.log("[pie-computer-use] bridge integration probes passed (stale look/ref failures, protocol v6).");
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async function main() {
|
|
175
|
+
if (process.platform !== "darwin") {
|
|
176
|
+
console.log(`[pie-computer-use] skipping native retention tests: they are macOS-only and cannot run on ${process.platform}.`);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
let tempDir;
|
|
180
|
+
try {
|
|
181
|
+
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "pie-computer-use-retention-"));
|
|
182
|
+
const moduleCache = path.join(tempDir, "module-cache");
|
|
183
|
+
const storeTestOutput = path.join(tempDir, "retention_tests");
|
|
184
|
+
console.log("[pie-computer-use] compiling native retention tests...");
|
|
185
|
+
await run("xcrun", swiftcArgs(storeTestSources, storeTestOutput, moduleCache, ["ApplicationServices", "CryptoKit"]));
|
|
186
|
+
console.log("[pie-computer-use] running native retention tests...");
|
|
187
|
+
await run(storeTestOutput, []);
|
|
188
|
+
|
|
189
|
+
const helperOutput = path.join(tempDir, "pie-computer-use");
|
|
190
|
+
console.log("[pie-computer-use] compiling native helper for bridge integration probes...");
|
|
191
|
+
await run("xcrun", swiftcArgs(helperSources, helperOutput, moduleCache, helperFrameworks));
|
|
192
|
+
await runBridgeIntegration(helperOutput);
|
|
193
|
+
await helperShutdownExits(helperOutput);
|
|
194
|
+
console.log("[pie-computer-use] helper shutdown exits cleanly with code 0.");
|
|
195
|
+
console.log("[pie-computer-use] native retention tests passed.");
|
|
196
|
+
} finally {
|
|
197
|
+
if (tempDir) await fs.rm(tempDir, { recursive: true, force: true }).catch(() => {});
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
main().catch((error) => {
|
|
202
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
203
|
+
process.exit(1);
|
|
204
|
+
});
|