screenhand 0.1.1 → 0.3.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 (241) hide show
  1. package/README.md +193 -109
  2. package/bin/darwin-arm64/macos-bridge +0 -0
  3. package/dist/mcp-desktop.js +5876 -0
  4. package/dist/scripts/codex-monitor-daemon.js +335 -0
  5. package/dist/scripts/export-help-center.js +112 -0
  6. package/dist/scripts/marketing-loop.js +117 -0
  7. package/dist/scripts/observer-daemon.js +288 -0
  8. package/dist/scripts/orchestrator-daemon.js +399 -0
  9. package/dist/scripts/supervisor-daemon.js +272 -0
  10. package/dist/scripts/threads-campaign.js +208 -0
  11. package/dist/scripts/worker-daemon.js +228 -0
  12. package/dist/src/agent/cli.js +82 -0
  13. package/dist/src/agent/loop.js +274 -0
  14. package/dist/src/community/fetcher.js +109 -0
  15. package/dist/src/community/index.js +6 -0
  16. package/dist/src/community/publisher.js +191 -0
  17. package/dist/src/community/remote-api.js +121 -0
  18. package/dist/src/community/types.js +3 -0
  19. package/dist/src/community/validator.js +95 -0
  20. package/{src/config.ts → dist/src/config.js} +5 -10
  21. package/dist/src/context-tracker.js +489 -0
  22. package/{src/index.ts → dist/src/index.js} +32 -52
  23. package/dist/src/ingestion/coverage-auditor.js +233 -0
  24. package/dist/src/ingestion/doc-parser.js +164 -0
  25. package/dist/src/ingestion/index.js +8 -0
  26. package/dist/src/ingestion/menu-scanner.js +152 -0
  27. package/dist/src/ingestion/reference-merger.js +186 -0
  28. package/dist/src/ingestion/shortcut-extractor.js +180 -0
  29. package/dist/src/ingestion/tutorial-extractor.js +170 -0
  30. package/dist/src/ingestion/types.js +3 -0
  31. package/dist/src/jobs/manager.js +305 -0
  32. package/dist/src/jobs/runner.js +806 -0
  33. package/dist/src/jobs/store.js +102 -0
  34. package/dist/src/jobs/types.js +30 -0
  35. package/dist/src/jobs/worker.js +97 -0
  36. package/dist/src/learning/engine.js +356 -0
  37. package/dist/src/learning/index.js +9 -0
  38. package/dist/src/learning/locator-policy.js +120 -0
  39. package/dist/src/learning/pattern-policy.js +89 -0
  40. package/dist/src/learning/recovery-policy.js +116 -0
  41. package/dist/src/learning/sensor-policy.js +115 -0
  42. package/dist/src/learning/timing-model.js +204 -0
  43. package/dist/src/learning/topology-policy.js +90 -0
  44. package/dist/src/learning/types.js +9 -0
  45. package/dist/src/logging/timeline-logger.js +48 -0
  46. package/dist/src/mcp/mcp-stdio-server.js +464 -0
  47. package/dist/src/mcp/server.js +363 -0
  48. package/dist/src/mcp-entry.js +60 -0
  49. package/dist/src/memory/playbook-seeds.js +200 -0
  50. package/dist/src/memory/recall.js +222 -0
  51. package/dist/src/memory/research.js +104 -0
  52. package/dist/src/memory/seeds.js +101 -0
  53. package/dist/src/memory/service.js +446 -0
  54. package/dist/src/memory/session.js +169 -0
  55. package/dist/src/memory/store.js +451 -0
  56. package/{src/runtime/locator-cache.ts → dist/src/memory/types.js} +1 -17
  57. package/dist/src/monitor/codex-monitor.js +382 -0
  58. package/dist/src/monitor/task-queue.js +97 -0
  59. package/dist/src/monitor/types.js +62 -0
  60. package/dist/src/native/bridge-client.js +412 -0
  61. package/{src/native/macos-bridge-client.ts → dist/src/native/macos-bridge-client.js} +0 -1
  62. package/dist/src/observer/state.js +199 -0
  63. package/dist/src/observer/types.js +43 -0
  64. package/dist/src/orchestrator/state.js +68 -0
  65. package/dist/src/orchestrator/types.js +22 -0
  66. package/dist/src/perception/ax-source.js +162 -0
  67. package/dist/src/perception/cdp-source.js +162 -0
  68. package/dist/src/perception/coordinator.js +771 -0
  69. package/dist/src/perception/frame-differ.js +287 -0
  70. package/dist/src/perception/index.js +22 -0
  71. package/dist/src/perception/manager.js +199 -0
  72. package/dist/src/perception/types.js +47 -0
  73. package/dist/src/perception/vision-source.js +399 -0
  74. package/dist/src/planner/deterministic.js +298 -0
  75. package/dist/src/planner/executor.js +870 -0
  76. package/dist/src/planner/goal-store.js +92 -0
  77. package/dist/src/planner/index.js +21 -0
  78. package/dist/src/planner/planner.js +520 -0
  79. package/dist/src/planner/tool-registry.js +71 -0
  80. package/dist/src/planner/types.js +22 -0
  81. package/dist/src/platform/explorer.js +213 -0
  82. package/dist/src/platform/help-center-markdown.js +527 -0
  83. package/dist/src/platform/learner.js +257 -0
  84. package/dist/src/playbook/engine.js +486 -0
  85. package/dist/src/playbook/index.js +20 -0
  86. package/dist/src/playbook/mcp-recorder.js +204 -0
  87. package/dist/src/playbook/recorder.js +536 -0
  88. package/dist/src/playbook/runner.js +408 -0
  89. package/dist/src/playbook/store.js +312 -0
  90. package/dist/src/playbook/types.js +17 -0
  91. package/dist/src/recovery/detectors.js +156 -0
  92. package/dist/src/recovery/engine.js +327 -0
  93. package/dist/src/recovery/index.js +20 -0
  94. package/dist/src/recovery/strategies.js +274 -0
  95. package/dist/src/recovery/types.js +20 -0
  96. package/dist/src/runtime/accessibility-adapter.js +430 -0
  97. package/dist/src/runtime/app-adapter.js +64 -0
  98. package/dist/src/runtime/applescript-adapter.js +305 -0
  99. package/dist/src/runtime/ax-role-map.js +96 -0
  100. package/dist/src/runtime/browser-adapter.js +52 -0
  101. package/dist/src/runtime/cdp-chrome-adapter.js +521 -0
  102. package/dist/src/runtime/composite-adapter.js +221 -0
  103. package/dist/src/runtime/execution-contract.js +159 -0
  104. package/dist/src/runtime/executor.js +286 -0
  105. package/dist/src/runtime/locator-cache.js +50 -0
  106. package/dist/src/runtime/planning-loop.js +63 -0
  107. package/dist/src/runtime/service.js +432 -0
  108. package/dist/src/runtime/session-manager.js +63 -0
  109. package/dist/src/runtime/state-observer.js +121 -0
  110. package/dist/src/runtime/vision-adapter.js +225 -0
  111. package/dist/src/state/app-map-types.js +72 -0
  112. package/dist/src/state/app-map.js +1974 -0
  113. package/dist/src/state/entity-tracker.js +108 -0
  114. package/dist/src/state/fusion.js +96 -0
  115. package/dist/src/state/index.js +21 -0
  116. package/dist/src/state/ladder-generator.js +236 -0
  117. package/dist/src/state/persistence.js +156 -0
  118. package/dist/src/state/types.js +17 -0
  119. package/dist/src/state/world-model.js +1456 -0
  120. package/dist/src/supervisor/locks.js +186 -0
  121. package/dist/src/supervisor/supervisor.js +403 -0
  122. package/dist/src/supervisor/types.js +30 -0
  123. package/dist/src/test-mcp-protocol.js +154 -0
  124. package/dist/src/types.js +17 -0
  125. package/dist/src/util/atomic-write.js +133 -0
  126. package/dist/src/util/sanitize.js +146 -0
  127. package/dist-app-maps/com.figma.Desktop.json +959 -0
  128. package/dist-app-maps/com.hnc.Discord.json +1146 -0
  129. package/dist-app-maps/notion.id.json +2831 -0
  130. package/dist-playbooks/canva-screenhand-carousel.json +445 -0
  131. package/dist-playbooks/codex-desktop.json +76 -0
  132. package/dist-playbooks/competitor-research-stack.json +122 -0
  133. package/dist-playbooks/davinci-color-grade.json +153 -0
  134. package/dist-playbooks/davinci-edit-timeline.json +162 -0
  135. package/dist-playbooks/davinci-render.json +114 -0
  136. package/dist-playbooks/devto.json +52 -0
  137. package/dist-playbooks/discord.json +41 -0
  138. package/dist-playbooks/google-flow-create-project.json +59 -0
  139. package/dist-playbooks/google-flow-edit-image.json +90 -0
  140. package/dist-playbooks/google-flow-edit-video.json +90 -0
  141. package/dist-playbooks/google-flow-generate-image.json +68 -0
  142. package/dist-playbooks/google-flow-generate-video.json +191 -0
  143. package/dist-playbooks/google-flow-open-project.json +48 -0
  144. package/dist-playbooks/google-flow-open-scenebuilder.json +64 -0
  145. package/dist-playbooks/google-flow-search-assets.json +64 -0
  146. package/dist-playbooks/instagram.json +57 -0
  147. package/dist-playbooks/linkedin.json +52 -0
  148. package/dist-playbooks/n8n.json +43 -0
  149. package/dist-playbooks/reddit.json +52 -0
  150. package/dist-playbooks/threads.json +59 -0
  151. package/dist-playbooks/x-twitter.json +59 -0
  152. package/dist-playbooks/youtube.json +59 -0
  153. package/dist-references/canva.json +646 -0
  154. package/dist-references/codex-desktop.json +305 -0
  155. package/dist-references/davinci-resolve-keyboard.json +594 -0
  156. package/dist-references/davinci-resolve-menu-map.json +1139 -0
  157. package/dist-references/davinci-resolve-menus-batch1.json +116 -0
  158. package/dist-references/davinci-resolve-menus-batch2.json +372 -0
  159. package/dist-references/davinci-resolve-menus-batch3.json +330 -0
  160. package/dist-references/davinci-resolve-menus-batch4.json +297 -0
  161. package/dist-references/davinci-resolve-shortcuts.json +333 -0
  162. package/dist-references/devto.json +317 -0
  163. package/dist-references/discord.json +549 -0
  164. package/dist-references/figma.json +1186 -0
  165. package/dist-references/finder.json +146 -0
  166. package/dist-references/google-ads-transparency.json +95 -0
  167. package/dist-references/google-flow.json +649 -0
  168. package/dist-references/instagram.json +341 -0
  169. package/dist-references/linkedin.json +324 -0
  170. package/dist-references/meta-ad-library.json +86 -0
  171. package/dist-references/n8n.json +387 -0
  172. package/dist-references/notes.json +27 -0
  173. package/dist-references/notion.json +163 -0
  174. package/dist-references/reddit.json +341 -0
  175. package/dist-references/threads.json +337 -0
  176. package/dist-references/x-twitter.json +403 -0
  177. package/dist-references/youtube.json +373 -0
  178. package/native/macos-bridge/Package.swift +1 -0
  179. package/native/macos-bridge/Sources/AccessibilityBridge.swift +257 -36
  180. package/native/macos-bridge/Sources/AppManagement.swift +212 -2
  181. package/native/macos-bridge/Sources/CoreGraphicsBridge.swift +348 -53
  182. package/native/macos-bridge/Sources/StreamCapture.swift +136 -0
  183. package/native/macos-bridge/Sources/VisionBridge.swift +165 -7
  184. package/native/macos-bridge/Sources/main.swift +169 -16
  185. package/native/windows-bridge/Program.cs +5 -0
  186. package/native/windows-bridge/ScreenCapture.cs +124 -0
  187. package/package.json +29 -4
  188. package/scripts/postinstall.cjs +127 -0
  189. package/.claude/commands/automate.md +0 -28
  190. package/.claude/commands/debug-ui.md +0 -19
  191. package/.claude/commands/screenshot.md +0 -15
  192. package/.github/FUNDING.yml +0 -1
  193. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -27
  194. package/.github/ISSUE_TEMPLATE/feature_request.md +0 -20
  195. package/.mcp.json +0 -8
  196. package/DESKTOP_MCP_GUIDE.md +0 -92
  197. package/SECURITY.md +0 -44
  198. package/docs/architecture.md +0 -47
  199. package/install-skills.sh +0 -19
  200. package/mcp-bridge.ts +0 -271
  201. package/mcp-desktop.ts +0 -1221
  202. package/playbooks/instagram.json +0 -41
  203. package/playbooks/instagram_v2.json +0 -201
  204. package/playbooks/x_v1.json +0 -211
  205. package/scripts/devpost-live-loop.mjs +0 -421
  206. package/src/logging/timeline-logger.ts +0 -55
  207. package/src/mcp/server.ts +0 -449
  208. package/src/memory/recall.ts +0 -191
  209. package/src/memory/research.ts +0 -146
  210. package/src/memory/seeds.ts +0 -123
  211. package/src/memory/session.ts +0 -201
  212. package/src/memory/store.ts +0 -434
  213. package/src/memory/types.ts +0 -69
  214. package/src/native/bridge-client.ts +0 -239
  215. package/src/runtime/accessibility-adapter.ts +0 -487
  216. package/src/runtime/app-adapter.ts +0 -169
  217. package/src/runtime/applescript-adapter.ts +0 -376
  218. package/src/runtime/ax-role-map.ts +0 -102
  219. package/src/runtime/browser-adapter.ts +0 -129
  220. package/src/runtime/cdp-chrome-adapter.ts +0 -676
  221. package/src/runtime/composite-adapter.ts +0 -274
  222. package/src/runtime/executor.ts +0 -396
  223. package/src/runtime/planning-loop.ts +0 -81
  224. package/src/runtime/service.ts +0 -448
  225. package/src/runtime/session-manager.ts +0 -50
  226. package/src/runtime/state-observer.ts +0 -136
  227. package/src/runtime/vision-adapter.ts +0 -297
  228. package/src/types.ts +0 -297
  229. package/tests/bridge-client.test.ts +0 -176
  230. package/tests/browser-stealth.test.ts +0 -210
  231. package/tests/composite-adapter.test.ts +0 -64
  232. package/tests/mcp-server.test.ts +0 -151
  233. package/tests/memory-recall.test.ts +0 -339
  234. package/tests/memory-research.test.ts +0 -159
  235. package/tests/memory-seeds.test.ts +0 -120
  236. package/tests/memory-store.test.ts +0 -392
  237. package/tests/types.test.ts +0 -92
  238. package/tsconfig.check.json +0 -17
  239. package/tsconfig.json +0 -19
  240. package/vitest.config.ts +0 -8
  241. /package/{playbooks → dist-references}/devpost.json +0 -0
