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