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,186 @@
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
+ import { writeFileAtomicSync } from "../util/atomic-write.js";
6
+ import { shortcutsToReferenceFormat } from "./shortcut-extractor.js";
7
+ /**
8
+ * ReferenceMerger — merges ingested knowledge into existing reference files.
9
+ * Creates new reference files when no matching file exists.
10
+ */
11
+ export class ReferenceMerger {
12
+ referencesDir;
13
+ constructor(referencesDir) {
14
+ this.referencesDir = referencesDir;
15
+ }
16
+ /**
17
+ * Merge shortcuts from a menu scan into the reference file.
18
+ */
19
+ mergeMenuScan(scan) {
20
+ const ref = this.loadOrCreate(scan.bundleId, scan.appName);
21
+ const { shortcuts: scannedShortcuts } = this.menuScanToShortcuts(scan);
22
+ let added = 0;
23
+ let updated = 0;
24
+ if (!ref.shortcuts)
25
+ ref.shortcuts = {};
26
+ for (const [category, entries] of Object.entries(scannedShortcuts)) {
27
+ if (!ref.shortcuts[category]) {
28
+ ref.shortcuts[category] = {};
29
+ }
30
+ for (const [name, keys] of Object.entries(entries)) {
31
+ if (!ref.shortcuts[category][name]) {
32
+ added++;
33
+ }
34
+ else if (ref.shortcuts[category][name] !== keys) {
35
+ updated++;
36
+ }
37
+ ref.shortcuts[category][name] = keys;
38
+ }
39
+ }
40
+ const filePath = this.save(ref);
41
+ return { filePath, added, updated };
42
+ }
43
+ /**
44
+ * Merge shortcuts from parsed documentation.
45
+ */
46
+ mergeDocShortcuts(shortcuts, bundleId, appName) {
47
+ const ref = this.loadOrCreate(bundleId, appName);
48
+ const formatted = shortcutsToReferenceFormat(shortcuts);
49
+ let added = 0;
50
+ let updated = 0;
51
+ if (!ref.shortcuts)
52
+ ref.shortcuts = {};
53
+ for (const [category, entries] of Object.entries(formatted)) {
54
+ if (!ref.shortcuts[category]) {
55
+ ref.shortcuts[category] = {};
56
+ }
57
+ for (const [name, keys] of Object.entries(entries)) {
58
+ if (!ref.shortcuts[category][name]) {
59
+ added++;
60
+ }
61
+ else if (ref.shortcuts[category][name] !== keys) {
62
+ updated++;
63
+ }
64
+ ref.shortcuts[category][name] = keys;
65
+ }
66
+ }
67
+ const filePath = this.save(ref);
68
+ return { filePath, added, updated };
69
+ }
70
+ /**
71
+ * Merge flows from parsed documentation.
72
+ */
73
+ mergeDocFlows(docResult, bundleId, appName) {
74
+ const ref = this.loadOrCreate(bundleId, appName);
75
+ if (!ref.flows)
76
+ ref.flows = {};
77
+ let added = 0;
78
+ for (const flow of docResult.flows) {
79
+ const key = flow.name
80
+ .toLowerCase()
81
+ .replace(/[^a-z0-9]+/g, "_")
82
+ .replace(/^_|_$/g, "");
83
+ if (!ref.flows[key]) {
84
+ const parsed = flow.steps
85
+ .filter((s) => s.tool)
86
+ .map((s) => ({ tool: s.tool, params: s.params ?? {} }));
87
+ ref.flows[key] = {
88
+ steps: flow.steps.map((s) => s.description),
89
+ description: flow.name,
90
+ ...(parsed.length > 0 ? { _parsed: parsed } : {}),
91
+ };
92
+ added++;
93
+ }
94
+ }
95
+ const filePath = this.save(ref);
96
+ return { filePath, added };
97
+ }
98
+ /**
99
+ * Merge errors/solutions into reference.
100
+ */
101
+ mergeErrors(errors, bundleId, appName) {
102
+ const ref = this.loadOrCreate(bundleId, appName);
103
+ if (!ref.errors)
104
+ ref.errors = [];
105
+ let added = 0;
106
+ const existingErrors = new Set(ref.errors.map((e) => e.error.toLowerCase()));
107
+ for (const err of errors) {
108
+ if (!existingErrors.has(err.error.toLowerCase())) {
109
+ ref.errors.push(err);
110
+ existingErrors.add(err.error.toLowerCase());
111
+ added++;
112
+ }
113
+ }
114
+ const filePath = this.save(ref);
115
+ return { filePath, added };
116
+ }
117
+ /**
118
+ * Load existing reference file for a bundleId, or create a new one.
119
+ */
120
+ loadOrCreate(bundleId, appName) {
121
+ // Search for existing file by bundleId
122
+ try {
123
+ const files = fs.readdirSync(this.referencesDir);
124
+ for (const file of files) {
125
+ if (!file.endsWith(".json"))
126
+ continue;
127
+ try {
128
+ const raw = fs.readFileSync(path.join(this.referencesDir, file), "utf-8");
129
+ const ref = JSON.parse(raw);
130
+ if (ref.bundleId === bundleId)
131
+ return ref;
132
+ }
133
+ catch {
134
+ /* skip malformed */
135
+ }
136
+ }
137
+ }
138
+ catch {
139
+ /* dir doesn't exist */
140
+ }
141
+ // Create new reference
142
+ const platform = appName.toLowerCase().replace(/\s+/g, "-");
143
+ return {
144
+ id: platform,
145
+ name: `${appName} — Auto-Generated Reference`,
146
+ platform,
147
+ bundleId,
148
+ shortcuts: {},
149
+ selectors: {},
150
+ flows: {},
151
+ errors: [],
152
+ };
153
+ }
154
+ save(ref) {
155
+ fs.mkdirSync(this.referencesDir, { recursive: true });
156
+ const filePath = path.join(this.referencesDir, `${ref.id}.json`);
157
+ writeFileAtomicSync(filePath, JSON.stringify(ref, null, 2) + "\n");
158
+ return filePath;
159
+ }
160
+ menuScanToShortcuts(scan) {
161
+ const shortcuts = {};
162
+ for (const topMenu of scan.menuTree) {
163
+ const category = topMenu.title;
164
+ if (!shortcuts[category])
165
+ shortcuts[category] = {};
166
+ const items = this.flattenMenuNode(topMenu, []);
167
+ for (const item of items) {
168
+ if (item.shortcut) {
169
+ shortcuts[category][item.label] = item.shortcut;
170
+ }
171
+ }
172
+ }
173
+ return { shortcuts };
174
+ }
175
+ flattenMenuNode(node, parentPath) {
176
+ const items = [];
177
+ const path = [...parentPath, node.title];
178
+ if (node.shortcut) {
179
+ items.push({ label: path.slice(1).join(" > "), shortcut: node.shortcut });
180
+ }
181
+ for (const child of node.children ?? []) {
182
+ items.push(...this.flattenMenuNode(child, path));
183
+ }
184
+ return items;
185
+ }
186
+ }
@@ -0,0 +1,180 @@
1
+ // Copyright (C) 2025 Clazro Technology Private Limited
2
+ // SPDX-License-Identifier: AGPL-3.0-only
3
+ /**
4
+ * ShortcutExtractor — parses keyboard shortcut lists from various formats
5
+ * (HTML tables, plain text lists, markdown) into structured data.
6
+ */
7
+ /**
8
+ * Parse shortcuts from an HTML table.
9
+ * Expects columns like: Action/Name | Shortcut/Keys
10
+ */
11
+ export function parseShortcutsFromHTML(html) {
12
+ const shortcuts = [];
13
+ // Extract table rows
14
+ const tableRegex = /<tr[^>]*>([\s\S]*?)<\/tr>/gi;
15
+ const cellRegex = /<t[dh][^>]*>([\s\S]*?)<\/t[dh]>/gi;
16
+ let tableMatch;
17
+ while ((tableMatch = tableRegex.exec(html)) !== null) {
18
+ const row = tableMatch[1];
19
+ const cells = [];
20
+ let cellMatch;
21
+ while ((cellMatch = cellRegex.exec(row)) !== null) {
22
+ // Strip HTML tags and normalize whitespace
23
+ const text = cellMatch[1]
24
+ .replace(/<[^>]+>/g, "")
25
+ .replace(/&amp;/g, "&")
26
+ .replace(/&lt;/g, "<")
27
+ .replace(/&gt;/g, ">")
28
+ .replace(/&nbsp;/g, " ")
29
+ .replace(/&#\d+;/g, "")
30
+ .trim();
31
+ cells.push(text);
32
+ }
33
+ if (cells.length >= 2 && cells[0] && cells[1]) {
34
+ // Skip header rows
35
+ const lower0 = cells[0].toLowerCase();
36
+ if (lower0 === "action" ||
37
+ lower0 === "command" ||
38
+ lower0 === "shortcut" ||
39
+ lower0 === "name" ||
40
+ lower0 === "function") {
41
+ continue;
42
+ }
43
+ shortcuts.push({
44
+ name: cells[0],
45
+ keys: normalizeKeys(cells[1]),
46
+ context: cells.length > 2 ? cells[2] : undefined,
47
+ });
48
+ }
49
+ }
50
+ return shortcuts;
51
+ }
52
+ /**
53
+ * Parse shortcuts from plain text lines.
54
+ * Supports formats:
55
+ * - "Action: Cmd+K"
56
+ * - "Action — Cmd+K"
57
+ * - "Action\tCmd+K"
58
+ * - "Cmd+K Action"
59
+ */
60
+ export function parseShortcutsFromText(text) {
61
+ const shortcuts = [];
62
+ const lines = text.split("\n");
63
+ let currentCategory;
64
+ for (const rawLine of lines) {
65
+ const line = rawLine.trim();
66
+ if (!line)
67
+ continue;
68
+ // Detect category headers (all caps or ending with colon, no shortcut key)
69
+ if ((line.endsWith(":") && !SHORTCUT_PATTERN.test(line)) ||
70
+ (line === line.toUpperCase() && line.length > 3 && !SHORTCUT_PATTERN.test(line))) {
71
+ currentCategory = line.replace(/:$/, "").trim();
72
+ continue;
73
+ }
74
+ // Try "Name: Keys" or "Name — Keys" or "Name\tKeys"
75
+ const separatorMatch = line.match(/^(.+?)(?:\s*[:—–\-|]\s*|\t)(.+)$/);
76
+ if (separatorMatch && SHORTCUT_PATTERN.test(separatorMatch[2])) {
77
+ shortcuts.push({
78
+ name: separatorMatch[1].trim(),
79
+ keys: normalizeKeys(separatorMatch[2].trim()),
80
+ category: currentCategory,
81
+ });
82
+ continue;
83
+ }
84
+ // Try "Keys Name" (shortcut first)
85
+ const keysFirstMatch = line.match(/^((?:Cmd|Ctrl|Alt|Option|Shift|Meta|⌘|⌃|⌥|⇧)[+\s].+?)\s{2,}(.+)$/i);
86
+ if (keysFirstMatch) {
87
+ shortcuts.push({
88
+ name: keysFirstMatch[2].trim(),
89
+ keys: normalizeKeys(keysFirstMatch[1].trim()),
90
+ category: currentCategory,
91
+ });
92
+ }
93
+ }
94
+ return shortcuts;
95
+ }
96
+ /**
97
+ * Parse shortcuts from markdown format.
98
+ */
99
+ export function parseShortcutsFromMarkdown(md) {
100
+ const shortcuts = [];
101
+ const lines = md.split("\n");
102
+ let currentCategory;
103
+ for (const rawLine of lines) {
104
+ const line = rawLine.trim();
105
+ // Category from heading
106
+ const headingMatch = line.match(/^#{1,3}\s+(.+)$/);
107
+ if (headingMatch) {
108
+ currentCategory = headingMatch[1].trim();
109
+ continue;
110
+ }
111
+ // Table row: | Name | Keys |
112
+ const tableMatch = line.match(/^\|(.+)\|(.+)\|/);
113
+ if (tableMatch) {
114
+ const name = tableMatch[1].trim();
115
+ const keys = tableMatch[2].trim();
116
+ if (name &&
117
+ keys &&
118
+ name !== "---" &&
119
+ !name.startsWith("-") &&
120
+ SHORTCUT_PATTERN.test(keys)) {
121
+ shortcuts.push({
122
+ name,
123
+ keys: normalizeKeys(keys),
124
+ category: currentCategory,
125
+ });
126
+ }
127
+ continue;
128
+ }
129
+ // List item: - Name: Keys or * Name — Keys
130
+ const listMatch = line.match(/^[*\-+]\s+(.+?)(?:\s*[:—–]\s*)(.+)$/);
131
+ if (listMatch && SHORTCUT_PATTERN.test(listMatch[2])) {
132
+ shortcuts.push({
133
+ name: listMatch[1].trim(),
134
+ keys: normalizeKeys(listMatch[2].trim()),
135
+ category: currentCategory,
136
+ });
137
+ continue;
138
+ }
139
+ // Inline code: `Cmd+K` — Description
140
+ const codeMatch = line.match(/`([^`]+)`\s*[-—:]\s*(.+)/);
141
+ if (codeMatch && SHORTCUT_PATTERN.test(codeMatch[1])) {
142
+ shortcuts.push({
143
+ name: codeMatch[2].trim(),
144
+ keys: normalizeKeys(codeMatch[1].trim()),
145
+ category: currentCategory,
146
+ });
147
+ }
148
+ }
149
+ return shortcuts;
150
+ }
151
+ /**
152
+ * Convert parsed shortcuts to reference JSON shortcuts format.
153
+ */
154
+ export function shortcutsToReferenceFormat(shortcuts) {
155
+ const result = {};
156
+ for (const sc of shortcuts) {
157
+ const category = sc.category ?? "general";
158
+ if (!result[category])
159
+ result[category] = {};
160
+ result[category][sc.name] = sc.keys;
161
+ }
162
+ return result;
163
+ }
164
+ // Pattern to detect shortcut-like strings
165
+ const SHORTCUT_PATTERN = /(?:Cmd|Ctrl|Alt|Option|Shift|Meta|⌘|⌃|⌥|⇧|Command|Control)[+\s]/i;
166
+ /**
167
+ * Normalize keyboard shortcut notation.
168
+ * Converts symbols to names: ⌘→Cmd, ⌃→Ctrl, ⌥→Option, ⇧→Shift
169
+ */
170
+ function normalizeKeys(keys) {
171
+ return keys
172
+ .replace(/⌘/g, "Cmd")
173
+ .replace(/⌃/g, "Ctrl")
174
+ .replace(/⌥/g, "Option")
175
+ .replace(/⇧/g, "Shift")
176
+ .replace(/Command/gi, "Cmd")
177
+ .replace(/Control/gi, "Ctrl")
178
+ .replace(/\s*\+\s*/g, "+")
179
+ .trim();
180
+ }
@@ -0,0 +1,170 @@
1
+ // Copyright (C) 2025 Clazro Technology Private Limited
2
+ // SPDX-License-Identifier: AGPL-3.0-only
3
+ // Patterns that indicate an action step in a tutorial
4
+ const ACTION_PATTERNS = [
5
+ /\b(?:click|tap|press|hit)\s+(?:on\s+)?(?:the\s+)?(.+?)(?:\s+button|\s+tab|\s+menu|\s+icon)?(?:\.|,|$)/i,
6
+ /\b(?:go to|navigate to|open|switch to)\s+(.+?)(?:\.|,|$)/i,
7
+ /\b(?:select|choose|pick)\s+(.+?)(?:\.|,|$)/i,
8
+ /\b(?:type|enter|input|write)\s+(.+?)(?:\.|,|$)/i,
9
+ /\b(?:drag|move)\s+(.+?)\s+to\s+(.+?)(?:\.|,|$)/i,
10
+ /\b(?:right[- ]?click|double[- ]?click)\s+(?:on\s+)?(.+?)(?:\.|,|$)/i,
11
+ /\bpress\s+((?:cmd|ctrl|alt|shift|command|control|option)[+\s].+?)(?:\.|,|$)/i,
12
+ /\b(?:scroll|zoom)\s+(.+?)(?:\.|,|$)/i,
13
+ /\b(?:set|change|adjust)\s+(.+?)\s+to\s+(.+?)(?:\.|,|$)/i,
14
+ ];
15
+ // Words that indicate non-action segments (skip these)
16
+ const SKIP_PATTERNS = [
17
+ /\bhey guys\b/i,
18
+ /\bsubscribe\b/i,
19
+ /\blike (?:this|the) video\b/i,
20
+ /\bcomment below\b/i,
21
+ /\bsponsor/i,
22
+ /\bwhat's up\b/i,
23
+ /\bhello everyone\b/i,
24
+ /\bwelcome (?:back|to)\b/i,
25
+ /\blet me know\b/i,
26
+ /\bcheck out\b/i,
27
+ /\blink in (?:the )?description\b/i,
28
+ ];
29
+ /**
30
+ * TutorialExtractor — extracts structured playbook steps from video
31
+ * transcripts (typically YouTube captions/subtitles).
32
+ */
33
+ export class TutorialExtractor {
34
+ /**
35
+ * Extract action steps from a transcript.
36
+ */
37
+ extract(segments, title, platform) {
38
+ const steps = [];
39
+ for (const segment of segments) {
40
+ // Skip filler/promo segments
41
+ if (SKIP_PATTERNS.some((p) => p.test(segment.text)))
42
+ continue;
43
+ const parsedSteps = this.parseSegment(segment.text);
44
+ for (const step of parsedSteps) {
45
+ // Deduplicate consecutive identical steps
46
+ const last = steps[steps.length - 1];
47
+ if (last && last.description === step.description)
48
+ continue;
49
+ steps.push(step);
50
+ }
51
+ }
52
+ return {
53
+ title,
54
+ platform,
55
+ steps,
56
+ rawSegments: segments.length,
57
+ actionSegments: steps.length,
58
+ extractedAt: new Date().toISOString(),
59
+ };
60
+ }
61
+ /**
62
+ * Convert extracted steps to a playbook-ready format.
63
+ */
64
+ toPlaybookSteps(result) {
65
+ return result.steps
66
+ .filter((s) => s.tool)
67
+ .map((step) => ({
68
+ action: step.tool === "key" ? "press" : step.tool === "type_text" ? "type" : "click",
69
+ tool: step.tool,
70
+ params: step.params ?? {},
71
+ description: step.description,
72
+ postcondition: step.postcondition,
73
+ }));
74
+ }
75
+ /**
76
+ * Parse a single transcript segment for action steps.
77
+ */
78
+ parseSegment(text) {
79
+ const steps = [];
80
+ // Split on sentence boundaries
81
+ const sentences = text.split(/(?<=[.!?])\s+|(?:,\s+(?:then|and then|next|after that)\s+)/i);
82
+ for (const sentence of sentences) {
83
+ const trimmed = sentence.trim();
84
+ if (trimmed.length < 5)
85
+ continue;
86
+ for (const pattern of ACTION_PATTERNS) {
87
+ const match = trimmed.match(pattern);
88
+ if (match) {
89
+ const step = this.mapToStep(trimmed, match);
90
+ if (step) {
91
+ steps.push(step);
92
+ break; // Only take first matching pattern per sentence
93
+ }
94
+ }
95
+ }
96
+ }
97
+ return steps;
98
+ }
99
+ /**
100
+ * Map a matched action to a ParsedFlowStep with tool and params.
101
+ */
102
+ mapToStep(fullText, match) {
103
+ const lower = fullText.toLowerCase();
104
+ const target = match[1]?.trim();
105
+ if (!target || target.length < 2)
106
+ return null;
107
+ // Determine tool and params
108
+ if (lower.includes("type") || lower.includes("enter") || lower.includes("input")) {
109
+ return {
110
+ description: fullText,
111
+ tool: "type_text",
112
+ params: { text: target },
113
+ };
114
+ }
115
+ if (lower.includes("press") && /(?:cmd|ctrl|alt|shift|command|control|option)/i.test(target)) {
116
+ return {
117
+ description: fullText,
118
+ tool: "key",
119
+ params: { combo: this.normalizeShortcut(target) },
120
+ };
121
+ }
122
+ if (lower.includes("drag") || lower.includes("move")) {
123
+ return {
124
+ description: fullText,
125
+ tool: "click_text",
126
+ params: { text: target },
127
+ };
128
+ }
129
+ if (lower.includes("scroll") || lower.includes("zoom")) {
130
+ return {
131
+ description: fullText,
132
+ tool: "scroll",
133
+ params: { direction: lower.includes("down") ? "down" : lower.includes("up") ? "up" : "down" },
134
+ };
135
+ }
136
+ if (lower.includes("menu") || target.includes(">")) {
137
+ const pathParts = target
138
+ .split(/\s*>\s*/)
139
+ .map((s) => s.trim())
140
+ .filter(Boolean);
141
+ if (pathParts.length >= 2) {
142
+ return {
143
+ description: fullText,
144
+ tool: "menu_click",
145
+ params: { menuPath: pathParts.join("/") },
146
+ };
147
+ }
148
+ }
149
+ if (lower.includes("set") || lower.includes("change") || lower.includes("adjust")) {
150
+ return {
151
+ description: fullText,
152
+ tool: "click_text",
153
+ params: { text: target },
154
+ };
155
+ }
156
+ // Default: click on the target
157
+ return {
158
+ description: fullText,
159
+ tool: "click_text",
160
+ params: { text: target },
161
+ };
162
+ }
163
+ normalizeShortcut(keys) {
164
+ return keys
165
+ .replace(/Command/gi, "Cmd")
166
+ .replace(/Control/gi, "Ctrl")
167
+ .replace(/\s+/g, "+")
168
+ .trim();
169
+ }
170
+ }
@@ -0,0 +1,3 @@
1
+ // Copyright (C) 2025 Clazro Technology Private Limited
2
+ // SPDX-License-Identifier: AGPL-3.0-only
3
+ export {};