@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.
Files changed (27) hide show
  1. package/dist/builtin/extensions/ask-user/index.js +3 -3
  2. package/dist/builtin/extensions/computer-use/index.js +49 -12
  3. package/dist/builtin/extensions/computer-use/package.json +3 -0
  4. package/dist/builtin/extensions/computer-use/prebuilt/macos/arm64/pie-computer-use +0 -0
  5. package/dist/builtin/extensions/computer-use/prebuilt/macos/universal/pie-computer-use.app/Contents/MacOS/pie-computer-use +0 -0
  6. package/dist/builtin/extensions/computer-use/prebuilt/macos/x64/pie-computer-use +0 -0
  7. package/dist/builtin/extensions/computer-use/scripts/build-native.mjs +2 -1
  8. package/dist/builtin/extensions/computer-use/scripts/setup-helper.mjs +2 -2
  9. package/dist/builtin/extensions/computer-use/scripts/test-native-retention.mjs +204 -0
  10. package/dist/builtin/extensions/herdr-agent-state/index.js +272 -0
  11. package/dist/builtin/extensions/herdr-agent-state/package.json +11 -0
  12. package/dist/builtin/extensions/mcp-adapter/index.js +188 -74
  13. package/dist/builtin/extensions/plan-mode/index.js +3 -3
  14. package/dist/builtin/extensions/subagent/index.js +5 -5
  15. package/dist/builtin/extensions/todo/index.js +3 -3
  16. package/dist/builtin/skills/chrome-devtools-axi/chrome-devtools-axi-runtime.js +1 -1
  17. package/dist/builtin/skills/chrome-devtools-axi/{cli-F2VCSVL4.js → cli-P4VG6ZZG.js} +48 -8
  18. package/dist/builtin/skills/skill-creator/scripts/pie_runner.mjs +33 -12
  19. package/dist/builtin/skills/tui-use/tui-use-daemon.cjs +5 -7
  20. package/dist/chunks/{chunk-HH2Q2SNX.js → chunk-FKUEIYFQ.js} +8213 -7367
  21. package/dist/chunks/{chunk-G3SE3I2L.js → chunk-XVUV3IM2.js} +112 -40
  22. package/dist/chunks/{chunk-IM43UYVV.js → chunk-YRXXRLMQ.js} +75 -14
  23. package/dist/chunks/{chunk-QJH6YN2W.js → chunk-ZKHMNEXL.js} +1 -1
  24. package/dist/chunks/{src-QSP2OPHU.js → src-SXUQ7OY6.js} +2 -2
  25. package/dist/chunks/{test-stream-CVGWFLSX.js → test-stream-OD3B57AS.js} +1 -1
  26. package/dist/cli.js +1321 -894
  27. 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-HH2Q2SNX.js";
5
- import "../../../chunks/chunk-QJH6YN2W.js";
6
- import "../../../chunks/chunk-IM43UYVV.js";
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) }, establishesFocus, usesCurrentFocus: false, needsForeground };
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 ?? "" }, establishesFocus: false, usesCurrentFocus: false, needsForeground: false };
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 ?? "" }, establishesFocus: false, usesCurrentFocus, needsForeground: false };
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) }, establishesFocus: false, usesCurrentFocus, needsForeground: false };
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) }, establishesFocus: false, usesCurrentFocus: false, needsForeground: false };
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) }, establishesFocus: false, usesCurrentFocus: false, needsForeground: false };
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: {}, establishesFocus: false, usesCurrentFocus: false, needsForeground: false };
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 && name && (!actions.includes("click") || backendNodeId)) {
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"
@@ -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
+ });