screenhand 0.2.0 → 0.3.1

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 (212) hide show
  1. package/README.md +165 -446
  2. package/bin/darwin-arm64/macos-bridge +0 -0
  3. package/dist/mcp-desktop.js +3615 -400
  4. package/dist/scripts/export-help-center.js +112 -0
  5. package/dist/scripts/marketing-loop.js +117 -0
  6. package/dist/scripts/observer-daemon.js +288 -0
  7. package/dist/scripts/orchestrator-daemon.js +399 -0
  8. package/dist/scripts/threads-campaign.js +208 -0
  9. package/dist/src/community/fetcher.js +109 -0
  10. package/dist/src/community/index.js +6 -0
  11. package/dist/src/community/publisher.js +191 -0
  12. package/dist/src/community/remote-api.js +121 -0
  13. package/dist/src/community/types.js +3 -0
  14. package/dist/src/community/validator.js +95 -0
  15. package/dist/src/context-tracker.js +489 -0
  16. package/dist/src/ingestion/coverage-auditor.js +233 -0
  17. package/dist/src/ingestion/doc-parser.js +164 -0
  18. package/dist/src/ingestion/index.js +8 -0
  19. package/dist/src/ingestion/menu-scanner.js +152 -0
  20. package/dist/src/ingestion/reference-merger.js +186 -0
  21. package/dist/src/ingestion/shortcut-extractor.js +180 -0
  22. package/dist/src/ingestion/tutorial-extractor.js +170 -0
  23. package/dist/src/ingestion/types.js +3 -0
  24. package/dist/src/jobs/manager.js +82 -14
  25. package/dist/src/jobs/runner.js +138 -15
  26. package/dist/src/learning/engine.js +356 -0
  27. package/dist/src/learning/index.js +9 -0
  28. package/dist/src/learning/locator-policy.js +120 -0
  29. package/dist/src/learning/pattern-policy.js +89 -0
  30. package/dist/src/learning/recovery-policy.js +116 -0
  31. package/dist/src/learning/sensor-policy.js +115 -0
  32. package/dist/src/learning/timing-model.js +204 -0
  33. package/dist/src/learning/topology-policy.js +90 -0
  34. package/dist/src/learning/types.js +9 -0
  35. package/dist/src/logging/timeline-logger.js +4 -1
  36. package/dist/src/memory/playbook-seeds.js +200 -0
  37. package/dist/src/memory/recall.js +60 -8
  38. package/dist/src/memory/service.js +30 -5
  39. package/dist/src/memory/store.js +34 -5
  40. package/dist/src/native/bridge-client.js +253 -31
  41. package/dist/src/observer/state.js +199 -0
  42. package/dist/src/observer/types.js +43 -0
  43. package/dist/src/orchestrator/state.js +68 -0
  44. package/dist/src/orchestrator/types.js +22 -0
  45. package/dist/src/perception/ax-source.js +162 -0
  46. package/dist/src/perception/cdp-source.js +162 -0
  47. package/dist/src/perception/coordinator.js +771 -0
  48. package/dist/src/perception/frame-differ.js +287 -0
  49. package/dist/src/perception/index.js +22 -0
  50. package/dist/src/perception/manager.js +199 -0
  51. package/dist/src/perception/types.js +47 -0
  52. package/dist/src/perception/vision-source.js +399 -0
  53. package/dist/src/planner/deterministic.js +298 -0
  54. package/dist/src/planner/executor.js +870 -0
  55. package/dist/src/planner/goal-store.js +92 -0
  56. package/dist/src/planner/index.js +21 -0
  57. package/dist/src/planner/planner.js +520 -0
  58. package/dist/src/planner/tool-registry.js +71 -0
  59. package/dist/src/planner/types.js +22 -0
  60. package/dist/src/platform/explorer.js +213 -0
  61. package/dist/src/platform/help-center-markdown.js +527 -0
  62. package/dist/src/platform/learner.js +257 -0
  63. package/dist/src/playbook/engine.js +296 -11
  64. package/dist/src/playbook/mcp-recorder.js +204 -0
  65. package/dist/src/playbook/recorder.js +3 -2
  66. package/dist/src/playbook/runner.js +1 -1
  67. package/dist/src/playbook/store.js +139 -10
  68. package/dist/src/recovery/detectors.js +156 -0
  69. package/dist/src/recovery/engine.js +327 -0
  70. package/dist/src/recovery/index.js +20 -0
  71. package/dist/src/recovery/strategies.js +274 -0
  72. package/dist/src/recovery/types.js +20 -0
  73. package/dist/src/runtime/accessibility-adapter.js +55 -18
  74. package/dist/src/runtime/applescript-adapter.js +8 -2
  75. package/dist/src/runtime/cdp-chrome-adapter.js +1 -1
  76. package/dist/src/runtime/executor.js +23 -3
  77. package/dist/src/runtime/locator-cache.js +24 -2
  78. package/dist/src/runtime/service.js +59 -15
  79. package/dist/src/runtime/session-manager.js +4 -1
  80. package/dist/src/runtime/vision-adapter.js +2 -1
  81. package/dist/src/state/app-map-types.js +72 -0
  82. package/dist/src/state/app-map.js +1974 -0
  83. package/dist/src/state/entity-tracker.js +108 -0
  84. package/dist/src/state/fusion.js +96 -0
  85. package/dist/src/state/index.js +21 -0
  86. package/dist/src/state/ladder-generator.js +236 -0
  87. package/dist/src/state/persistence.js +156 -0
  88. package/dist/src/state/types.js +17 -0
  89. package/dist/src/state/world-model.js +1456 -0
  90. package/dist/src/util/atomic-write.js +19 -4
  91. package/dist/src/util/sanitize.js +146 -0
  92. package/dist-app-maps/com.figma.Desktop.json +959 -0
  93. package/dist-app-maps/com.hnc.Discord.json +1146 -0
  94. package/dist-app-maps/notion.id.json +2831 -0
  95. package/dist-playbooks/canva-screenhand-carousel.json +445 -0
  96. package/dist-playbooks/codex-desktop.json +76 -0
  97. package/dist-playbooks/competitor-research-stack.json +122 -0
  98. package/dist-playbooks/davinci-color-grade.json +153 -0
  99. package/dist-playbooks/davinci-edit-timeline.json +162 -0
  100. package/dist-playbooks/davinci-render.json +114 -0
  101. package/dist-playbooks/devto.json +52 -0
  102. package/dist-playbooks/discord.json +41 -0
  103. package/dist-playbooks/google-flow-create-project.json +59 -0
  104. package/dist-playbooks/google-flow-edit-image.json +90 -0
  105. package/dist-playbooks/google-flow-edit-video.json +90 -0
  106. package/dist-playbooks/google-flow-generate-image.json +68 -0
  107. package/dist-playbooks/google-flow-generate-video.json +191 -0
  108. package/dist-playbooks/google-flow-open-project.json +48 -0
  109. package/dist-playbooks/google-flow-open-scenebuilder.json +64 -0
  110. package/dist-playbooks/google-flow-search-assets.json +64 -0
  111. package/dist-playbooks/instagram.json +57 -0
  112. package/dist-playbooks/linkedin.json +52 -0
  113. package/dist-playbooks/n8n.json +43 -0
  114. package/dist-playbooks/reddit.json +52 -0
  115. package/dist-playbooks/threads.json +59 -0
  116. package/dist-playbooks/x-twitter.json +59 -0
  117. package/dist-playbooks/youtube.json +59 -0
  118. package/dist-references/canva.json +646 -0
  119. package/dist-references/codex-desktop.json +305 -0
  120. package/dist-references/davinci-resolve-keyboard.json +594 -0
  121. package/dist-references/davinci-resolve-menu-map.json +1139 -0
  122. package/dist-references/davinci-resolve-menus-batch1.json +116 -0
  123. package/dist-references/davinci-resolve-menus-batch2.json +372 -0
  124. package/dist-references/davinci-resolve-menus-batch3.json +330 -0
  125. package/dist-references/davinci-resolve-menus-batch4.json +297 -0
  126. package/dist-references/davinci-resolve-shortcuts.json +333 -0
  127. package/dist-references/devpost.json +186 -0
  128. package/dist-references/devto.json +317 -0
  129. package/dist-references/discord.json +549 -0
  130. package/dist-references/figma.json +1186 -0
  131. package/dist-references/finder.json +146 -0
  132. package/dist-references/google-ads-transparency.json +95 -0
  133. package/dist-references/google-flow.json +649 -0
  134. package/dist-references/instagram.json +341 -0
  135. package/dist-references/linkedin.json +324 -0
  136. package/dist-references/meta-ad-library.json +86 -0
  137. package/dist-references/n8n.json +387 -0
  138. package/dist-references/notes.json +27 -0
  139. package/dist-references/notion.json +163 -0
  140. package/dist-references/reddit.json +341 -0
  141. package/dist-references/threads.json +337 -0
  142. package/dist-references/x-twitter.json +403 -0
  143. package/dist-references/youtube.json +373 -0
  144. package/native/macos-bridge/Package.swift +22 -0
  145. package/native/macos-bridge/Sources/AccessibilityBridge.swift +482 -0
  146. package/native/macos-bridge/Sources/AppManagement.swift +339 -0
  147. package/native/macos-bridge/Sources/CoreGraphicsBridge.swift +537 -0
  148. package/native/macos-bridge/Sources/ObserverBridge.swift +120 -0
  149. package/native/macos-bridge/Sources/StreamCapture.swift +136 -0
  150. package/native/macos-bridge/Sources/VisionBridge.swift +238 -0
  151. package/native/macos-bridge/Sources/main.swift +498 -0
  152. package/native/windows-bridge/AppManagement.cs +234 -0
  153. package/native/windows-bridge/InputBridge.cs +436 -0
  154. package/native/windows-bridge/Program.cs +270 -0
  155. package/native/windows-bridge/ScreenCapture.cs +453 -0
  156. package/native/windows-bridge/UIAutomationBridge.cs +571 -0
  157. package/native/windows-bridge/WindowsBridge.csproj +17 -0
  158. package/package.json +12 -1
  159. package/scripts/postinstall.cjs +127 -0
  160. package/dist/.audit-log.jsonl +0 -55
  161. package/dist/.screenhand/memory/.lock +0 -1
  162. package/dist/.screenhand/memory/actions.jsonl +0 -85
  163. package/dist/.screenhand/memory/errors.jsonl +0 -5
  164. package/dist/.screenhand/memory/errors.jsonl.bak +0 -4
  165. package/dist/.screenhand/memory/state.json +0 -35
  166. package/dist/.screenhand/memory/state.json.bak +0 -35
  167. package/dist/.screenhand/memory/strategies.jsonl +0 -12
  168. package/dist/agent/cli.js +0 -73
  169. package/dist/agent/loop.js +0 -258
  170. package/dist/config.js +0 -9
  171. package/dist/index.js +0 -56
  172. package/dist/logging/timeline-logger.js +0 -29
  173. package/dist/mcp/mcp-stdio-server.js +0 -448
  174. package/dist/mcp/server.js +0 -347
  175. package/dist/mcp-entry.js +0 -59
  176. package/dist/memory/recall.js +0 -160
  177. package/dist/memory/research.js +0 -98
  178. package/dist/memory/seeds.js +0 -89
  179. package/dist/memory/session.js +0 -161
  180. package/dist/memory/store.js +0 -391
  181. package/dist/memory/types.js +0 -4
  182. package/dist/monitor/codex-monitor.js +0 -377
  183. package/dist/monitor/task-queue.js +0 -84
  184. package/dist/monitor/types.js +0 -49
  185. package/dist/native/bridge-client.js +0 -174
  186. package/dist/native/macos-bridge-client.js +0 -5
  187. package/dist/npm-publish-helper.js +0 -117
  188. package/dist/npm-token-cdp.js +0 -113
  189. package/dist/npm-token-create.js +0 -135
  190. package/dist/npm-token-finish.js +0 -126
  191. package/dist/playbook/engine.js +0 -193
  192. package/dist/playbook/index.js +0 -4
  193. package/dist/playbook/recorder.js +0 -519
  194. package/dist/playbook/runner.js +0 -392
  195. package/dist/playbook/store.js +0 -166
  196. package/dist/playbook/types.js +0 -4
  197. package/dist/runtime/accessibility-adapter.js +0 -377
  198. package/dist/runtime/app-adapter.js +0 -48
  199. package/dist/runtime/applescript-adapter.js +0 -283
  200. package/dist/runtime/ax-role-map.js +0 -80
  201. package/dist/runtime/browser-adapter.js +0 -36
  202. package/dist/runtime/cdp-chrome-adapter.js +0 -505
  203. package/dist/runtime/composite-adapter.js +0 -205
  204. package/dist/runtime/executor.js +0 -250
  205. package/dist/runtime/locator-cache.js +0 -12
  206. package/dist/runtime/planning-loop.js +0 -47
  207. package/dist/runtime/service.js +0 -372
  208. package/dist/runtime/session-manager.js +0 -28
  209. package/dist/runtime/state-observer.js +0 -105
  210. package/dist/runtime/vision-adapter.js +0 -208
  211. package/dist/test-mcp-protocol.js +0 -138
  212. package/dist/types.js +0 -1
@@ -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
+ }