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,233 @@
1
+ // Copyright (C) 2025 Clazro Technology Private Limited
2
+ // SPDX-License-Identifier: AGPL-3.0-only
3
+ import * as fs from "node:fs";
4
+ import * as path from "node:path";
5
+ /**
6
+ * CoverageAuditor — answers "How well do we know this app?"
7
+ *
8
+ * Compares reference files, playbooks, menu scans, and learning data
9
+ * to identify gaps and generate recommendations.
10
+ */
11
+ /**
12
+ * Normalize a menu path shortcut name by extracting the last segment.
13
+ * "Edit > Undo" → "undo", "File.Save" → "save"
14
+ */
15
+ function normalizeShortcutName(name) {
16
+ const parts = name.split(/[.>]/);
17
+ return (parts[parts.length - 1] ?? name).trim().toLowerCase();
18
+ }
19
+ /**
20
+ * Strip parenthetical descriptions from key combos.
21
+ * "Cmd+N (desktop app)" → "cmd+n"
22
+ */
23
+ function normalizeKeyCombo(keys) {
24
+ return keys.replace(/\s*\([^)]*\)\s*/g, "").trim().toLowerCase();
25
+ }
26
+ export class CoverageAuditor {
27
+ referencesDir;
28
+ playbooksDir;
29
+ learningEngine;
30
+ goalStore;
31
+ constructor(referencesDir, playbooksDir, learningEngine, goalStore) {
32
+ this.referencesDir = referencesDir;
33
+ this.playbooksDir = playbooksDir;
34
+ this.learningEngine = learningEngine;
35
+ this.goalStore = goalStore;
36
+ }
37
+ /**
38
+ * Generate a full coverage report for an app.
39
+ */
40
+ audit(bundleId, appName, menuScan) {
41
+ const refs = this.loadReferences(bundleId);
42
+ const playbooks = this.loadPlaybooks(bundleId);
43
+ // Count what we know
44
+ let shortcutsKnown = 0;
45
+ let selectorsKnown = 0;
46
+ let flowsKnown = 0;
47
+ let errorsDocumented = 0;
48
+ for (const ref of refs) {
49
+ if (ref.shortcuts) {
50
+ for (const category of Object.values(ref.shortcuts)) {
51
+ shortcutsKnown += Object.keys(category).length;
52
+ }
53
+ }
54
+ if (ref.selectors) {
55
+ for (const group of Object.values(ref.selectors)) {
56
+ selectorsKnown += Object.keys(group).length;
57
+ }
58
+ }
59
+ if (ref.flows) {
60
+ flowsKnown += Object.keys(ref.flows).length;
61
+ }
62
+ if (ref.errors) {
63
+ errorsDocumented += ref.errors.length;
64
+ }
65
+ }
66
+ // Compare menu scan against reference shortcuts
67
+ const menuPathsNotCovered = [];
68
+ const shortcutsNotInReference = [];
69
+ if (menuScan) {
70
+ // Build a map keyed by normalized name → normalized keys for comparison
71
+ const refShortcuts = new Map();
72
+ for (const ref of refs) {
73
+ if (ref.shortcuts) {
74
+ for (const category of Object.values(ref.shortcuts)) {
75
+ for (const [name, keys] of Object.entries(category)) {
76
+ refShortcuts.set(name.toLowerCase(), normalizeKeyCombo(keys));
77
+ }
78
+ }
79
+ }
80
+ }
81
+ for (const [menuPath, keys] of Object.entries(menuScan.shortcuts)) {
82
+ const normalizedName = normalizeShortcutName(menuPath);
83
+ const normalizedKeys = normalizeKeyCombo(keys);
84
+ const refKeys = refShortcuts.get(normalizedName);
85
+ if (refKeys !== normalizedKeys) {
86
+ shortcutsNotInReference.push(`${menuPath}: ${keys}`);
87
+ }
88
+ }
89
+ // Find menu paths not covered at all
90
+ const flatPaths = this.flattenMenuPaths(menuScan.menuTree);
91
+ for (const p of flatPaths) {
92
+ const covered = refs.some((ref) => {
93
+ if (!ref.flows)
94
+ return false;
95
+ return Object.values(ref.flows).some((f) => f.steps.some((s) => s.toLowerCase().includes(p.toLowerCase())));
96
+ });
97
+ if (!covered) {
98
+ menuPathsNotCovered.push(p);
99
+ }
100
+ }
101
+ }
102
+ // Identify common workflows without playbooks
103
+ const COMMON_WORKFLOWS = [
104
+ "export", "import", "save as", "new project", "undo",
105
+ "preferences", "settings", "print", "share",
106
+ ];
107
+ const workflowsWithNoPlaybook = COMMON_WORKFLOWS.filter((w) => {
108
+ const hasPlaybook = playbooks.some((p) => p.name.toLowerCase().includes(w));
109
+ const hasFlow = refs.some((r) => r.flows && Object.keys(r.flows).some((k) => k.includes(w)));
110
+ return !hasPlaybook && !hasFlow;
111
+ });
112
+ // Quality scores from learning engine
113
+ let selectorStabilityScore = 0;
114
+ let playbookSuccessRate = 0;
115
+ let averageRecoveryTime = 0;
116
+ // Compute playbookSuccessRate from GoalStore
117
+ if (this.goalStore) {
118
+ const allGoals = this.goalStore.list();
119
+ const playbookGoals = allGoals.filter((g) => g.subgoals.some((sg) => sg.plan?.source === "playbook"));
120
+ if (playbookGoals.length > 0) {
121
+ const completed = playbookGoals.filter((g) => g.status === "completed").length;
122
+ playbookSuccessRate = completed / playbookGoals.length;
123
+ }
124
+ }
125
+ if (this.learningEngine) {
126
+ const summary = this.learningEngine.getAppSummary(bundleId);
127
+ if (summary.locatorEntries > 0) {
128
+ const entries = this.learningEngine.locators.getAllEntries()
129
+ .filter((e) => e.key.startsWith(`${bundleId}::`));
130
+ if (entries.length > 0) {
131
+ selectorStabilityScore =
132
+ entries.reduce((sum, e) => sum + e.score, 0) / entries.length;
133
+ }
134
+ }
135
+ const recEntries = this.learningEngine.recovery.getAllEntries()
136
+ .filter((e) => e.key.endsWith(`::${bundleId}`));
137
+ if (recEntries.length > 0) {
138
+ averageRecoveryTime =
139
+ recEntries.reduce((sum, e) => sum + e.avgDurationMs, 0) / recEntries.length;
140
+ }
141
+ }
142
+ // Generate recommendations
143
+ const highValueGaps = [];
144
+ if (shortcutsKnown === 0) {
145
+ highValueGaps.push("No shortcuts documented — run scan_menu_bar to extract keyboard shortcuts");
146
+ }
147
+ if (selectorsKnown === 0) {
148
+ highValueGaps.push("No selectors documented — run platform_explore to discover stable selectors");
149
+ }
150
+ if (playbooks.length === 0) {
151
+ highValueGaps.push("No playbooks available — record common workflows with playbook_record");
152
+ }
153
+ if (errorsDocumented === 0) {
154
+ highValueGaps.push("No error patterns documented — errors will be learned automatically over time");
155
+ }
156
+ if (workflowsWithNoPlaybook.length > 0) {
157
+ highValueGaps.push(`Common workflows without playbooks: ${workflowsWithNoPlaybook.join(", ")}`);
158
+ }
159
+ if (menuScan && shortcutsNotInReference.length > 10) {
160
+ highValueGaps.push(`${shortcutsNotInReference.length} shortcuts found in menu bar but missing from reference`);
161
+ }
162
+ return {
163
+ app: appName,
164
+ bundleId,
165
+ shortcutsKnown,
166
+ selectorsKnown,
167
+ flowsKnown,
168
+ playbooksAvailable: playbooks.length,
169
+ errorsDocumented,
170
+ menuPathsNotCovered: menuPathsNotCovered.slice(0, 50),
171
+ shortcutsNotInReference: shortcutsNotInReference.slice(0, 50),
172
+ workflowsWithNoPlaybook,
173
+ selectorStabilityScore,
174
+ playbookSuccessRate,
175
+ averageRecoveryTime,
176
+ highValueGaps,
177
+ generatedAt: new Date().toISOString(),
178
+ };
179
+ }
180
+ loadReferences(bundleId) {
181
+ const refs = [];
182
+ try {
183
+ const files = fs.readdirSync(this.referencesDir);
184
+ for (const file of files) {
185
+ if (!file.endsWith(".json"))
186
+ continue;
187
+ try {
188
+ const raw = fs.readFileSync(path.join(this.referencesDir, file), "utf-8");
189
+ const ref = JSON.parse(raw);
190
+ if (ref.bundleId === bundleId || ref.platform === bundleId) {
191
+ refs.push(ref);
192
+ }
193
+ }
194
+ catch { /* skip */ }
195
+ }
196
+ }
197
+ catch { /* dir not found */ }
198
+ return refs;
199
+ }
200
+ loadPlaybooks(bundleId) {
201
+ const playbooks = [];
202
+ try {
203
+ const files = fs.readdirSync(this.playbooksDir);
204
+ for (const file of files) {
205
+ if (!file.endsWith(".json"))
206
+ continue;
207
+ try {
208
+ const raw = fs.readFileSync(path.join(this.playbooksDir, file), "utf-8");
209
+ const pb = JSON.parse(raw);
210
+ if (pb.bundleId === bundleId || pb.platform === bundleId) {
211
+ playbooks.push(pb);
212
+ }
213
+ }
214
+ catch { /* skip */ }
215
+ }
216
+ }
217
+ catch { /* dir not found */ }
218
+ return playbooks;
219
+ }
220
+ flattenMenuPaths(nodes, prefix = []) {
221
+ const paths = [];
222
+ for (const node of nodes) {
223
+ const p = [...prefix, node.title];
224
+ if (!node.children || node.children.length === 0) {
225
+ paths.push(p.join(" > "));
226
+ }
227
+ else {
228
+ paths.push(...this.flattenMenuPaths(node.children, p));
229
+ }
230
+ }
231
+ return paths;
232
+ }
233
+ }
@@ -0,0 +1,164 @@
1
+ // Copyright (C) 2025 Clazro Technology Private Limited
2
+ // SPDX-License-Identifier: AGPL-3.0-only
3
+ import { parseShortcutsFromHTML, parseShortcutsFromText, parseShortcutsFromMarkdown, } from "./shortcut-extractor.js";
4
+ /**
5
+ * DocParser — extracts structured knowledge from documentation pages.
6
+ * Handles HTML, markdown, and plain text.
7
+ */
8
+ export class DocParser {
9
+ /**
10
+ * Parse a documentation page and extract shortcuts, flows, and tips.
11
+ */
12
+ parse(content, url, format = "html") {
13
+ const title = this.extractTitle(content, format);
14
+ const shortcuts = this.extractShortcuts(content, format);
15
+ const flows = this.extractFlows(content, format);
16
+ const tips = this.extractTips(content, format);
17
+ return {
18
+ url,
19
+ title,
20
+ shortcuts,
21
+ flows,
22
+ tips,
23
+ parsedAt: new Date().toISOString(),
24
+ };
25
+ }
26
+ /**
27
+ * Extract page title.
28
+ */
29
+ extractTitle(content, format) {
30
+ if (format === "html") {
31
+ const titleMatch = content.match(/<title[^>]*>([\s\S]*?)<\/title>/i);
32
+ if (titleMatch)
33
+ return titleMatch[1].replace(/<[^>]+>/g, "").trim();
34
+ const h1Match = content.match(/<h1[^>]*>([\s\S]*?)<\/h1>/i);
35
+ if (h1Match)
36
+ return h1Match[1].replace(/<[^>]+>/g, "").trim();
37
+ }
38
+ else if (format === "markdown") {
39
+ const mdTitle = content.match(/^#\s+(.+)$/m);
40
+ if (mdTitle)
41
+ return mdTitle[1].trim();
42
+ }
43
+ return "Untitled";
44
+ }
45
+ /**
46
+ * Extract shortcuts from the content.
47
+ */
48
+ extractShortcuts(content, format) {
49
+ switch (format) {
50
+ case "html":
51
+ return parseShortcutsFromHTML(content);
52
+ case "markdown":
53
+ return parseShortcutsFromMarkdown(content);
54
+ case "text":
55
+ return parseShortcutsFromText(content);
56
+ default:
57
+ return [];
58
+ }
59
+ }
60
+ /**
61
+ * Extract workflow/how-to steps from documentation.
62
+ * Looks for numbered lists, step-by-step sections.
63
+ */
64
+ extractFlows(content, format) {
65
+ const flows = [];
66
+ const stripped = format === "html" ? this.stripHTML(content) : content;
67
+ // Find sections with numbered steps
68
+ // Pattern: heading followed by numbered list
69
+ const sectionRegex = /(?:^|\n)(?:#{1,3}\s+|<h[1-3][^>]*>)(.+?)(?:<\/h[1-3]>)?(?:\n|$)([\s\S]*?)(?=(?:\n(?:#{1,3}\s+|<h[1-3]))|$)/gi;
70
+ let match;
71
+ while ((match = sectionRegex.exec(stripped)) !== null) {
72
+ const heading = match[1].replace(/<[^>]+>/g, "").trim();
73
+ const body = match[2];
74
+ // Check if the section has numbered steps
75
+ const stepRegex = /(?:^|\n)\s*(?:(\d+)[.)]\s*|Step\s+\d+[:.]\s*)(.+)/gi;
76
+ const steps = [];
77
+ let stepMatch;
78
+ while ((stepMatch = stepRegex.exec(body)) !== null) {
79
+ const desc = stepMatch[2].trim();
80
+ if (desc.length > 5) {
81
+ steps.push({
82
+ description: desc,
83
+ tool: this.inferTool(desc),
84
+ params: this.inferParams(desc),
85
+ });
86
+ }
87
+ }
88
+ if (steps.length >= 2) {
89
+ flows.push({ name: heading, steps });
90
+ }
91
+ }
92
+ return flows;
93
+ }
94
+ /**
95
+ * Extract tips/best practices from the content.
96
+ */
97
+ extractTips(content, format) {
98
+ const tips = [];
99
+ const stripped = format === "html" ? this.stripHTML(content) : content;
100
+ // Look for tip/note/important callouts
101
+ const tipRegex = /(?:tip|note|important|best practice|pro tip|hint)[:\s]*(.+?)(?:\n|$)/gi;
102
+ let match;
103
+ while ((match = tipRegex.exec(stripped)) !== null) {
104
+ const tip = match[1].trim();
105
+ if (tip.length > 10 && tip.length < 500) {
106
+ tips.push(tip);
107
+ }
108
+ }
109
+ return tips.slice(0, 20); // Cap tips
110
+ }
111
+ /**
112
+ * Infer the ScreenHand tool from a step description.
113
+ */
114
+ inferTool(description) {
115
+ const lower = description.toLowerCase();
116
+ if (lower.includes("click") && lower.includes("menu"))
117
+ return "menu_click";
118
+ if (lower.includes("click"))
119
+ return "click_text";
120
+ if (lower.includes("type") || lower.includes("enter"))
121
+ return "type_text";
122
+ if (lower.includes("press") || lower.includes("keyboard") || lower.includes("shortcut"))
123
+ return "key";
124
+ if (lower.includes("select"))
125
+ return "click_text";
126
+ if (lower.includes("drag"))
127
+ return "drag";
128
+ if (lower.includes("scroll"))
129
+ return "scroll";
130
+ if (lower.includes("navigate") || lower.includes("go to") || lower.includes("open"))
131
+ return "menu_click";
132
+ return undefined;
133
+ }
134
+ /**
135
+ * Infer tool params from a step description.
136
+ */
137
+ inferParams(description) {
138
+ // Extract quoted text as target
139
+ const quoteMatch = description.match(/["'"](.+?)["'"]/);
140
+ if (quoteMatch) {
141
+ return { text: quoteMatch[1] };
142
+ }
143
+ // Extract menu path: File > Export > Media
144
+ const menuMatch = description.match(/(?:click|go to|select|choose|open)\s+(.+?>.*)/i);
145
+ if (menuMatch) {
146
+ const path = menuMatch[1]
147
+ .split(/\s*>\s*/)
148
+ .map((s) => s.trim())
149
+ .filter(Boolean);
150
+ if (path.length >= 2) {
151
+ return { menuPath: path.join("/") };
152
+ }
153
+ }
154
+ return undefined;
155
+ }
156
+ stripHTML(html) {
157
+ return html
158
+ .replace(/<script[\s\S]*?<\/script>/gi, "")
159
+ .replace(/<style[\s\S]*?<\/style>/gi, "")
160
+ .replace(/<[^>]+>/g, " ")
161
+ .replace(/\s+/g, " ")
162
+ .trim();
163
+ }
164
+ }
@@ -0,0 +1,8 @@
1
+ // Copyright (C) 2025 Clazro Technology Private Limited
2
+ // SPDX-License-Identifier: AGPL-3.0-only
3
+ export { MenuScanner } from "./menu-scanner.js";
4
+ export { DocParser } from "./doc-parser.js";
5
+ export { TutorialExtractor } from "./tutorial-extractor.js";
6
+ export { ReferenceMerger } from "./reference-merger.js";
7
+ export { CoverageAuditor } from "./coverage-auditor.js";
8
+ export { parseShortcutsFromHTML, parseShortcutsFromText, parseShortcutsFromMarkdown, shortcutsToReferenceFormat, } from "./shortcut-extractor.js";
@@ -0,0 +1,152 @@
1
+ // Copyright (C) 2025 Clazro Technology Private Limited
2
+ // SPDX-License-Identifier: AGPL-3.0-only
3
+ /**
4
+ * MenuScanner — scans an app's menu bar via AX tree, extracting
5
+ * all menu paths, keyboard shortcuts, and enabled/disabled states.
6
+ *
7
+ * Uses the native bridge to walk the AXMenuBar subtree.
8
+ */
9
+ export class MenuScanner {
10
+ bridge;
11
+ constructor(bridge) {
12
+ this.bridge = bridge;
13
+ }
14
+ /**
15
+ * Scan the menu bar of a running app.
16
+ */
17
+ async scan(pid, bundleId, appName) {
18
+ const tree = await this.bridge.call("ax.getMenuBar", {
19
+ pid,
20
+ maxDepth: 10,
21
+ });
22
+ const menuTree = this.parseAXTree(tree);
23
+ const items = this.flattenTree(menuTree, []);
24
+ const shortcuts = {};
25
+ for (const item of items) {
26
+ if (item.shortcut) {
27
+ shortcuts[item.path.join(".")] = item.shortcut;
28
+ }
29
+ }
30
+ return {
31
+ bundleId,
32
+ appName,
33
+ totalMenus: menuTree.length,
34
+ totalItems: items.length,
35
+ shortcuts,
36
+ menuTree,
37
+ scannedAt: new Date().toISOString(),
38
+ };
39
+ }
40
+ /**
41
+ * Convert scan result to reference JSON format (shortcuts + flows sections).
42
+ */
43
+ toReferenceFormat(result) {
44
+ const shortcuts = {};
45
+ const menuPaths = [];
46
+ for (const topMenu of result.menuTree) {
47
+ const category = topMenu.title;
48
+ const items = this.flattenTree([topMenu], []);
49
+ for (const item of items) {
50
+ menuPaths.push(item.path.join(" > "));
51
+ if (item.shortcut) {
52
+ if (!shortcuts[category])
53
+ shortcuts[category] = {};
54
+ shortcuts[category][item.title] = item.shortcut;
55
+ }
56
+ }
57
+ }
58
+ return { shortcuts, menuPaths };
59
+ }
60
+ /**
61
+ * Parse raw AX tree response into MenuNode tree.
62
+ */
63
+ parseAXTree(tree) {
64
+ if (!tree || !Array.isArray(tree.children))
65
+ return [];
66
+ const nodes = [];
67
+ for (const child of tree.children) {
68
+ const node = this.parseNode(child);
69
+ if (node)
70
+ nodes.push(node);
71
+ }
72
+ return nodes;
73
+ }
74
+ parseNode(axNode) {
75
+ if (!axNode)
76
+ return null;
77
+ const role = axNode.role ?? axNode.AXRole ?? "";
78
+ const title = axNode.title ??
79
+ axNode.AXTitle ??
80
+ axNode.description ??
81
+ axNode.AXDescription ??
82
+ "";
83
+ // Skip separators
84
+ if (role === "AXSeparator" || title === "separator")
85
+ return null;
86
+ const shortcut = this.extractShortcut(axNode);
87
+ const enabled = axNode.enabled !== false && axNode.AXEnabled !== false;
88
+ const children = [];
89
+ if (Array.isArray(axNode.children)) {
90
+ for (const child of axNode.children) {
91
+ const childNode = this.parseNode(child);
92
+ if (childNode)
93
+ children.push(childNode);
94
+ }
95
+ }
96
+ // AXMenu containers have no title but hold the actual menu items as children.
97
+ // Pass through their children instead of dropping the entire subtree.
98
+ if (!title && children.length > 0) {
99
+ return { title: role, shortcut: null, enabled: true, children };
100
+ }
101
+ // Skip leaf nodes with no title (not containers, not useful)
102
+ if (!title)
103
+ return null;
104
+ return {
105
+ title: String(title),
106
+ shortcut,
107
+ enabled,
108
+ children,
109
+ };
110
+ }
111
+ /**
112
+ * Extract keyboard shortcut from AX node attributes.
113
+ */
114
+ extractShortcut(axNode) {
115
+ // macOS AX provides shortcuts via AXMenuItemCmdChar, AXMenuItemCmdModifiers
116
+ const cmdChar = axNode.AXMenuItemCmdChar ?? axNode.cmdChar ?? null;
117
+ const cmdModifiers = axNode.AXMenuItemCmdModifiers ?? axNode.cmdModifiers ?? 0;
118
+ if (!cmdChar)
119
+ return null;
120
+ const parts = [];
121
+ // Modifier masks: Control=4, Option=2, Shift=1, Cmd=0 (always present)
122
+ if (cmdModifiers & 4)
123
+ parts.push("Ctrl");
124
+ if (cmdModifiers & 2)
125
+ parts.push("Option");
126
+ if (cmdModifiers & 1)
127
+ parts.push("Shift");
128
+ parts.push("Cmd");
129
+ parts.push(String(cmdChar));
130
+ return parts.join("+");
131
+ }
132
+ /**
133
+ * Flatten tree to list of items with full paths.
134
+ */
135
+ flattenTree(nodes, parentPath) {
136
+ const items = [];
137
+ for (const node of nodes) {
138
+ const path = [...parentPath, node.title];
139
+ items.push({
140
+ path,
141
+ title: node.title,
142
+ shortcut: node.shortcut,
143
+ enabled: node.enabled,
144
+ hasSubmenu: node.children.length > 0,
145
+ });
146
+ if (node.children.length > 0) {
147
+ items.push(...this.flattenTree(node.children, path));
148
+ }
149
+ }
150
+ return items;
151
+ }
152
+ }