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,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
+ }