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,68 @@
|
|
|
1
|
+
// Copyright (C) 2025 Clazro Technology Private Limited
|
|
2
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
3
|
+
/**
|
|
4
|
+
* Orchestrator state helpers — read/write state file, PID management.
|
|
5
|
+
*/
|
|
6
|
+
import fs from "node:fs";
|
|
7
|
+
import crypto from "node:crypto";
|
|
8
|
+
import { readJsonWithRecovery, writeFileAtomicSync } from "../util/atomic-write.js";
|
|
9
|
+
import { ORCHESTRATOR_DIR, ORCHESTRATOR_STATE_FILE, ORCHESTRATOR_PID_FILE, } from "./types.js";
|
|
10
|
+
/** Read current orchestrator state from disk. */
|
|
11
|
+
export function readOrchestratorState() {
|
|
12
|
+
return readJsonWithRecovery(ORCHESTRATOR_STATE_FILE);
|
|
13
|
+
}
|
|
14
|
+
/** Write orchestrator state to disk (atomic). */
|
|
15
|
+
export function writeOrchestratorState(state) {
|
|
16
|
+
fs.mkdirSync(ORCHESTRATOR_DIR, { recursive: true });
|
|
17
|
+
writeFileAtomicSync(ORCHESTRATOR_STATE_FILE, JSON.stringify(state, null, 2));
|
|
18
|
+
}
|
|
19
|
+
/** Get PID of running orchestrator daemon, or null. */
|
|
20
|
+
export function getOrchestratorDaemonPid() {
|
|
21
|
+
try {
|
|
22
|
+
const pid = Number(fs.readFileSync(ORCHESTRATOR_PID_FILE, "utf-8").trim());
|
|
23
|
+
if (Number.isNaN(pid))
|
|
24
|
+
return null;
|
|
25
|
+
try {
|
|
26
|
+
process.kill(pid, 0);
|
|
27
|
+
return pid;
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
try {
|
|
31
|
+
fs.unlinkSync(ORCHESTRATOR_PID_FILE);
|
|
32
|
+
}
|
|
33
|
+
catch { /* ignore */ }
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/** Generate a unique task ID */
|
|
42
|
+
export function generateTaskId() {
|
|
43
|
+
return "task_" + Date.now().toString(36) + "_" + crypto.randomBytes(3).toString("hex");
|
|
44
|
+
}
|
|
45
|
+
/** Auto-detect task mode from playbook and parameters */
|
|
46
|
+
export function detectTaskMode(playbookId, bundleId) {
|
|
47
|
+
// If it has a bundleId, it's at least partially native
|
|
48
|
+
if (bundleId)
|
|
49
|
+
return "native";
|
|
50
|
+
// CDP-only playbooks are web
|
|
51
|
+
// For now, default to web if no bundleId specified
|
|
52
|
+
return "web";
|
|
53
|
+
}
|
|
54
|
+
/** Create a new task */
|
|
55
|
+
export function createTask(task, opts = {}) {
|
|
56
|
+
return {
|
|
57
|
+
id: generateTaskId(),
|
|
58
|
+
task,
|
|
59
|
+
mode: opts.mode ?? detectTaskMode(opts.playbookId, opts.bundleId),
|
|
60
|
+
...(opts.playbookId !== undefined ? { playbookId: opts.playbookId } : {}),
|
|
61
|
+
...(opts.bundleId !== undefined ? { bundleId: opts.bundleId } : {}),
|
|
62
|
+
...(opts.windowId !== undefined ? { windowId: opts.windowId } : {}),
|
|
63
|
+
...(opts.vars ? { vars: opts.vars } : {}),
|
|
64
|
+
priority: opts.priority ?? 10,
|
|
65
|
+
status: "queued",
|
|
66
|
+
createdAt: new Date().toISOString(),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Copyright (C) 2025 Clazro Technology Private Limited
|
|
2
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
3
|
+
/**
|
|
4
|
+
* Orchestrator types — multi-agent task routing and coordination.
|
|
5
|
+
*
|
|
6
|
+
* Architecture:
|
|
7
|
+
* Orchestrator daemon manages a pool of worker slots.
|
|
8
|
+
* Web tasks (CDP-only) run in parallel — no mouse/keyboard conflict.
|
|
9
|
+
* Native tasks (AX/keyboard) are serialized per-app via lease locks.
|
|
10
|
+
* Mixed tasks split into web + native phases automatically.
|
|
11
|
+
*/
|
|
12
|
+
import os from "node:os";
|
|
13
|
+
import path from "node:path";
|
|
14
|
+
export const DEFAULT_ORCHESTRATOR_CONFIG = {
|
|
15
|
+
webSlots: 4,
|
|
16
|
+
nativeSlots: 1,
|
|
17
|
+
pollMs: 1000,
|
|
18
|
+
};
|
|
19
|
+
export const ORCHESTRATOR_DIR = path.join(os.homedir(), ".screenhand", "orchestrator");
|
|
20
|
+
export const ORCHESTRATOR_STATE_FILE = path.join(ORCHESTRATOR_DIR, "state.json");
|
|
21
|
+
export const ORCHESTRATOR_PID_FILE = path.join(ORCHESTRATOR_DIR, "orchestrator.pid");
|
|
22
|
+
export const ORCHESTRATOR_LOG_FILE = path.join(ORCHESTRATOR_DIR, "orchestrator.log");
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
// Copyright (C) 2025 Clazro Technology Private Limited
|
|
2
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
3
|
+
//
|
|
4
|
+
// This file is part of ScreenHand.
|
|
5
|
+
//
|
|
6
|
+
// ScreenHand is free software: you can redistribute it and/or modify
|
|
7
|
+
// it under the terms of the GNU Affero General Public License as
|
|
8
|
+
// published by the Free Software Foundation, version 3.
|
|
9
|
+
//
|
|
10
|
+
// ScreenHand is distributed in the hope that it will be useful,
|
|
11
|
+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
12
|
+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
13
|
+
// GNU Affero General Public License for more details.
|
|
14
|
+
//
|
|
15
|
+
// You should have received a copy of the GNU Affero General Public License
|
|
16
|
+
// along with ScreenHand. If not, see <https://www.gnu.org/licenses/>.
|
|
17
|
+
/**
|
|
18
|
+
* AX perception source — wraps StateObserver for push events
|
|
19
|
+
* and provides periodic AX tree polling for structured snapshots.
|
|
20
|
+
*/
|
|
21
|
+
export class AXSource {
|
|
22
|
+
observer;
|
|
23
|
+
bridge;
|
|
24
|
+
/** Rolling average of recent poll latencies for adaptive backoff */
|
|
25
|
+
recentLatencies = [];
|
|
26
|
+
static LATENCY_WINDOW = 5;
|
|
27
|
+
constructor(observer, bridge) {
|
|
28
|
+
this.observer = observer;
|
|
29
|
+
this.bridge = bridge;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* FAST rate: drain buffered AX events from StateObserver.
|
|
33
|
+
* Returns a PerceptionEvent if there are events, null otherwise.
|
|
34
|
+
*/
|
|
35
|
+
drainEvents() {
|
|
36
|
+
const events = this.observer.drainEvents();
|
|
37
|
+
if (events.length === 0)
|
|
38
|
+
return null;
|
|
39
|
+
return {
|
|
40
|
+
source: "ax_events",
|
|
41
|
+
rate: "fast",
|
|
42
|
+
timestamp: new Date().toISOString(),
|
|
43
|
+
data: {
|
|
44
|
+
type: "ax_events",
|
|
45
|
+
events,
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/** Known browser bundle IDs — use lower AX tree depth to avoid blowing up on heavy web DOMs */
|
|
50
|
+
static BROWSER_BUNDLE_IDS = new Set([
|
|
51
|
+
"com.apple.Safari",
|
|
52
|
+
"com.google.Chrome",
|
|
53
|
+
"com.brave.Browser",
|
|
54
|
+
"com.microsoft.edgemac",
|
|
55
|
+
"org.mozilla.firefox",
|
|
56
|
+
"com.operasoftware.Opera",
|
|
57
|
+
"company.thebrowser.Browser", // Arc
|
|
58
|
+
]);
|
|
59
|
+
/**
|
|
60
|
+
* MEDIUM rate: poll the full AX tree for the active window.
|
|
61
|
+
* More expensive but gives a complete structural snapshot.
|
|
62
|
+
* Uses lower depth for browsers to prevent crash on heavy web apps (Canva, Figma).
|
|
63
|
+
*/
|
|
64
|
+
/**
|
|
65
|
+
* Get the adaptive max depth based on recent poll performance.
|
|
66
|
+
* If recent polls are slow or returning huge trees, reduce depth automatically.
|
|
67
|
+
*/
|
|
68
|
+
getAdaptiveMaxDepth(bundleId) {
|
|
69
|
+
const isBrowser = AXSource.BROWSER_BUNDLE_IDS.has(bundleId);
|
|
70
|
+
const baseDepth = isBrowser ? 5 : 10;
|
|
71
|
+
if (this.recentLatencies.length < 2)
|
|
72
|
+
return baseDepth;
|
|
73
|
+
const avgLatency = this.recentLatencies.reduce((a, b) => a + b, 0) / this.recentLatencies.length;
|
|
74
|
+
// If average latency > 3s, reduce depth aggressively
|
|
75
|
+
if (avgLatency > 3000)
|
|
76
|
+
return Math.max(2, baseDepth - 3);
|
|
77
|
+
// If average latency > 1.5s, reduce depth by 1
|
|
78
|
+
if (avgLatency > 1500)
|
|
79
|
+
return Math.max(3, baseDepth - 1);
|
|
80
|
+
return baseDepth;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Check if AX polling should be skipped this cycle due to recent instability.
|
|
84
|
+
* Returns true if the last few polls were extremely slow (>5s average).
|
|
85
|
+
*/
|
|
86
|
+
shouldSkipPoll() {
|
|
87
|
+
if (this.recentLatencies.length < 3)
|
|
88
|
+
return false;
|
|
89
|
+
const avgLatency = this.recentLatencies.reduce((a, b) => a + b, 0) / this.recentLatencies.length;
|
|
90
|
+
return avgLatency > 5000;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Get average recent poll latency (ms). Returns 0 if no data.
|
|
94
|
+
*/
|
|
95
|
+
getAverageLatency() {
|
|
96
|
+
if (this.recentLatencies.length === 0)
|
|
97
|
+
return 0;
|
|
98
|
+
return this.recentLatencies.reduce((a, b) => a + b, 0) / this.recentLatencies.length;
|
|
99
|
+
}
|
|
100
|
+
async pollAXTree(pid, windowId, appContext, maxDepth) {
|
|
101
|
+
// Use adaptive depth if not explicitly provided
|
|
102
|
+
if (maxDepth === undefined) {
|
|
103
|
+
maxDepth = this.getAdaptiveMaxDepth(appContext.bundleId);
|
|
104
|
+
}
|
|
105
|
+
const start = Date.now();
|
|
106
|
+
try {
|
|
107
|
+
const tree = await this.bridge.call("ax.getElementTree", {
|
|
108
|
+
pid,
|
|
109
|
+
maxDepth,
|
|
110
|
+
windowId: windowId > 0 ? windowId : undefined,
|
|
111
|
+
});
|
|
112
|
+
const latencyMs = Date.now() - start;
|
|
113
|
+
const nodeCount = tree?._nodeCount ?? 0;
|
|
114
|
+
// Track latency for adaptive backoff
|
|
115
|
+
this.recentLatencies.push(latencyMs);
|
|
116
|
+
if (this.recentLatencies.length > AXSource.LATENCY_WINDOW) {
|
|
117
|
+
this.recentLatencies.shift();
|
|
118
|
+
}
|
|
119
|
+
if (!tree)
|
|
120
|
+
return { event: null, latencyMs, nodeCount: 0 };
|
|
121
|
+
return {
|
|
122
|
+
event: {
|
|
123
|
+
source: "ax_tree",
|
|
124
|
+
rate: "medium",
|
|
125
|
+
timestamp: new Date().toISOString(),
|
|
126
|
+
data: {
|
|
127
|
+
type: "ax_tree",
|
|
128
|
+
windowId,
|
|
129
|
+
tree,
|
|
130
|
+
appContext,
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
latencyMs,
|
|
134
|
+
nodeCount,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
const latencyMs = Date.now() - start;
|
|
139
|
+
// Track failures as high-latency for backoff purposes
|
|
140
|
+
this.recentLatencies.push(latencyMs > 1000 ? latencyMs : 5000);
|
|
141
|
+
if (this.recentLatencies.length > AXSource.LATENCY_WINDOW) {
|
|
142
|
+
this.recentLatencies.shift();
|
|
143
|
+
}
|
|
144
|
+
return { event: null, latencyMs, nodeCount: 0 };
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Start observing a process for AX events.
|
|
149
|
+
*/
|
|
150
|
+
async startObserving(pid) {
|
|
151
|
+
await this.observer.startObserving(pid);
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Stop observing a process.
|
|
155
|
+
*/
|
|
156
|
+
async stopObserving(pid) {
|
|
157
|
+
await this.observer.stopObserving(pid);
|
|
158
|
+
}
|
|
159
|
+
get isObserving() {
|
|
160
|
+
return this.observer.isObserving;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
// Copyright (C) 2025 Clazro Technology Private Limited
|
|
2
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
3
|
+
//
|
|
4
|
+
// This file is part of ScreenHand.
|
|
5
|
+
//
|
|
6
|
+
// ScreenHand is free software: you can redistribute it and/or modify
|
|
7
|
+
// it under the terms of the GNU Affero General Public License as
|
|
8
|
+
// published by the Free Software Foundation, version 3.
|
|
9
|
+
//
|
|
10
|
+
// ScreenHand is distributed in the hope that it will be useful,
|
|
11
|
+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
12
|
+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
13
|
+
// GNU Affero General Public License for more details.
|
|
14
|
+
//
|
|
15
|
+
// You should have received a copy of the GNU Affero General Public License
|
|
16
|
+
// along with ScreenHand. If not, see <https://www.gnu.org/licenses/>.
|
|
17
|
+
/**
|
|
18
|
+
* CDP perception source — tracks DOM mutations and provides snapshots
|
|
19
|
+
* via Chrome DevTools Protocol.
|
|
20
|
+
*
|
|
21
|
+
* The CDP client is injected as `any` since it comes from the `chrome-remote-interface`
|
|
22
|
+
* package which is dynamically imported in mcp-desktop.ts.
|
|
23
|
+
*/
|
|
24
|
+
export class CDPSource {
|
|
25
|
+
mutationBuffer = [];
|
|
26
|
+
observerInstalled = false;
|
|
27
|
+
maxBufferSize = 100;
|
|
28
|
+
/**
|
|
29
|
+
* Install a DOM mutation observer via CDP Runtime.evaluate.
|
|
30
|
+
* This injects a MutationObserver into the page that sends mutations
|
|
31
|
+
* back via CDP's Runtime.consoleAPICalled.
|
|
32
|
+
*/
|
|
33
|
+
async installMutationObserver(cdpClient) {
|
|
34
|
+
if (this.observerInstalled)
|
|
35
|
+
return;
|
|
36
|
+
try {
|
|
37
|
+
// Enable DOM and Runtime domains
|
|
38
|
+
await cdpClient.DOM.enable();
|
|
39
|
+
await cdpClient.Runtime.enable();
|
|
40
|
+
// Inject MutationObserver that logs changes as JSON
|
|
41
|
+
await cdpClient.Runtime.evaluate({
|
|
42
|
+
expression: `
|
|
43
|
+
(() => {
|
|
44
|
+
if (window.__shMutationObserver) return;
|
|
45
|
+
const observer = new MutationObserver((mutations) => {
|
|
46
|
+
const summary = mutations.slice(0, 20).map(m => ({
|
|
47
|
+
type: m.type,
|
|
48
|
+
target: m.target.nodeName + (m.target.id ? '#' + m.target.id : ''),
|
|
49
|
+
attribute: m.attributeName || undefined,
|
|
50
|
+
addedNodes: m.addedNodes.length,
|
|
51
|
+
removedNodes: m.removedNodes.length,
|
|
52
|
+
}));
|
|
53
|
+
console.debug('__sh_mutations__' + JSON.stringify(summary));
|
|
54
|
+
});
|
|
55
|
+
observer.observe(document.body, {
|
|
56
|
+
childList: true, subtree: true,
|
|
57
|
+
attributes: true, attributeOldValue: true,
|
|
58
|
+
});
|
|
59
|
+
window.__shMutationObserver = observer;
|
|
60
|
+
})();
|
|
61
|
+
`,
|
|
62
|
+
returnByValue: true,
|
|
63
|
+
});
|
|
64
|
+
// Listen for console messages that contain mutation data
|
|
65
|
+
cdpClient.Runtime.consoleAPICalled((params) => {
|
|
66
|
+
if (params.type === "debug" && params.args?.length > 0) {
|
|
67
|
+
const msg = params.args[0]?.value;
|
|
68
|
+
if (typeof msg === "string") {
|
|
69
|
+
this.processCDPConsoleMessage(msg);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
this.observerInstalled = true;
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
// CDP not available or page not ready
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Process a CDP console message that might contain mutation data.
|
|
81
|
+
* Call this from a Runtime.consoleAPICalled handler.
|
|
82
|
+
*/
|
|
83
|
+
processCDPConsoleMessage(message) {
|
|
84
|
+
if (!message.startsWith("__sh_mutations__"))
|
|
85
|
+
return;
|
|
86
|
+
try {
|
|
87
|
+
const raw = JSON.parse(message.slice("__sh_mutations__".length));
|
|
88
|
+
for (const m of raw) {
|
|
89
|
+
const entry = {
|
|
90
|
+
selector: m.target,
|
|
91
|
+
};
|
|
92
|
+
if (m.attribute)
|
|
93
|
+
entry.attribute = m.attribute;
|
|
94
|
+
if (m.addedNodes)
|
|
95
|
+
entry.addedNodes = m.addedNodes;
|
|
96
|
+
if (m.removedNodes)
|
|
97
|
+
entry.removedNodes = m.removedNodes;
|
|
98
|
+
this.mutationBuffer.push(entry);
|
|
99
|
+
}
|
|
100
|
+
// Cap buffer
|
|
101
|
+
if (this.mutationBuffer.length > this.maxBufferSize) {
|
|
102
|
+
this.mutationBuffer = this.mutationBuffer.slice(-this.maxBufferSize);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
// Malformed message
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* FAST rate: drain buffered DOM mutations.
|
|
111
|
+
*/
|
|
112
|
+
drainMutations() {
|
|
113
|
+
if (this.mutationBuffer.length === 0)
|
|
114
|
+
return null;
|
|
115
|
+
const mutations = [...this.mutationBuffer];
|
|
116
|
+
this.mutationBuffer = [];
|
|
117
|
+
return {
|
|
118
|
+
source: "cdp_mutations",
|
|
119
|
+
rate: "fast",
|
|
120
|
+
timestamp: new Date().toISOString(),
|
|
121
|
+
data: {
|
|
122
|
+
type: "cdp_mutations",
|
|
123
|
+
mutations,
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* MEDIUM rate: take a DOM snapshot — page URL, title, node count.
|
|
129
|
+
*/
|
|
130
|
+
async pollSnapshot(cdpClient) {
|
|
131
|
+
try {
|
|
132
|
+
const result = await cdpClient.Runtime.evaluate({
|
|
133
|
+
expression: `JSON.stringify({
|
|
134
|
+
url: location.href,
|
|
135
|
+
title: document.title,
|
|
136
|
+
nodeCount: document.querySelectorAll('*').length,
|
|
137
|
+
})`,
|
|
138
|
+
returnByValue: true,
|
|
139
|
+
});
|
|
140
|
+
const data = JSON.parse(result.result.value);
|
|
141
|
+
return {
|
|
142
|
+
source: "cdp_snapshot",
|
|
143
|
+
rate: "medium",
|
|
144
|
+
timestamp: new Date().toISOString(),
|
|
145
|
+
data: {
|
|
146
|
+
type: "cdp_snapshot",
|
|
147
|
+
...data,
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Reset state (e.g., on page navigation).
|
|
157
|
+
*/
|
|
158
|
+
reset() {
|
|
159
|
+
this.mutationBuffer = [];
|
|
160
|
+
this.observerInstalled = false;
|
|
161
|
+
}
|
|
162
|
+
}
|