screenhand 0.1.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (241) hide show
  1. package/README.md +193 -109
  2. package/bin/darwin-arm64/macos-bridge +0 -0
  3. package/dist/mcp-desktop.js +5876 -0
  4. package/dist/scripts/codex-monitor-daemon.js +335 -0
  5. package/dist/scripts/export-help-center.js +112 -0
  6. package/dist/scripts/marketing-loop.js +117 -0
  7. package/dist/scripts/observer-daemon.js +288 -0
  8. package/dist/scripts/orchestrator-daemon.js +399 -0
  9. package/dist/scripts/supervisor-daemon.js +272 -0
  10. package/dist/scripts/threads-campaign.js +208 -0
  11. package/dist/scripts/worker-daemon.js +228 -0
  12. package/dist/src/agent/cli.js +82 -0
  13. package/dist/src/agent/loop.js +274 -0
  14. package/dist/src/community/fetcher.js +109 -0
  15. package/dist/src/community/index.js +6 -0
  16. package/dist/src/community/publisher.js +191 -0
  17. package/dist/src/community/remote-api.js +121 -0
  18. package/dist/src/community/types.js +3 -0
  19. package/dist/src/community/validator.js +95 -0
  20. package/{src/config.ts → dist/src/config.js} +5 -10
  21. package/dist/src/context-tracker.js +489 -0
  22. package/{src/index.ts → dist/src/index.js} +32 -52
  23. package/dist/src/ingestion/coverage-auditor.js +233 -0
  24. package/dist/src/ingestion/doc-parser.js +164 -0
  25. package/dist/src/ingestion/index.js +8 -0
  26. package/dist/src/ingestion/menu-scanner.js +152 -0
  27. package/dist/src/ingestion/reference-merger.js +186 -0
  28. package/dist/src/ingestion/shortcut-extractor.js +180 -0
  29. package/dist/src/ingestion/tutorial-extractor.js +170 -0
  30. package/dist/src/ingestion/types.js +3 -0
  31. package/dist/src/jobs/manager.js +305 -0
  32. package/dist/src/jobs/runner.js +806 -0
  33. package/dist/src/jobs/store.js +102 -0
  34. package/dist/src/jobs/types.js +30 -0
  35. package/dist/src/jobs/worker.js +97 -0
  36. package/dist/src/learning/engine.js +356 -0
  37. package/dist/src/learning/index.js +9 -0
  38. package/dist/src/learning/locator-policy.js +120 -0
  39. package/dist/src/learning/pattern-policy.js +89 -0
  40. package/dist/src/learning/recovery-policy.js +116 -0
  41. package/dist/src/learning/sensor-policy.js +115 -0
  42. package/dist/src/learning/timing-model.js +204 -0
  43. package/dist/src/learning/topology-policy.js +90 -0
  44. package/dist/src/learning/types.js +9 -0
  45. package/dist/src/logging/timeline-logger.js +48 -0
  46. package/dist/src/mcp/mcp-stdio-server.js +464 -0
  47. package/dist/src/mcp/server.js +363 -0
  48. package/dist/src/mcp-entry.js +60 -0
  49. package/dist/src/memory/playbook-seeds.js +200 -0
  50. package/dist/src/memory/recall.js +222 -0
  51. package/dist/src/memory/research.js +104 -0
  52. package/dist/src/memory/seeds.js +101 -0
  53. package/dist/src/memory/service.js +446 -0
  54. package/dist/src/memory/session.js +169 -0
  55. package/dist/src/memory/store.js +451 -0
  56. package/{src/runtime/locator-cache.ts → dist/src/memory/types.js} +1 -17
  57. package/dist/src/monitor/codex-monitor.js +382 -0
  58. package/dist/src/monitor/task-queue.js +97 -0
  59. package/dist/src/monitor/types.js +62 -0
  60. package/dist/src/native/bridge-client.js +412 -0
  61. package/{src/native/macos-bridge-client.ts → dist/src/native/macos-bridge-client.js} +0 -1
  62. package/dist/src/observer/state.js +199 -0
  63. package/dist/src/observer/types.js +43 -0
  64. package/dist/src/orchestrator/state.js +68 -0
  65. package/dist/src/orchestrator/types.js +22 -0
  66. package/dist/src/perception/ax-source.js +162 -0
  67. package/dist/src/perception/cdp-source.js +162 -0
  68. package/dist/src/perception/coordinator.js +771 -0
  69. package/dist/src/perception/frame-differ.js +287 -0
  70. package/dist/src/perception/index.js +22 -0
  71. package/dist/src/perception/manager.js +199 -0
  72. package/dist/src/perception/types.js +47 -0
  73. package/dist/src/perception/vision-source.js +399 -0
  74. package/dist/src/planner/deterministic.js +298 -0
  75. package/dist/src/planner/executor.js +870 -0
  76. package/dist/src/planner/goal-store.js +92 -0
  77. package/dist/src/planner/index.js +21 -0
  78. package/dist/src/planner/planner.js +520 -0
  79. package/dist/src/planner/tool-registry.js +71 -0
  80. package/dist/src/planner/types.js +22 -0
  81. package/dist/src/platform/explorer.js +213 -0
  82. package/dist/src/platform/help-center-markdown.js +527 -0
  83. package/dist/src/platform/learner.js +257 -0
  84. package/dist/src/playbook/engine.js +486 -0
  85. package/dist/src/playbook/index.js +20 -0
  86. package/dist/src/playbook/mcp-recorder.js +204 -0
  87. package/dist/src/playbook/recorder.js +536 -0
  88. package/dist/src/playbook/runner.js +408 -0
  89. package/dist/src/playbook/store.js +312 -0
  90. package/dist/src/playbook/types.js +17 -0
  91. package/dist/src/recovery/detectors.js +156 -0
  92. package/dist/src/recovery/engine.js +327 -0
  93. package/dist/src/recovery/index.js +20 -0
  94. package/dist/src/recovery/strategies.js +274 -0
  95. package/dist/src/recovery/types.js +20 -0
  96. package/dist/src/runtime/accessibility-adapter.js +430 -0
  97. package/dist/src/runtime/app-adapter.js +64 -0
  98. package/dist/src/runtime/applescript-adapter.js +305 -0
  99. package/dist/src/runtime/ax-role-map.js +96 -0
  100. package/dist/src/runtime/browser-adapter.js +52 -0
  101. package/dist/src/runtime/cdp-chrome-adapter.js +521 -0
  102. package/dist/src/runtime/composite-adapter.js +221 -0
  103. package/dist/src/runtime/execution-contract.js +159 -0
  104. package/dist/src/runtime/executor.js +286 -0
  105. package/dist/src/runtime/locator-cache.js +50 -0
  106. package/dist/src/runtime/planning-loop.js +63 -0
  107. package/dist/src/runtime/service.js +432 -0
  108. package/dist/src/runtime/session-manager.js +63 -0
  109. package/dist/src/runtime/state-observer.js +121 -0
  110. package/dist/src/runtime/vision-adapter.js +225 -0
  111. package/dist/src/state/app-map-types.js +72 -0
  112. package/dist/src/state/app-map.js +1974 -0
  113. package/dist/src/state/entity-tracker.js +108 -0
  114. package/dist/src/state/fusion.js +96 -0
  115. package/dist/src/state/index.js +21 -0
  116. package/dist/src/state/ladder-generator.js +236 -0
  117. package/dist/src/state/persistence.js +156 -0
  118. package/dist/src/state/types.js +17 -0
  119. package/dist/src/state/world-model.js +1456 -0
  120. package/dist/src/supervisor/locks.js +186 -0
  121. package/dist/src/supervisor/supervisor.js +403 -0
  122. package/dist/src/supervisor/types.js +30 -0
  123. package/dist/src/test-mcp-protocol.js +154 -0
  124. package/dist/src/types.js +17 -0
  125. package/dist/src/util/atomic-write.js +133 -0
  126. package/dist/src/util/sanitize.js +146 -0
  127. package/dist-app-maps/com.figma.Desktop.json +959 -0
  128. package/dist-app-maps/com.hnc.Discord.json +1146 -0
  129. package/dist-app-maps/notion.id.json +2831 -0
  130. package/dist-playbooks/canva-screenhand-carousel.json +445 -0
  131. package/dist-playbooks/codex-desktop.json +76 -0
  132. package/dist-playbooks/competitor-research-stack.json +122 -0
  133. package/dist-playbooks/davinci-color-grade.json +153 -0
  134. package/dist-playbooks/davinci-edit-timeline.json +162 -0
  135. package/dist-playbooks/davinci-render.json +114 -0
  136. package/dist-playbooks/devto.json +52 -0
  137. package/dist-playbooks/discord.json +41 -0
  138. package/dist-playbooks/google-flow-create-project.json +59 -0
  139. package/dist-playbooks/google-flow-edit-image.json +90 -0
  140. package/dist-playbooks/google-flow-edit-video.json +90 -0
  141. package/dist-playbooks/google-flow-generate-image.json +68 -0
  142. package/dist-playbooks/google-flow-generate-video.json +191 -0
  143. package/dist-playbooks/google-flow-open-project.json +48 -0
  144. package/dist-playbooks/google-flow-open-scenebuilder.json +64 -0
  145. package/dist-playbooks/google-flow-search-assets.json +64 -0
  146. package/dist-playbooks/instagram.json +57 -0
  147. package/dist-playbooks/linkedin.json +52 -0
  148. package/dist-playbooks/n8n.json +43 -0
  149. package/dist-playbooks/reddit.json +52 -0
  150. package/dist-playbooks/threads.json +59 -0
  151. package/dist-playbooks/x-twitter.json +59 -0
  152. package/dist-playbooks/youtube.json +59 -0
  153. package/dist-references/canva.json +646 -0
  154. package/dist-references/codex-desktop.json +305 -0
  155. package/dist-references/davinci-resolve-keyboard.json +594 -0
  156. package/dist-references/davinci-resolve-menu-map.json +1139 -0
  157. package/dist-references/davinci-resolve-menus-batch1.json +116 -0
  158. package/dist-references/davinci-resolve-menus-batch2.json +372 -0
  159. package/dist-references/davinci-resolve-menus-batch3.json +330 -0
  160. package/dist-references/davinci-resolve-menus-batch4.json +297 -0
  161. package/dist-references/davinci-resolve-shortcuts.json +333 -0
  162. package/dist-references/devto.json +317 -0
  163. package/dist-references/discord.json +549 -0
  164. package/dist-references/figma.json +1186 -0
  165. package/dist-references/finder.json +146 -0
  166. package/dist-references/google-ads-transparency.json +95 -0
  167. package/dist-references/google-flow.json +649 -0
  168. package/dist-references/instagram.json +341 -0
  169. package/dist-references/linkedin.json +324 -0
  170. package/dist-references/meta-ad-library.json +86 -0
  171. package/dist-references/n8n.json +387 -0
  172. package/dist-references/notes.json +27 -0
  173. package/dist-references/notion.json +163 -0
  174. package/dist-references/reddit.json +341 -0
  175. package/dist-references/threads.json +337 -0
  176. package/dist-references/x-twitter.json +403 -0
  177. package/dist-references/youtube.json +373 -0
  178. package/native/macos-bridge/Package.swift +1 -0
  179. package/native/macos-bridge/Sources/AccessibilityBridge.swift +257 -36
  180. package/native/macos-bridge/Sources/AppManagement.swift +212 -2
  181. package/native/macos-bridge/Sources/CoreGraphicsBridge.swift +348 -53
  182. package/native/macos-bridge/Sources/StreamCapture.swift +136 -0
  183. package/native/macos-bridge/Sources/VisionBridge.swift +165 -7
  184. package/native/macos-bridge/Sources/main.swift +169 -16
  185. package/native/windows-bridge/Program.cs +5 -0
  186. package/native/windows-bridge/ScreenCapture.cs +124 -0
  187. package/package.json +29 -4
  188. package/scripts/postinstall.cjs +127 -0
  189. package/.claude/commands/automate.md +0 -28
  190. package/.claude/commands/debug-ui.md +0 -19
  191. package/.claude/commands/screenshot.md +0 -15
  192. package/.github/FUNDING.yml +0 -1
  193. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -27
  194. package/.github/ISSUE_TEMPLATE/feature_request.md +0 -20
  195. package/.mcp.json +0 -8
  196. package/DESKTOP_MCP_GUIDE.md +0 -92
  197. package/SECURITY.md +0 -44
  198. package/docs/architecture.md +0 -47
  199. package/install-skills.sh +0 -19
  200. package/mcp-bridge.ts +0 -271
  201. package/mcp-desktop.ts +0 -1221
  202. package/playbooks/instagram.json +0 -41
  203. package/playbooks/instagram_v2.json +0 -201
  204. package/playbooks/x_v1.json +0 -211
  205. package/scripts/devpost-live-loop.mjs +0 -421
  206. package/src/logging/timeline-logger.ts +0 -55
  207. package/src/mcp/server.ts +0 -449
  208. package/src/memory/recall.ts +0 -191
  209. package/src/memory/research.ts +0 -146
  210. package/src/memory/seeds.ts +0 -123
  211. package/src/memory/session.ts +0 -201
  212. package/src/memory/store.ts +0 -434
  213. package/src/memory/types.ts +0 -69
  214. package/src/native/bridge-client.ts +0 -239
  215. package/src/runtime/accessibility-adapter.ts +0 -487
  216. package/src/runtime/app-adapter.ts +0 -169
  217. package/src/runtime/applescript-adapter.ts +0 -376
  218. package/src/runtime/ax-role-map.ts +0 -102
  219. package/src/runtime/browser-adapter.ts +0 -129
  220. package/src/runtime/cdp-chrome-adapter.ts +0 -676
  221. package/src/runtime/composite-adapter.ts +0 -274
  222. package/src/runtime/executor.ts +0 -396
  223. package/src/runtime/planning-loop.ts +0 -81
  224. package/src/runtime/service.ts +0 -448
  225. package/src/runtime/session-manager.ts +0 -50
  226. package/src/runtime/state-observer.ts +0 -136
  227. package/src/runtime/vision-adapter.ts +0 -297
  228. package/src/types.ts +0 -297
  229. package/tests/bridge-client.test.ts +0 -176
  230. package/tests/browser-stealth.test.ts +0 -210
  231. package/tests/composite-adapter.test.ts +0 -64
  232. package/tests/mcp-server.test.ts +0 -151
  233. package/tests/memory-recall.test.ts +0 -339
  234. package/tests/memory-research.test.ts +0 -159
  235. package/tests/memory-seeds.test.ts +0 -120
  236. package/tests/memory-store.test.ts +0 -392
  237. package/tests/types.test.ts +0 -92
  238. package/tsconfig.check.json +0 -17
  239. package/tsconfig.json +0 -19
  240. package/vitest.config.ts +0 -8
  241. /package/{playbooks → dist-references}/devpost.json +0 -0
@@ -0,0 +1,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
+ }