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.
- package/README.md +193 -109
- package/bin/darwin-arm64/macos-bridge +0 -0
- package/dist/mcp-desktop.js +5876 -0
- package/dist/scripts/codex-monitor-daemon.js +335 -0
- package/dist/scripts/export-help-center.js +112 -0
- package/dist/scripts/marketing-loop.js +117 -0
- package/dist/scripts/observer-daemon.js +288 -0
- package/dist/scripts/orchestrator-daemon.js +399 -0
- package/dist/scripts/supervisor-daemon.js +272 -0
- package/dist/scripts/threads-campaign.js +208 -0
- package/dist/scripts/worker-daemon.js +228 -0
- package/dist/src/agent/cli.js +82 -0
- package/dist/src/agent/loop.js +274 -0
- package/dist/src/community/fetcher.js +109 -0
- package/dist/src/community/index.js +6 -0
- package/dist/src/community/publisher.js +191 -0
- package/dist/src/community/remote-api.js +121 -0
- package/dist/src/community/types.js +3 -0
- package/dist/src/community/validator.js +95 -0
- package/{src/config.ts → dist/src/config.js} +5 -10
- package/dist/src/context-tracker.js +489 -0
- package/{src/index.ts → dist/src/index.js} +32 -52
- package/dist/src/ingestion/coverage-auditor.js +233 -0
- package/dist/src/ingestion/doc-parser.js +164 -0
- package/dist/src/ingestion/index.js +8 -0
- package/dist/src/ingestion/menu-scanner.js +152 -0
- package/dist/src/ingestion/reference-merger.js +186 -0
- package/dist/src/ingestion/shortcut-extractor.js +180 -0
- package/dist/src/ingestion/tutorial-extractor.js +170 -0
- package/dist/src/ingestion/types.js +3 -0
- package/dist/src/jobs/manager.js +305 -0
- package/dist/src/jobs/runner.js +806 -0
- package/dist/src/jobs/store.js +102 -0
- package/dist/src/jobs/types.js +30 -0
- package/dist/src/jobs/worker.js +97 -0
- package/dist/src/learning/engine.js +356 -0
- package/dist/src/learning/index.js +9 -0
- package/dist/src/learning/locator-policy.js +120 -0
- package/dist/src/learning/pattern-policy.js +89 -0
- package/dist/src/learning/recovery-policy.js +116 -0
- package/dist/src/learning/sensor-policy.js +115 -0
- package/dist/src/learning/timing-model.js +204 -0
- package/dist/src/learning/topology-policy.js +90 -0
- package/dist/src/learning/types.js +9 -0
- package/dist/src/logging/timeline-logger.js +48 -0
- package/dist/src/mcp/mcp-stdio-server.js +464 -0
- package/dist/src/mcp/server.js +363 -0
- package/dist/src/mcp-entry.js +60 -0
- package/dist/src/memory/playbook-seeds.js +200 -0
- package/dist/src/memory/recall.js +222 -0
- package/dist/src/memory/research.js +104 -0
- package/dist/src/memory/seeds.js +101 -0
- package/dist/src/memory/service.js +446 -0
- package/dist/src/memory/session.js +169 -0
- package/dist/src/memory/store.js +451 -0
- package/{src/runtime/locator-cache.ts → dist/src/memory/types.js} +1 -17
- package/dist/src/monitor/codex-monitor.js +382 -0
- package/dist/src/monitor/task-queue.js +97 -0
- package/dist/src/monitor/types.js +62 -0
- package/dist/src/native/bridge-client.js +412 -0
- package/{src/native/macos-bridge-client.ts → dist/src/native/macos-bridge-client.js} +0 -1
- package/dist/src/observer/state.js +199 -0
- package/dist/src/observer/types.js +43 -0
- package/dist/src/orchestrator/state.js +68 -0
- package/dist/src/orchestrator/types.js +22 -0
- package/dist/src/perception/ax-source.js +162 -0
- package/dist/src/perception/cdp-source.js +162 -0
- package/dist/src/perception/coordinator.js +771 -0
- package/dist/src/perception/frame-differ.js +287 -0
- package/dist/src/perception/index.js +22 -0
- package/dist/src/perception/manager.js +199 -0
- package/dist/src/perception/types.js +47 -0
- package/dist/src/perception/vision-source.js +399 -0
- package/dist/src/planner/deterministic.js +298 -0
- package/dist/src/planner/executor.js +870 -0
- package/dist/src/planner/goal-store.js +92 -0
- package/dist/src/planner/index.js +21 -0
- package/dist/src/planner/planner.js +520 -0
- package/dist/src/planner/tool-registry.js +71 -0
- package/dist/src/planner/types.js +22 -0
- package/dist/src/platform/explorer.js +213 -0
- package/dist/src/platform/help-center-markdown.js +527 -0
- package/dist/src/platform/learner.js +257 -0
- package/dist/src/playbook/engine.js +486 -0
- package/dist/src/playbook/index.js +20 -0
- package/dist/src/playbook/mcp-recorder.js +204 -0
- package/dist/src/playbook/recorder.js +536 -0
- package/dist/src/playbook/runner.js +408 -0
- package/dist/src/playbook/store.js +312 -0
- package/dist/src/playbook/types.js +17 -0
- package/dist/src/recovery/detectors.js +156 -0
- package/dist/src/recovery/engine.js +327 -0
- package/dist/src/recovery/index.js +20 -0
- package/dist/src/recovery/strategies.js +274 -0
- package/dist/src/recovery/types.js +20 -0
- package/dist/src/runtime/accessibility-adapter.js +430 -0
- package/dist/src/runtime/app-adapter.js +64 -0
- package/dist/src/runtime/applescript-adapter.js +305 -0
- package/dist/src/runtime/ax-role-map.js +96 -0
- package/dist/src/runtime/browser-adapter.js +52 -0
- package/dist/src/runtime/cdp-chrome-adapter.js +521 -0
- package/dist/src/runtime/composite-adapter.js +221 -0
- package/dist/src/runtime/execution-contract.js +159 -0
- package/dist/src/runtime/executor.js +286 -0
- package/dist/src/runtime/locator-cache.js +50 -0
- package/dist/src/runtime/planning-loop.js +63 -0
- package/dist/src/runtime/service.js +432 -0
- package/dist/src/runtime/session-manager.js +63 -0
- package/dist/src/runtime/state-observer.js +121 -0
- package/dist/src/runtime/vision-adapter.js +225 -0
- package/dist/src/state/app-map-types.js +72 -0
- package/dist/src/state/app-map.js +1974 -0
- package/dist/src/state/entity-tracker.js +108 -0
- package/dist/src/state/fusion.js +96 -0
- package/dist/src/state/index.js +21 -0
- package/dist/src/state/ladder-generator.js +236 -0
- package/dist/src/state/persistence.js +156 -0
- package/dist/src/state/types.js +17 -0
- package/dist/src/state/world-model.js +1456 -0
- package/dist/src/supervisor/locks.js +186 -0
- package/dist/src/supervisor/supervisor.js +403 -0
- package/dist/src/supervisor/types.js +30 -0
- package/dist/src/test-mcp-protocol.js +154 -0
- package/dist/src/types.js +17 -0
- package/dist/src/util/atomic-write.js +133 -0
- package/dist/src/util/sanitize.js +146 -0
- package/dist-app-maps/com.figma.Desktop.json +959 -0
- package/dist-app-maps/com.hnc.Discord.json +1146 -0
- package/dist-app-maps/notion.id.json +2831 -0
- package/dist-playbooks/canva-screenhand-carousel.json +445 -0
- package/dist-playbooks/codex-desktop.json +76 -0
- package/dist-playbooks/competitor-research-stack.json +122 -0
- package/dist-playbooks/davinci-color-grade.json +153 -0
- package/dist-playbooks/davinci-edit-timeline.json +162 -0
- package/dist-playbooks/davinci-render.json +114 -0
- package/dist-playbooks/devto.json +52 -0
- package/dist-playbooks/discord.json +41 -0
- package/dist-playbooks/google-flow-create-project.json +59 -0
- package/dist-playbooks/google-flow-edit-image.json +90 -0
- package/dist-playbooks/google-flow-edit-video.json +90 -0
- package/dist-playbooks/google-flow-generate-image.json +68 -0
- package/dist-playbooks/google-flow-generate-video.json +191 -0
- package/dist-playbooks/google-flow-open-project.json +48 -0
- package/dist-playbooks/google-flow-open-scenebuilder.json +64 -0
- package/dist-playbooks/google-flow-search-assets.json +64 -0
- package/dist-playbooks/instagram.json +57 -0
- package/dist-playbooks/linkedin.json +52 -0
- package/dist-playbooks/n8n.json +43 -0
- package/dist-playbooks/reddit.json +52 -0
- package/dist-playbooks/threads.json +59 -0
- package/dist-playbooks/x-twitter.json +59 -0
- package/dist-playbooks/youtube.json +59 -0
- package/dist-references/canva.json +646 -0
- package/dist-references/codex-desktop.json +305 -0
- package/dist-references/davinci-resolve-keyboard.json +594 -0
- package/dist-references/davinci-resolve-menu-map.json +1139 -0
- package/dist-references/davinci-resolve-menus-batch1.json +116 -0
- package/dist-references/davinci-resolve-menus-batch2.json +372 -0
- package/dist-references/davinci-resolve-menus-batch3.json +330 -0
- package/dist-references/davinci-resolve-menus-batch4.json +297 -0
- package/dist-references/davinci-resolve-shortcuts.json +333 -0
- package/dist-references/devto.json +317 -0
- package/dist-references/discord.json +549 -0
- package/dist-references/figma.json +1186 -0
- package/dist-references/finder.json +146 -0
- package/dist-references/google-ads-transparency.json +95 -0
- package/dist-references/google-flow.json +649 -0
- package/dist-references/instagram.json +341 -0
- package/dist-references/linkedin.json +324 -0
- package/dist-references/meta-ad-library.json +86 -0
- package/dist-references/n8n.json +387 -0
- package/dist-references/notes.json +27 -0
- package/dist-references/notion.json +163 -0
- package/dist-references/reddit.json +341 -0
- package/dist-references/threads.json +337 -0
- package/dist-references/x-twitter.json +403 -0
- package/dist-references/youtube.json +373 -0
- package/native/macos-bridge/Package.swift +1 -0
- package/native/macos-bridge/Sources/AccessibilityBridge.swift +257 -36
- package/native/macos-bridge/Sources/AppManagement.swift +212 -2
- package/native/macos-bridge/Sources/CoreGraphicsBridge.swift +348 -53
- package/native/macos-bridge/Sources/StreamCapture.swift +136 -0
- package/native/macos-bridge/Sources/VisionBridge.swift +165 -7
- package/native/macos-bridge/Sources/main.swift +169 -16
- package/native/windows-bridge/Program.cs +5 -0
- package/native/windows-bridge/ScreenCapture.cs +124 -0
- package/package.json +29 -4
- package/scripts/postinstall.cjs +127 -0
- package/.claude/commands/automate.md +0 -28
- package/.claude/commands/debug-ui.md +0 -19
- package/.claude/commands/screenshot.md +0 -15
- package/.github/FUNDING.yml +0 -1
- package/.github/ISSUE_TEMPLATE/bug_report.md +0 -27
- package/.github/ISSUE_TEMPLATE/feature_request.md +0 -20
- package/.mcp.json +0 -8
- package/DESKTOP_MCP_GUIDE.md +0 -92
- package/SECURITY.md +0 -44
- package/docs/architecture.md +0 -47
- package/install-skills.sh +0 -19
- package/mcp-bridge.ts +0 -271
- package/mcp-desktop.ts +0 -1221
- package/playbooks/instagram.json +0 -41
- package/playbooks/instagram_v2.json +0 -201
- package/playbooks/x_v1.json +0 -211
- package/scripts/devpost-live-loop.mjs +0 -421
- package/src/logging/timeline-logger.ts +0 -55
- package/src/mcp/server.ts +0 -449
- package/src/memory/recall.ts +0 -191
- package/src/memory/research.ts +0 -146
- package/src/memory/seeds.ts +0 -123
- package/src/memory/session.ts +0 -201
- package/src/memory/store.ts +0 -434
- package/src/memory/types.ts +0 -69
- package/src/native/bridge-client.ts +0 -239
- package/src/runtime/accessibility-adapter.ts +0 -487
- package/src/runtime/app-adapter.ts +0 -169
- package/src/runtime/applescript-adapter.ts +0 -376
- package/src/runtime/ax-role-map.ts +0 -102
- package/src/runtime/browser-adapter.ts +0 -129
- package/src/runtime/cdp-chrome-adapter.ts +0 -676
- package/src/runtime/composite-adapter.ts +0 -274
- package/src/runtime/executor.ts +0 -396
- package/src/runtime/planning-loop.ts +0 -81
- package/src/runtime/service.ts +0 -448
- package/src/runtime/session-manager.ts +0 -50
- package/src/runtime/state-observer.ts +0 -136
- package/src/runtime/vision-adapter.ts +0 -297
- package/src/types.ts +0 -297
- package/tests/bridge-client.test.ts +0 -176
- package/tests/browser-stealth.test.ts +0 -210
- package/tests/composite-adapter.test.ts +0 -64
- package/tests/mcp-server.test.ts +0 -151
- package/tests/memory-recall.test.ts +0 -339
- package/tests/memory-research.test.ts +0 -159
- package/tests/memory-seeds.test.ts +0 -120
- package/tests/memory-store.test.ts +0 -392
- package/tests/types.test.ts +0 -92
- package/tsconfig.check.json +0 -17
- package/tsconfig.json +0 -19
- package/vitest.config.ts +0 -8
- /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(/&/g, "&")
|
|
26
|
+
.replace(/</g, "<")
|
|
27
|
+
.replace(/>/g, ">")
|
|
28
|
+
.replace(/ /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
|
+
}
|