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,489 @@
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
+ * ContextTracker — lightweight singleton that connects tool execution to playbook knowledge.
7
+ *
8
+ * Three jobs, each fires at the right moment:
9
+ * 1. DETECT context — on domain/app change (from tool params), cache matching playbook
10
+ * 2. GET hints — per tool call, return 0-2 relevant hint strings (0ms, in-memory)
11
+ * 3. COLLECT outcome — per tool call, push to in-memory buffer (no disk, no AI)
12
+ * 4. FLUSH — on session_release or every N actions, merge learnings into playbook
13
+ */
14
+ import fs from "node:fs";
15
+ import path from "node:path";
16
+ // ── Tool → error relevance mapping ──
17
+ // Maps tool names to the error context keywords that are relevant to them
18
+ const TOOL_ERROR_KEYWORDS = {
19
+ browser_click: ["click", "button", "element"],
20
+ browser_human_click: ["click", "button", "element"],
21
+ click: ["click", "button", "element"],
22
+ click_text: ["click", "button", "element"],
23
+ click_with_fallback: ["click", "button", "element"],
24
+ browser_type: ["type", "input", "form", "field", "value"],
25
+ type_text: ["type", "input", "form", "field", "value"],
26
+ type_with_fallback: ["type", "input", "form", "field", "value"],
27
+ browser_fill_form: ["form", "field", "input", "value"],
28
+ browser_navigate: ["navigate", "url", "page", "load"],
29
+ browser_dom: ["dom", "selector", "element"],
30
+ browser_js: ["script", "eval", "js"],
31
+ browser_wait: ["wait", "load", "timeout"],
32
+ scroll: ["scroll"],
33
+ scroll_with_fallback: ["scroll"],
34
+ };
35
+ // Tools that carry a URL in their params
36
+ const URL_TOOLS = new Set([
37
+ "browser_open", "browser_navigate",
38
+ ]);
39
+ // Tools that carry a bundleId — native app tools where context should trigger
40
+ const BUNDLE_ID_TOOLS = new Set([
41
+ "focus", "launch", "ui_tree", "ui_find", "ui_press", "ui_set_value", "menu_click",
42
+ "click_with_fallback", "type_with_fallback", "read_with_fallback",
43
+ "locate_with_fallback", "select_with_fallback", "scroll_with_fallback",
44
+ "wait_for_state",
45
+ ]);
46
+ // Tools that carry a target/selector in their params
47
+ const TARGET_PARAM_NAMES = ["selector", "target", "text", "label", "placeholder"];
48
+ const FLUSH_THRESHOLD = 50;
49
+ const MIN_OCCURRENCES_TO_PROMOTE = 2;
50
+ export class ContextTracker {
51
+ store;
52
+ execPlaybooksDir;
53
+ context = null;
54
+ learnings = [];
55
+ actionCount = 0;
56
+ appMap = null;
57
+ _currentPageContext = null;
58
+ _previousPageContext = null;
59
+ _pendingTransition = null;
60
+ constructor(store, execPlaybooksDir) {
61
+ this.store = store;
62
+ this.execPlaybooksDir = execPlaybooksDir;
63
+ }
64
+ /**
65
+ * Current page/view context derived from window title.
66
+ * Used by AppMap to place elements in page-specific zones
67
+ * instead of the flat "auto_discovered" bucket.
68
+ */
69
+ get currentPageContext() {
70
+ return this._currentPageContext;
71
+ }
72
+ /** Set the AppMap instance for loading app mastery data on context change. */
73
+ setAppMap(map) {
74
+ this.appMap = map;
75
+ }
76
+ /** Get the current app mastery map data (if loaded). */
77
+ getAppMapData() {
78
+ return this.context?.appMapData ?? null;
79
+ }
80
+ /**
81
+ * Update the page context from a window title.
82
+ * Called after each tool call with the focused window's title.
83
+ * Extracts the first segment (page/view name) for page-aware zone routing.
84
+ * Tracks transitions: when page changes, stores a consumable transition.
85
+ */
86
+ updatePageContext(windowTitle) {
87
+ const oldPage = this._currentPageContext;
88
+ if (!windowTitle) {
89
+ this._currentPageContext = null;
90
+ return;
91
+ }
92
+ const newPage = extractPageContext(windowTitle);
93
+ this._currentPageContext = newPage;
94
+ // Detect transition: both old and new must be non-null and different
95
+ if (oldPage && newPage && oldPage !== newPage) {
96
+ this._previousPageContext = oldPage;
97
+ this._pendingTransition = { from: oldPage, to: newPage };
98
+ }
99
+ }
100
+ /**
101
+ * Consume a pending page transition (returns null if no transition occurred).
102
+ * Each transition is consumed once — subsequent calls return null until the
103
+ * next page change.
104
+ */
105
+ consumePageTransition() {
106
+ const transition = this._pendingTransition;
107
+ this._pendingTransition = null;
108
+ return transition;
109
+ }
110
+ // ═══════════════════════════════════════════════
111
+ // 1. DETECT — update context when domain changes
112
+ // ═══════════════════════════════════════════════
113
+ /**
114
+ * Call after every tool call. Extracts domain from URL params or
115
+ * bundleId from native tool params. Only does a playbook lookup
116
+ * when the context key actually changes.
117
+ */
118
+ updateContext(toolName, params) {
119
+ // Path 1: URL-bearing tools (browser_open, browser_navigate)
120
+ if (URL_TOOLS.has(toolName)) {
121
+ const url = params.url;
122
+ if (!url)
123
+ return;
124
+ let domain;
125
+ try {
126
+ domain = new URL(url).hostname.replace(/^www\./, "");
127
+ }
128
+ catch {
129
+ return;
130
+ }
131
+ if (!domain)
132
+ return; // javascript:, data:, blob: URLs have empty hostname
133
+ if (this.context?.domain === domain)
134
+ return;
135
+ const playbook = this.store.matchByDomain(domain);
136
+ this.context = buildCachedContext(domain, playbook);
137
+ return;
138
+ }
139
+ // Path 2: bundleId-bearing tools (native app automation)
140
+ if (BUNDLE_ID_TOOLS.has(toolName)) {
141
+ const rawBundleId = params.bundleId ?? params.pid;
142
+ if (!rawBundleId || typeof rawBundleId !== "string")
143
+ return;
144
+ const bundleId = rawBundleId;
145
+ const contextKey = `native:${bundleId}`;
146
+ if (this.context?.domain === contextKey)
147
+ return;
148
+ const playbook = this.store.matchByBundleId(bundleId);
149
+ this.context = buildCachedContext(contextKey, playbook);
150
+ // Load app mastery map on bundleId change
151
+ if (this.appMap) {
152
+ this.context.appMapData = this.appMap.load(bundleId) ?? null;
153
+ this.appMap.incrementSession(bundleId);
154
+ }
155
+ }
156
+ }
157
+ /**
158
+ * Extract domain from a browser window title and load matching reference.
159
+ * Covers Safari and other non-CDP browsers where URL isn't in tool params.
160
+ */
161
+ updateContextFromWindowTitle(bundleId, windowTitle) {
162
+ if (!windowTitle)
163
+ return;
164
+ // Only for known browser bundleIds
165
+ const BROWSER_BUNDLE_IDS = new Set([
166
+ "com.apple.Safari", "com.brave.Browser",
167
+ "org.chromium.Chromium", "com.vivaldi.Vivaldi",
168
+ "com.operasoftware.Opera",
169
+ ]);
170
+ if (!BROWSER_BUNDLE_IDS.has(bundleId))
171
+ return;
172
+ // Try extracting domain from title patterns:
173
+ // "Page Title — Safari" or "Page Title - Safari" or URL directly in title
174
+ let domain = null;
175
+ // Pattern 1: title contains a URL-like segment
176
+ const urlMatch = windowTitle.match(/https?:\/\/([^/\s]+)/);
177
+ if (urlMatch?.[1]) {
178
+ domain = urlMatch[1].replace(/^www\./, "");
179
+ }
180
+ // Pattern 2: common "title — domain.com" or "title - domain.com — Browser"
181
+ if (!domain) {
182
+ // Strip trailing " — Safari", " - Brave" etc.
183
+ const stripped = windowTitle.replace(/\s*[—–-]\s*(Safari|Brave|Chromium|Vivaldi|Opera)\s*$/, "");
184
+ // Check if the last segment looks like a domain
185
+ const parts = stripped.split(/\s*[—–-]\s*/);
186
+ const lastPart = parts[parts.length - 1]?.trim() ?? "";
187
+ if (/^[a-z0-9-]+\.[a-z]{2,}$/i.test(lastPart)) {
188
+ domain = lastPart.toLowerCase();
189
+ }
190
+ }
191
+ if (!domain)
192
+ return;
193
+ if (this.context?.domain === domain)
194
+ return;
195
+ const playbook = this.store.matchByDomain(domain);
196
+ this.context = buildCachedContext(domain, playbook);
197
+ }
198
+ // ═══════════════════════════════════════════════
199
+ // 2. GET HINTS — 0-2 lines per tool call
200
+ // ═══════════════════════════════════════════════
201
+ /**
202
+ * Returns relevant hints for this tool call. Max 2 hints.
203
+ * Cost: map lookups only, ~0ms.
204
+ */
205
+ getHints(toolName, params) {
206
+ if (!this.context?.playbook)
207
+ return [];
208
+ const hints = [];
209
+ // Check for known errors relevant to this tool
210
+ const errors = this.context.errorsByTool.get(toolName);
211
+ if (errors && errors.length > 0) {
212
+ // Pick highest severity error
213
+ const top = errors[0];
214
+ hints.push(`⚠ Known issue (${this.context.playbook.platform}): ${top.error} → ${top.solution}`);
215
+ }
216
+ // Check if there's a preferred selector for what the tool is targeting
217
+ if (hints.length < 2) {
218
+ const target = extractTarget(params);
219
+ if (target) {
220
+ // Look for a matching selector in playbook
221
+ const match = findRelevantSelector(target, this.context.allSelectors);
222
+ if (match) {
223
+ hints.push(`💡 Preferred selector (${this.context.playbook.platform}): ${match}`);
224
+ }
225
+ }
226
+ }
227
+ // Check if an executable playbook exists in playbooks/ dir
228
+ if (hints.length < 2 && this.execPlaybooksDir) {
229
+ const pb = this.context.playbook;
230
+ const execPath = path.join(this.execPlaybooksDir, `${pb.id}.json`);
231
+ try {
232
+ if (fs.existsSync(execPath)) {
233
+ const execPb = JSON.parse(fs.readFileSync(execPath, "utf-8"));
234
+ if (Array.isArray(execPb.steps) && execPb.steps.length > 0) {
235
+ hints.push(`📋 Executable playbook "${pb.id}" has ${execPb.steps.length} steps. Use job_create(task=..., playbookId="${pb.id}") for auto-execution.`);
236
+ }
237
+ }
238
+ }
239
+ catch { /* skip — don't break hints for a file read error */ }
240
+ }
241
+ // App mastery map hint
242
+ if (hints.length < 2 && this.context?.appMapData) {
243
+ const map = this.context.appMapData;
244
+ const zones = Object.keys(map.zones).length;
245
+ const verifiedPaths = map.navigationGraph.edges.filter((e) => e.verified).length;
246
+ const totalPaths = map.navigationGraph.edges.length;
247
+ const ratingDisplay = map.rating
248
+ ? (map.rating.grade === "0" ? "0" : `${map.rating.grade}${map.rating.subTier}`)
249
+ : map.masteryLevel.toUpperCase();
250
+ // Include page-specific zone info if we have page context
251
+ let pageInfo = "";
252
+ if (this._currentPageContext) {
253
+ const pageZoneKey = `page::${this._currentPageContext}`;
254
+ const pageZone = map.zones[pageZoneKey];
255
+ if (pageZone) {
256
+ pageInfo = `, page "${this._currentPageContext}" ${pageZone.elements.length} els`;
257
+ }
258
+ else {
259
+ pageInfo = `, page "${this._currentPageContext}" (new)`;
260
+ }
261
+ }
262
+ // Navigation graph info
263
+ const navNodes = Object.keys(map.navigationGraph.nodes).length;
264
+ let navInfo = "";
265
+ if (navNodes > 0) {
266
+ navInfo = `, nav: ${navNodes} pages ${totalPaths} transitions`;
267
+ // Show outgoing edges from current page
268
+ if (this._currentPageContext) {
269
+ const outgoing = map.navigationGraph.edges.filter((e) => e.from === this._currentPageContext);
270
+ if (outgoing.length > 0) {
271
+ const destinations = outgoing
272
+ .slice(0, 3)
273
+ .map((e) => `${e.to} (${e.action})`)
274
+ .join(", ");
275
+ const more = outgoing.length > 3 ? ` +${outgoing.length - 3} more` : "";
276
+ navInfo += ` [from here: ${destinations}${more}]`;
277
+ }
278
+ }
279
+ }
280
+ hints.push(`🗺 Map: ${map.appName} — Rating ${ratingDisplay} ` +
281
+ `(${(map.confidence * 100).toFixed(0)}%, ${zones} zones, ` +
282
+ `${verifiedPaths}/${totalPaths} paths${pageInfo}${navInfo})`);
283
+ }
284
+ return hints;
285
+ }
286
+ // ═══════════════════════════════════════════════
287
+ // 3. COLLECT — record outcome in memory buffer
288
+ // ═══════════════════════════════════════════════
289
+ /**
290
+ * Record a tool outcome. Just an array push — no disk, no AI.
291
+ */
292
+ recordOutcome(toolName, params, success, error) {
293
+ if (!this.context)
294
+ return;
295
+ this.learnings.push({
296
+ tool: toolName,
297
+ target: extractTarget(params),
298
+ domain: this.context.domain,
299
+ success,
300
+ error,
301
+ timestamp: new Date().toISOString(),
302
+ });
303
+ this.actionCount++;
304
+ // Auto-flush at threshold
305
+ if (this.actionCount >= FLUSH_THRESHOLD) {
306
+ this.flush();
307
+ }
308
+ }
309
+ // ═══════════════════════════════════════════════
310
+ // 4. FLUSH — merge learnings into playbook (one write)
311
+ // ═══════════════════════════════════════════════
312
+ /**
313
+ * Merge collected learnings into the matched playbook.
314
+ * Call on session_release or process exit.
315
+ * One disk write via PlaybookStore.save().
316
+ */
317
+ flush() {
318
+ if (this.learnings.length === 0)
319
+ return;
320
+ if (!this.context?.playbook) {
321
+ this.learnings = [];
322
+ this.actionCount = 0;
323
+ return;
324
+ }
325
+ const playbook = this.context.playbook;
326
+ let changed = false;
327
+ // ── Promote selectors that worked 2+ times ──
328
+ const selectorSuccessCount = new Map();
329
+ for (const l of this.learnings) {
330
+ if (l.success && l.target && /^[#.\[]|^[a-z]+[\[.#\s>+~]/.test(l.target) &&
331
+ !/\bon\w+\s*=/i.test(l.target)) {
332
+ const key = l.target;
333
+ selectorSuccessCount.set(key, (selectorSuccessCount.get(key) ?? 0) + 1);
334
+ }
335
+ }
336
+ if (!playbook.selectors)
337
+ playbook.selectors = {};
338
+ if (!playbook.selectors["auto_discovered"])
339
+ playbook.selectors["auto_discovered"] = {};
340
+ for (const [selector, count] of selectorSuccessCount) {
341
+ if (count >= MIN_OCCURRENCES_TO_PROMOTE) {
342
+ // Don't overwrite existing selectors
343
+ const existing = this.context.allSelectors;
344
+ const alreadyKnown = [...existing.values()].some(s => s === selector);
345
+ if (!alreadyKnown) {
346
+ const key = `auto_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 5)}`;
347
+ playbook.selectors["auto_discovered"][key] = selector;
348
+ changed = true;
349
+ }
350
+ }
351
+ }
352
+ // ── Promote error patterns seen 2+ times with a common error message ──
353
+ const errorCounts = new Map();
354
+ for (const l of this.learnings) {
355
+ if (!l.success && l.error) {
356
+ const key = `${l.tool}::${l.error}`;
357
+ const existing = errorCounts.get(key);
358
+ if (existing) {
359
+ existing.count++;
360
+ }
361
+ else {
362
+ errorCounts.set(key, { count: 1, tool: l.tool, error: l.error });
363
+ }
364
+ }
365
+ }
366
+ if (!playbook.errors)
367
+ playbook.errors = [];
368
+ for (const [, { count, tool, error }] of errorCounts) {
369
+ if (count >= MIN_OCCURRENCES_TO_PROMOTE) {
370
+ // Don't add duplicates
371
+ const alreadyKnown = playbook.errors.some(e => e.error === error);
372
+ if (!alreadyKnown) {
373
+ playbook.errors.push({
374
+ error,
375
+ context: `tool: ${tool}, domain: ${this.context.domain}`,
376
+ solution: "No resolution yet — investigate and update this entry",
377
+ severity: count >= 4 ? "high" : "medium",
378
+ });
379
+ changed = true;
380
+ }
381
+ }
382
+ }
383
+ // ── Save if changed ──
384
+ if (changed) {
385
+ this.store.save(playbook);
386
+ }
387
+ // Reset
388
+ this.learnings = [];
389
+ this.actionCount = 0;
390
+ }
391
+ /** Get the currently matched playbook (if any). */
392
+ getActivePlaybook() {
393
+ return this.context?.playbook ?? null;
394
+ }
395
+ /** Get the current domain being tracked. */
396
+ getCurrentDomain() {
397
+ return this.context?.domain ?? null;
398
+ }
399
+ }
400
+ // ── Helpers ──
401
+ function buildCachedContext(domain, playbook) {
402
+ const errorsByTool = new Map();
403
+ const allSelectors = new Map();
404
+ if (playbook) {
405
+ // Index errors by relevant tool names
406
+ if (playbook.errors) {
407
+ for (const err of playbook.errors) {
408
+ const errLower = `${err.error} ${err.context} ${err.solution}`.toLowerCase();
409
+ for (const [tool, keywords] of Object.entries(TOOL_ERROR_KEYWORDS)) {
410
+ if (keywords.some(kw => errLower.includes(kw))) {
411
+ const existing = errorsByTool.get(tool) ?? [];
412
+ existing.push(err);
413
+ errorsByTool.set(tool, existing);
414
+ }
415
+ }
416
+ }
417
+ // Sort each tool's errors by severity
418
+ const severityOrder = { high: 0, medium: 1, low: 2 };
419
+ for (const [tool, errors] of errorsByTool) {
420
+ errors.sort((a, b) => (severityOrder[a.severity] ?? 2) - (severityOrder[b.severity] ?? 2));
421
+ errorsByTool.set(tool, errors);
422
+ }
423
+ }
424
+ // Flatten all selectors into one map
425
+ if (playbook.selectors) {
426
+ for (const [group, sels] of Object.entries(playbook.selectors)) {
427
+ for (const [name, sel] of Object.entries(sels)) {
428
+ allSelectors.set(`${group}.${name}`, sel);
429
+ }
430
+ }
431
+ }
432
+ }
433
+ return { domain, playbook, errorsByTool, allSelectors, appMapData: null };
434
+ }
435
+ function extractTarget(params) {
436
+ for (const name of TARGET_PARAM_NAMES) {
437
+ const val = params[name];
438
+ if (typeof val === "string" && val.length > 0)
439
+ return val;
440
+ }
441
+ return null;
442
+ }
443
+ function findRelevantSelector(target, selectors) {
444
+ if (selectors.size === 0 || !target)
445
+ return null;
446
+ const targetLower = target.toLowerCase();
447
+ // Check if any selector name loosely matches the target
448
+ for (const [name, sel] of selectors) {
449
+ const nameLower = name.toLowerCase();
450
+ // If target text matches a selector name (e.g., target="Search" matches "toolbar.search")
451
+ if (nameLower.includes(targetLower) || targetLower.includes(nameLower.split(".").pop() ?? "")) {
452
+ return `${name}: ${sel}`;
453
+ }
454
+ }
455
+ return null;
456
+ }
457
+ /**
458
+ * Extract a page/view context from a window title.
459
+ *
460
+ * Window titles commonly follow patterns like:
461
+ * "Tasks - My Workspace - Notion" → "Tasks"
462
+ * "Settings > General - MyApp" → "Settings > General"
463
+ * "Home | Slack" → "Home"
464
+ * "Untitled - Figma" → "Untitled"
465
+ * "MyApp" → "MyApp" (single segment, still useful)
466
+ *
467
+ * Strategy: split on common delimiters (" - ", " | ", " — "), take the first
468
+ * segment as the page context. This is intentionally simple and conservative.
469
+ */
470
+ export function extractPageContext(windowTitle) {
471
+ if (!windowTitle || windowTitle.trim().length === 0)
472
+ return null;
473
+ const title = windowTitle.trim();
474
+ // Split on common title delimiters: " - ", " — ", " | "
475
+ const parts = title.split(/\s+[-—|]\s+/);
476
+ // Take the first segment — this is typically the page/view/document name
477
+ const page = parts[0]?.trim();
478
+ if (!page || page.length === 0)
479
+ return null;
480
+ // V5: Reject garbage — too short or consisting only of punctuation/delimiters
481
+ if (page.length < 2)
482
+ return null;
483
+ if (/^[\s\-—|_.,;:!?]+$/.test(page))
484
+ return null;
485
+ // Truncate overly long page contexts (window titles can be verbose)
486
+ if (page.length > 80)
487
+ return page.slice(0, 80);
488
+ return page;
489
+ }
@@ -14,18 +14,11 @@
14
14
  //
15
15
  // You should have received a copy of the GNU Affero General Public License
16
16
  // along with ScreenHand. If not, see <https://www.gnu.org/licenses/>.
17
-
18
17
  import { TimelineLogger } from "./logging/timeline-logger.js";
19
18
  import { MvpMcpServer } from "./mcp/server.js";
20
- import {
21
- type AppAdapter,
22
- PlaceholderAppAdapter,
23
- } from "./runtime/app-adapter.js";
19
+ import { PlaceholderAppAdapter, } from "./runtime/app-adapter.js";
24
20
  import { CdpChromeAdapter } from "./runtime/cdp-chrome-adapter.js";
25
21
  import { AutomationRuntimeService } from "./runtime/service.js";
26
-
27
- // Re-export types and adapters for external use
28
- export type { AppAdapter } from "./runtime/app-adapter.js";
29
22
  export { PlaceholderAppAdapter } from "./runtime/app-adapter.js";
30
23
  export { CdpChromeAdapter } from "./runtime/cdp-chrome-adapter.js";
31
24
  export { AccessibilityAdapter } from "./runtime/accessibility-adapter.js";
@@ -37,56 +30,43 @@ export { StateObserver } from "./runtime/state-observer.js";
37
30
  export { PlanningLoop } from "./runtime/planning-loop.js";
38
31
  export { AutomationRuntimeService } from "./runtime/service.js";
39
32
  export { MvpMcpServer } from "./mcp/server.js";
40
-
41
- export interface RuntimeApp {
42
- runtime: AutomationRuntimeService;
43
- mcp: MvpMcpServer;
33
+ export { createMcpStdioServer, startMcpStdioServer } from "./mcp/mcp-stdio-server.js";
34
+ export { runAgentLoop } from "./agent/loop.js";
35
+ export function createRuntimeApp(adapter) {
36
+ const logger = new TimelineLogger();
37
+ const runtime = new AutomationRuntimeService(adapter, logger);
38
+ const mcp = new MvpMcpServer(runtime);
39
+ return { runtime, mcp };
44
40
  }
45
-
46
- export function createRuntimeApp(adapter: AppAdapter): RuntimeApp {
47
- const logger = new TimelineLogger();
48
- const runtime = new AutomationRuntimeService(adapter, logger);
49
- const mcp = new MvpMcpServer(runtime);
50
- return { runtime, mcp };
51
- }
52
-
53
- async function createDefaultAdapter(): Promise<AppAdapter> {
54
- if (process.env.AUTOMATOR_ADAPTER === "placeholder") {
55
- return new PlaceholderAppAdapter();
56
- }
57
- if (process.env.AUTOMATOR_ADAPTER === "composite") {
58
- // Lazy import to avoid requiring Swift bridge for CDP-only usage
59
- const { MacOSBridgeClient } = await import("./native/macos-bridge-client.js");
60
- const { CompositeAdapter } = await import("./runtime/composite-adapter.js");
61
- const bridge = new MacOSBridgeClient();
62
- return new CompositeAdapter(bridge, {
63
- headless: process.env.AUTOMATOR_HEADLESS === "1",
41
+ async function createDefaultAdapter() {
42
+ if (process.env.AUTOMATOR_ADAPTER === "placeholder") {
43
+ return new PlaceholderAppAdapter();
44
+ }
45
+ if (process.env.AUTOMATOR_ADAPTER === "composite") {
46
+ // Lazy import to avoid requiring Swift bridge for CDP-only usage
47
+ const { MacOSBridgeClient } = await import("./native/macos-bridge-client.js");
48
+ const { CompositeAdapter } = await import("./runtime/composite-adapter.js");
49
+ const bridge = new MacOSBridgeClient();
50
+ return new CompositeAdapter(bridge, {
51
+ headless: process.env.AUTOMATOR_HEADLESS === "1",
52
+ });
53
+ }
54
+ if (process.env.AUTOMATOR_ADAPTER === "accessibility") {
55
+ const { MacOSBridgeClient } = await import("./native/macos-bridge-client.js");
56
+ const { AccessibilityAdapter } = await import("./runtime/accessibility-adapter.js");
57
+ const bridge = new MacOSBridgeClient();
58
+ return new AccessibilityAdapter(bridge);
59
+ }
60
+ return new CdpChromeAdapter({
61
+ headless: process.env.AUTOMATOR_HEADLESS === "1",
64
62
  });
65
- }
66
- if (process.env.AUTOMATOR_ADAPTER === "accessibility") {
67
- const { MacOSBridgeClient } = await import("./native/macos-bridge-client.js");
68
- const { AccessibilityAdapter } = await import("./runtime/accessibility-adapter.js");
69
- const bridge = new MacOSBridgeClient();
70
- return new AccessibilityAdapter(bridge);
71
- }
72
- return new CdpChromeAdapter({
73
- headless: process.env.AUTOMATOR_HEADLESS === "1",
74
- });
75
63
  }
76
-
77
64
  const app = createRuntimeApp(await createDefaultAdapter());
78
-
79
65
  if (process.argv.includes("--healthcheck")) {
80
- const session = await app.runtime.sessionStart("automation");
81
- console.log(
82
- JSON.stringify(
83
- {
66
+ const session = await app.runtime.sessionStart("automation");
67
+ console.log(JSON.stringify({
84
68
  status: "ok",
85
69
  session,
86
70
  note: "Runtime loaded with universal adapter support.",
87
- },
88
- null,
89
- 2,
90
- ),
91
- );
71
+ }, null, 2));
92
72
  }