@@ -0,0 +1,71 @@
1
+ // Copyright (C) 2025 Clazro Technology Private Limited
2
+ // SPDX-License-Identifier: AGPL-3.0-only
3
+ //
4
+ // This file is part of ScreenHand.
5
+ //
6
+ // ScreenHand is free software: you can redistribute it and/or modify
7
+ // it under the terms of the GNU Affero General Public License as
8
+ // published by the Free Software Foundation, version 3.
9
+ //
10
+ // ScreenHand is distributed in the hope that it will be useful,
11
+ // but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ // GNU Affero General Public License for more details.
14
+ //
15
+ // You should have received a copy of the GNU Affero General Public License
16
+ // along with ScreenHand. If not, see <https://www.gnu.org/licenses/>.
17
+ export class ToolRegistry {
18
+ handlers = new Map();
19
+ register(name, handler) {
20
+ this.handlers.set(name, handler);
21
+ }
22
+ has(name) {
23
+ return this.handlers.has(name);
24
+ }
25
+ getToolNames() {
26
+ return [...this.handlers.keys()];
27
+ }
28
+ async execute(name, params) {
29
+ const handler = this.handlers.get(name);
30
+ if (!handler) {
31
+ return { ok: false, error: `Unknown tool: ${name}` };
32
+ }
33
+ try {
34
+ const result = await handler(params);
35
+ // Extract text from MCP result format { content: [{ type: "text", text: "..." }] }
36
+ let text = "";
37
+ if (result?.content && Array.isArray(result.content)) {
38
+ text = result.content
39
+ .filter((c) => c.type === "text")
40
+ .map((c) => c.text)
41
+ .join("\n");
42
+ }
43
+ // Detect failures: explicit isError flag OR soft failure patterns in text
44
+ if (result?.isError) {
45
+ return { ok: false, error: text };
46
+ }
47
+ const lower = text.toLowerCase();
48
+ const isSoftFailure = (lower.includes("not found") &&
49
+ !lower.includes("clicked") &&
50
+ !lower.includes("using default") &&
51
+ !lower.includes("created") &&
52
+ !lower.includes("using fallback")) ||
53
+ lower.includes("window not found") ||
54
+ (lower.startsWith("error:") &&
55
+ !/^error:\s*(none|no |nothing|0 )/.test(lower));
56
+ if (isSoftFailure) {
57
+ return { ok: false, error: text };
58
+ }
59
+ return { ok: true, result: text };
60
+ }
61
+ catch (err) {
62
+ return {
63
+ ok: false,
64
+ error: err instanceof Error ? err.message : String(err),
65
+ };
66
+ }
67
+ }
68
+ toExecutor() {
69
+ return (tool, params) => this.execute(tool, params);
70
+ }
71
+ }
@@ -0,0 +1,22 @@
1
+ // Copyright (C) 2025 Clazro Technology Private Limited
2
+ // SPDX-License-Identifier: AGPL-3.0-only
3
+ //
4
+ // This file is part of ScreenHand.
5
+ //
6
+ // ScreenHand is free software: you can redistribute it and/or modify
7
+ // it under the terms of the GNU Affero General Public License as
8
+ // published by the Free Software Foundation, version 3.
9
+ //
10
+ // ScreenHand is distributed in the hope that it will be useful,
11
+ // but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ // GNU Affero General Public License for more details.
14
+ //
15
+ // You should have received a copy of the GNU Affero General Public License
16
+ // along with ScreenHand. If not, see <https://www.gnu.org/licenses/>.
17
+ export const DEFAULT_PLANNER_CONFIG = {
18
+ defaultMaxAttempts: 3,
19
+ defaultStepTimeout: 10_000,
20
+ postconditionWaitMs: 2_000,
21
+ minConfidenceForExecution: 0.3,
22
+ };
@@ -0,0 +1,213 @@
1
+ // Copyright (C) 2025 Clazro Technology Private Limited
2
+ // SPDX-License-Identifier: AGPL-3.0-only
3
+ /**
4
+ * PlatformExplorer — autonomously explore an app or website.
5
+ *
6
+ * Maps all interactive elements, tries each one, records working selectors,
7
+ * broken paths, and errors. Outputs a reference JSON.
8
+ */
9
+ import fs from "node:fs";
10
+ import path from "node:path";
11
+ import { writeFileAtomicSync } from "../util/atomic-write.js";
12
+ /** Patterns for buttons that should never be clicked */
13
+ const DANGEROUS_PATTERNS = /delete|logout|log.?out|remove|cancel|unsubscribe|deactivate|sign.?out|close.?account|erase|destroy|terminate|disconnect|revoke/i;
14
+ /** Discover all interactive elements on a web page via CDP */
15
+ export async function discoverWebElements(cdpEvaluate, maxElements) {
16
+ const result = await cdpEvaluate(`(() => {
17
+ const elements = [];
18
+ const selectors = new Set();
19
+ const interactive = document.querySelectorAll(
20
+ 'a[href], button, input, select, textarea, [role="button"], [role="tab"], [role="menuitem"], [role="link"], [onclick], [data-testid], summary'
21
+ );
22
+ for (const el of interactive) {
23
+ if (elements.length >= ${maxElements}) break;
24
+ const rect = el.getBoundingClientRect();
25
+ if (rect.width === 0 || rect.height === 0) continue;
26
+ if (rect.top < 0 || rect.left < 0) continue;
27
+
28
+ let selector = '';
29
+ if (el.getAttribute('data-testid')) selector = '[data-testid="' + el.getAttribute('data-testid') + '"]';
30
+ else if (el.id) selector = '#' + el.id;
31
+ else if (el.getAttribute('aria-label')) selector = '[aria-label="' + el.getAttribute('aria-label') + '"]';
32
+ else {
33
+ const tag = el.tagName.toLowerCase();
34
+ const text = (el.textContent || '').trim().substring(0, 30);
35
+ selector = tag + (text ? ':text("' + text + '")' : ':nth(' + elements.length + ')');
36
+ }
37
+
38
+ if (selectors.has(selector)) continue;
39
+ selectors.add(selector);
40
+
41
+ elements.push({
42
+ selector,
43
+ text: (el.textContent || '').trim().substring(0, 100),
44
+ tag: el.tagName.toLowerCase(),
45
+ role: el.getAttribute('role') || undefined,
46
+ ariaLabel: el.getAttribute('aria-label') || undefined,
47
+ });
48
+ }
49
+ return elements;
50
+ })()`);
51
+ return result.result?.value ?? [];
52
+ }
53
+ /** Test clicking an element and record what happens */
54
+ export async function testWebElement(cdpEvaluate, element) {
55
+ // Check if dangerous
56
+ if (DANGEROUS_PATTERNS.test(element.text) || DANGEROUS_PATTERNS.test(element.ariaLabel ?? "")) {
57
+ return { ...element, clickWorked: false, result: "skipped_dangerous", error: "Skipped: potentially destructive action" };
58
+ }
59
+ try {
60
+ // Get pre-click state
61
+ const preState = await cdpEvaluate(`(() => ({ url: location.href, title: document.title }))()`);
62
+ const pre = preState.result?.value ?? { url: "", title: "" };
63
+ // Click the element
64
+ const clickResult = await cdpEvaluate(`(() => {
65
+ const el = document.querySelector('${element.selector.replace(/'/g, "\\'")}');
66
+ if (!el) return { found: false };
67
+ el.click();
68
+ return { found: true };
69
+ })()`);
70
+ if (!clickResult.result?.value?.found) {
71
+ return { ...element, clickWorked: false, result: "element_not_found" };
72
+ }
73
+ // Wait for UI to settle
74
+ await new Promise(r => setTimeout(r, 800));
75
+ // Get post-click state
76
+ const postState = await cdpEvaluate(`(() => ({ url: location.href, title: document.title }))()`);
77
+ const post = postState.result?.value ?? { url: "", title: "" };
78
+ const urlChanged = pre.url !== post.url;
79
+ const titleChanged = pre.title !== post.title;
80
+ const stateChange = urlChanged ? `url_changed: ${post.url}` : titleChanged ? `title_changed: ${post.title}` : "no_visible_change";
81
+ // Navigate back if URL changed
82
+ if (urlChanged) {
83
+ await cdpEvaluate(`history.back()`);
84
+ await new Promise(r => setTimeout(r, 500));
85
+ }
86
+ return {
87
+ ...element,
88
+ clickWorked: true,
89
+ result: stateChange,
90
+ newUrl: urlChanged ? post.url : undefined,
91
+ stateChange,
92
+ };
93
+ }
94
+ catch (err) {
95
+ return {
96
+ ...element,
97
+ clickWorked: false,
98
+ result: "error",
99
+ error: err instanceof Error ? err.message : String(err),
100
+ };
101
+ }
102
+ }
103
+ /** Discover all interactive elements in a native app via AX bridge */
104
+ export async function discoverNativeElements(bridge, pid, maxElements) {
105
+ const tree = await bridge.call("ax.getElementTree", { pid, maxDepth: 5 });
106
+ const elements = [];
107
+ const CLICKABLE_ROLES = new Set([
108
+ "AXButton", "AXLink", "AXMenuItem", "AXTab", "AXCheckBox",
109
+ "AXPopUpButton", "AXRadioButton", "AXSwitch", "AXDisclosureTriangle",
110
+ ]);
111
+ function walk(node, path = []) {
112
+ if (elements.length >= maxElements)
113
+ return;
114
+ if (node.role && CLICKABLE_ROLES.has(node.role)) {
115
+ const title = node.title || node.description || node.value || "";
116
+ if (title && !DANGEROUS_PATTERNS.test(title)) {
117
+ elements.push({
118
+ selector: path.join("."),
119
+ text: String(title).substring(0, 100),
120
+ tag: node.role,
121
+ role: node.role,
122
+ ariaLabel: node.description,
123
+ });
124
+ }
125
+ }
126
+ if (Array.isArray(node.children)) {
127
+ node.children.forEach((child, i) => walk(child, [...path, i]));
128
+ }
129
+ }
130
+ walk(tree);
131
+ return elements;
132
+ }
133
+ /** Compile exploration results into a reference JSON */
134
+ export function compileReference(platform, source, tested, url, bundleId) {
135
+ const working = tested.filter(t => t.clickWorked);
136
+ const broken = tested.filter(t => !t.clickWorked && t.result !== "skipped_dangerous");
137
+ const skipped = tested.filter(t => t.result === "skipped_dangerous");
138
+ // Group working selectors by tag/role
139
+ const selectors = {};
140
+ for (const el of working) {
141
+ const group = el.role ?? el.tag;
142
+ if (!selectors[group])
143
+ selectors[group] = {};
144
+ const key = el.text.replace(/[^a-zA-Z0-9_]/g, "_").substring(0, 40) || `el_${Object.keys(selectors[group]).length}`;
145
+ selectors[group][key] = el.selector;
146
+ }
147
+ const navigation = working
148
+ .filter((t) => t.newUrl !== undefined)
149
+ .map(t => ({ text: t.text, selector: t.selector, worked: true, url: t.newUrl }));
150
+ const errors = broken.map(t => ({
151
+ error: t.error ?? t.result,
152
+ context: `Clicking "${t.text}" (${t.selector})`,
153
+ solution: "Element may be hidden, disabled, or require prior interaction",
154
+ severity: "low",
155
+ }));
156
+ const keyDiscoveries = [];
157
+ keyDiscoveries.push(`Found ${tested.length} interactive elements, ${working.length} clickable, ${broken.length} broken, ${skipped.length} skipped (dangerous)`);
158
+ if (navigation.length > 0)
159
+ keyDiscoveries.push(`${navigation.length} navigation links discovered`);
160
+ if (Object.keys(selectors).length > 0)
161
+ keyDiscoveries.push(`Selector groups: ${Object.keys(selectors).join(", ")}`);
162
+ return {
163
+ platform,
164
+ exploredAt: new Date().toISOString(),
165
+ source,
166
+ ...(url !== undefined ? { url } : {}),
167
+ ...(bundleId !== undefined ? { bundleId } : {}),
168
+ totalElements: tested.length,
169
+ testedElements: tested.filter(t => t.result !== "skipped_dangerous").length,
170
+ workingSelectors: working.length,
171
+ selectors,
172
+ navigation,
173
+ errors,
174
+ keyDiscoveries,
175
+ };
176
+ }
177
+ /** Save exploration result as a reference JSON */
178
+ export function saveExploreResult(referencesDir, result) {
179
+ if (!fs.existsSync(referencesDir)) {
180
+ fs.mkdirSync(referencesDir, { recursive: true });
181
+ }
182
+ const safePlatform = result.platform.replace(/[^a-zA-Z0-9_\-]/g, "_").slice(0, 100);
183
+ const filePath = path.join(referencesDir, `${safePlatform}-explore.json`);
184
+ const resolved = path.resolve(filePath);
185
+ if (!resolved.startsWith(path.resolve(referencesDir))) {
186
+ throw new Error("Invalid platform name — path traversal detected");
187
+ }
188
+ // Build reference format matching existing references
189
+ const reference = {
190
+ id: `${result.platform}-explore`,
191
+ name: `${result.platform} — Auto-Explored`,
192
+ description: `Auto-explored ${result.source === "web" ? result.url : result.bundleId}. Found ${result.workingSelectors} working selectors out of ${result.totalElements} elements.`,
193
+ platform: result.platform,
194
+ version: "1.0.0",
195
+ tags: [result.platform, "auto-explored"],
196
+ successCount: result.workingSelectors,
197
+ failCount: result.errors.length,
198
+ selectors: result.selectors,
199
+ errors: result.errors,
200
+ _meta: {
201
+ exploredAt: result.exploredAt,
202
+ source: result.source,
203
+ url: result.url,
204
+ bundleId: result.bundleId,
205
+ totalElements: result.totalElements,
206
+ testedElements: result.testedElements,
207
+ workingSelectors: result.workingSelectors,
208
+ keyDiscoveries: result.keyDiscoveries,
209
+ },
210
+ };
211
+ writeFileAtomicSync(filePath, JSON.stringify(reference, null, 2));
212
+ return filePath;
213
+ }