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